From ac42a13f142f33842cff70fe1a9ba96f624a2ca5 Mon Sep 17 00:00:00 2001 From: Davin Date: Mon, 15 Aug 2022 00:41:43 +1000 Subject: [PATCH 1/8] Created a .NET 5 branch of VGMS --- DLS2/Chunks/Chunk.cs | 98 + DLS2/Chunks/CollectionHeaderChunk.cs | 29 + DLS2/Chunks/ConditionalChunk.cs | 36 + DLS2/Chunks/DLSIDChunk.cs | 45 + DLS2/Chunks/DataChunk.cs | 10 + DLS2/Chunks/FormatChunk.cs | 105 + DLS2/Chunks/InfoSubChunk.cs | 53 + DLS2/Chunks/InstrumentHeaderChunk.cs | 46 + DLS2/Chunks/Level1ArticulatorChunk.cs | 119 + DLS2/Chunks/Level2ArticulatorChunk.cs | 119 + DLS2/Chunks/ListChunk.cs | 112 + DLS2/Chunks/PoolTableChunk.cs | 71 + DLS2/Chunks/RawDataChunk.cs | 50 + DLS2/Chunks/RegionHeaderChunk.cs | 52 + DLS2/Chunks/UnsupportedChunk.cs | 10 + DLS2/Chunks/VersionChunk.cs | 14 + DLS2/Chunks/WaveLinkChunk.cs | 43 + DLS2/Chunks/WaveSampleChunk.cs | 69 + DLS2/DLS.cs | 235 + DLS2/DLS2.csproj | 30 + DLS2/Enums/Level1ArticulatorEnums.cs | 44 + DLS2/Enums/Level2ArticulatorEnums.cs | 69 + DLS2/Enums/WaveFormat.cs | 15 + DLS2/Enums/WaveLinkChannels.cs | 28 + DLS2/Enums/WaveLinkOptions.cs | 12 + DLS2/Enums/WaveSampleLoop.cs | 8 + DLS2/Enums/WaveSampleOptions.cs | 12 + DLS2/Structs/ConnectionBlock.cs | 163 + DLS2/Structs/DLSID.cs | 122 + DLS2/Structs/MIDILocale.cs | 87 + DLS2/Structs/Range.cs | 28 + DLS2/Structs/WaveInfo.cs | 32 + DLS2/Structs/WaveSampleLoop.cs | 33 + EndianBinaryIO/Attributes.cs | 117 + EndianBinaryIO/EndianBinaryIO.csproj | 39 + EndianBinaryIO/EndianBinaryReader.cs | 925 ++ EndianBinaryIO/EndianBinaryWriter.cs | 926 ++ EndianBinaryIO/EndianBitConverter.cs | 587 + EndianBinaryIO/Enums.cs | 18 + EndianBinaryIO/IBinarySerializable.cs | 8 + EndianBinaryIO/Utils.cs | 178 + .../DLS2/.gitattributes | 63 + Legacy Repo (.NET Framework)/DLS2/.gitignore | 262 + Legacy Repo (.NET Framework)/DLS2/DLS2.sln | 25 + .../DLS2/DLS2/Chunks/Chunk.cs | 98 + .../DLS2/DLS2/Chunks/CollectionHeaderChunk.cs | 29 + .../DLS2/DLS2/Chunks/ConditionalChunk.cs | 36 + .../DLS2/DLS2/Chunks/DLSIDChunk.cs | 45 + .../DLS2/DLS2/Chunks/DataChunk.cs | 10 + .../DLS2/DLS2/Chunks/FormatChunk.cs | 105 + .../DLS2/DLS2/Chunks/InfoSubChunk.cs | 53 + .../DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs | 46 + .../DLS2/Chunks/Level1ArticulatorChunk.cs | 119 + .../DLS2/Chunks/Level2ArticulatorChunk.cs | 119 + .../DLS2/DLS2/Chunks/ListChunk.cs | 112 + .../DLS2/DLS2/Chunks/PoolTableChunk.cs | 71 + .../DLS2/DLS2/Chunks/RawDataChunk.cs | 50 + .../DLS2/DLS2/Chunks/RegionHeaderChunk.cs | 52 + .../DLS2/DLS2/Chunks/UnsupportedChunk.cs | 10 + .../DLS2/DLS2/Chunks/VersionChunk.cs | 14 + .../DLS2/DLS2/Chunks/WaveLinkChunk.cs | 43 + .../DLS2/DLS2/Chunks/WaveSampleChunk.cs | 69 + Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs | 235 + .../DLS2/DLS2/DLS2.csproj | 30 + .../DLS2/DLS2/Enums/Level1ArticulatorEnums.cs | 44 + .../DLS2/DLS2/Enums/Level2ArticulatorEnums.cs | 69 + .../DLS2/DLS2/Enums/WaveFormat.cs | 15 + .../DLS2/DLS2/Enums/WaveLinkChannels.cs | 28 + .../DLS2/DLS2/Enums/WaveLinkOptions.cs | 12 + .../DLS2/DLS2/Enums/WaveSampleLoop.cs | 8 + .../DLS2/DLS2/Enums/WaveSampleOptions.cs | 12 + .../DLS2/DLS2/Structs/ConnectionBlock.cs | 163 + .../DLS2/DLS2/Structs/DLSID.cs | 122 + .../DLS2/DLS2/Structs/MIDILocale.cs | 87 + .../DLS2/DLS2/Structs/Range.cs | 28 + .../DLS2/DLS2/Structs/WaveInfo.cs | 32 + .../DLS2/DLS2/Structs/WaveSampleLoop.cs | 33 + Legacy Repo (.NET Framework)/DLS2/LICENSE.md | 23 + .../SoundFont2/.gitattributes | 63 + .../SoundFont2/.gitignore | 262 + .../SoundFont2/LICENSE.md | 23 + .../SoundFont2/README.md | 11 + .../SoundFont2/SoundFont2.sln | 22 + .../SoundFont2/SoundFont2/SF2.cs | 152 + .../SoundFont2/SoundFont2/SF2Chunks.cs | 1105 ++ .../SoundFont2/SoundFont2/SF2Types.cs | 142 + .../SoundFont2/SoundFont2/SoundFont2.csproj | 26 + Legacy Repo (.NET Framework)/VGMusicStudio | 1 + .../VG Music Studio/NuGetUpgradeLog.html | 223 + .../VG Music Studio/VG Music Studio.csproj | 411 + .../655cc60b/VG Music Studio/packages.config | 65 + .../CellEditing/CellEditKeyEngine.cs | 520 + ObjectListView/CellEditing/CellEditors.cs | 325 + ObjectListView/CellEditing/EditorRegistry.cs | 213 + ObjectListView/CustomDictionary.xml | 46 + ObjectListView/DataListView.cs | 240 + ObjectListView/DataTreeListView.cs | 240 + ObjectListView/DragDrop/DragSource.cs | 219 + ObjectListView/DragDrop/DropSink.cs | 1562 +++ ObjectListView/DragDrop/OLVDataObject.cs | 185 + ObjectListView/FastDataListView.cs | 169 + ObjectListView/FastObjectListView.cs | 422 + ObjectListView/Filtering/Cluster.cs | 125 + .../Filtering/ClusteringStrategy.cs | 189 + .../Filtering/ClustersFromGroupsStrategy.cs | 70 + .../Filtering/DateTimeClusteringStrategy.cs | 187 + ObjectListView/Filtering/FilterMenuBuilder.cs | 369 + ObjectListView/Filtering/Filters.cs | 489 + .../Filtering/FlagClusteringStrategy.cs | 160 + ObjectListView/Filtering/ICluster.cs | 56 + .../Filtering/IClusteringStrategy.cs | 80 + ObjectListView/Filtering/TextMatchFilter.cs | 642 + ObjectListView/FullClassDiagram.cd | 1261 ++ ObjectListView/Implementation/Attributes.cs | 335 + ObjectListView/Implementation/Comparers.cs | 330 + .../Implementation/DataSourceAdapter.cs | 628 + ObjectListView/Implementation/Delegates.cs | 168 + ObjectListView/Implementation/DragSource.cs | 407 + ObjectListView/Implementation/DropSink.cs | 1402 ++ ObjectListView/Implementation/Enums.cs | 104 + ObjectListView/Implementation/Events.cs | 2514 ++++ .../Implementation/GroupingParameters.cs | 204 + ObjectListView/Implementation/Groups.cs | 761 ++ ObjectListView/Implementation/Munger.cs | 568 + .../Implementation/NativeMethods.cs | 1223 ++ .../Implementation/NullableDictionary.cs | 87 + ObjectListView/Implementation/OLVListItem.cs | 325 + .../Implementation/OLVListSubItem.cs | 173 + .../Implementation/OlvListViewHitTestInfo.cs | 388 + .../Implementation/TreeDataSourceAdapter.cs | 262 + .../Implementation/VirtualGroups.cs | 341 + .../Implementation/VirtualListDataSource.cs | 349 + ObjectListView/OLVColumn.cs | 1909 +++ ObjectListView/ObjectListView.DesignTime.cs | 550 + ObjectListView/ObjectListView.FxCop | 3521 +++++ ObjectListView/ObjectListView.cs | 10924 ++++++++++++++++ ObjectListView/ObjectListView.shfb | 47 + ObjectListView/ObjectListView2005.csproj | 182 + ObjectListView/ObjectListView2008.csproj | 188 + .../ObjectListView2008.ncrunchproject | 16 + ObjectListView/ObjectListView2010.csproj | 188 + .../ObjectListView2010.ncrunchproject | 27 + ObjectListView/ObjectListView2012.csproj | 194 + .../ObjectListView2012.ncrunchproject | 22 + ObjectListView/ObjectListView2012.nuspec | 22 + .../ObjectListView2012.sln.DotSettings | 7 + .../ObjectListView2012.v2.ncrunchproject | 22 + ObjectListView/ObjectListView2019.csproj | 54 + ObjectListView/ObjectListView2019.nuspec | 22 + ObjectListView/Properties/AssemblyInfo.cs | 36 + .../Properties/Resources.Designer.cs | 113 + ObjectListView/Properties/Resources.resx | 137 + ObjectListView/Rendering/Adornments.cs | 743 ++ ObjectListView/Rendering/Decorations.cs | 973 ++ ObjectListView/Rendering/Overlays.cs | 302 + ObjectListView/Rendering/Renderers.cs | 3887 ++++++ ObjectListView/Rendering/Styles.cs | 400 + ObjectListView/Rendering/TreeRenderer.cs | 309 + ObjectListView/Resources/clear-filter.png | Bin 0 -> 1381 bytes ObjectListView/Resources/coffee.jpg | Bin 0 -> 73464 bytes ObjectListView/Resources/filter-icons3.png | Bin 0 -> 1305 bytes ObjectListView/Resources/filter.png | Bin 0 -> 1331 bytes ObjectListView/Resources/sort-ascending.png | Bin 0 -> 1364 bytes ObjectListView/Resources/sort-descending.png | Bin 0 -> 1371 bytes ObjectListView/SubControls/GlassPanelForm.cs | 459 + ObjectListView/SubControls/HeaderControl.cs | 1230 ++ .../SubControls/ToolStripCheckedListBox.cs | 189 + ObjectListView/SubControls/ToolTipControl.cs | 699 + ObjectListView/TreeListView.cs | 2269 ++++ .../Utilities/ColumnSelectionForm.Designer.cs | 190 + .../Utilities/ColumnSelectionForm.cs | 263 + .../Utilities/ColumnSelectionForm.resx | 120 + ObjectListView/Utilities/Generator.cs | 563 + ObjectListView/Utilities/OLVExporter.cs | 277 + .../Utilities/TypedObjectListView.cs | 561 + ObjectListView/VirtualObjectListView.cs | 1255 ++ ObjectListView/olv-keyfile.snk | Bin 0 -> 596 bytes .../Icon_-_General_MIDI.png | Bin 0 -> 928 bytes .../Sanford.Multimedia.Midi.6.1.2.nuspec | 32 + .../Sanford.Multimedia.Midi.6.2.0.nuspec | 33 + .../Sanford.Multimedia.Midi.6.2.1.nuspec | 33 + .../Sanford.Multimedia.Midi.6.3.0.nuspec | 31 + .../Sanford.Multimedia.Midi.6.4.0.nuspec | 31 + .../Sanford.Multimedia.Midi.6.4.1.nuspec | 31 + .../Sanford.Multimedia.Midi.6.4.2.nuspec | 31 + .../Sanford.Multimedia.Midi.6.4.3.nuspec | 31 + .../Sanford.Multimedia.Midi.6.5.0.nuspec | 32 + .../Sanford.Multimedia.Midi.6.6.0.nuspec | 33 + .../Sanford.Collections/Deque.cs | 935 ++ .../Generic/Deque/GenericDeque.Enumerator.cs | 151 + .../Generic/Deque/GenericDeque.Node.cs | 61 + .../Deque/GenericDeque.Synchronized.cs | 188 + .../Generic/Deque/GenericDeque.cs | 601 + .../Generic/UndoableList/ICommand.cs | 12 + .../Generic/UndoableList/UndoManager.cs | 113 + .../UndoableList/UndoableList.Commands.cs | 511 + .../Generic/UndoableList/UndoableList.Test.cs | 274 + .../Generic/UndoableList/UndoableList.cs | 410 + .../AVL Tree Classes/AvlEnumerator.cs | 167 + .../Immutable/AVL Tree Classes/AvlNode.cs | 440 + .../Immutable/AVL Tree Classes/IAvlNode.cs | 91 + .../Immutable/AVL Tree Classes/NullAvlNode.cs | 124 + .../Sanford.Collections/Immutable/Array.cs | 213 + .../Immutable/ArrayList.cs | 694 + .../RAL Helper Classes/RalEnumerator.cs | 211 + .../RAL Helper Classes/RalTopNode.cs | 155 + .../RAL Helper Classes/RalTreeNode.cs | 250 + .../Immutable/RandomAccessList.cs | 302 + .../Immutable/SortedList.cs | 509 + .../Sanford.Collections/Immutable/Stack.cs | 315 + .../Sanford.Collections/PriorityQueue.cs | 887 ++ .../Sanford.Collections/SkipList.cs | 1110 ++ .../Sanford.Multimedia.Midi.Core.csproj | 12 + .../Sanford.Multimedia.Midi/Clocks/IClock.cs | 89 + .../Clocks/MidiInternalClock.cs | 381 + .../Clocks/PpqnClock.cs | 259 + .../InputDevice.Construction.cs | 79 + .../InputDevice Class/InputDevice.Events.cs | 202 + .../InputDevice Class/InputDevice.Fields.cs | 71 + .../InputDevice.Messaging.cs | 338 + .../InputDevice.Properties.cs | 79 + .../InputDevice.PublicMethods.cs | 214 + .../InputDevice Class/InputDevice.Win32.cs | 95 + .../InputDevice Class/InputDevice.cs | 134 + .../InputDevice Class/MidiInCaps.cs | 79 + .../Device Classes/MidiDevice.cs | 120 + .../Device Classes/MidiDeviceException.cs | 73 + .../Device Classes/MidiHeader.cs | 101 + .../Device Classes/MidiHeaderBuilder.cs | 248 + .../OutputDevice Classes/MidiOutCaps.cs | 107 + .../OutputDevice Classes/NoOpEventArgs.cs | 59 + .../OutputDevice Classes/OutputDevice.cs | 297 + .../OutputDevice Classes/OutputDeviceBase.cs | 357 + .../OutputDevice Classes/OutputStream.cs | 617 + .../Sanford.Multimedia.Midi/GeneralMidi.cs | 171 + .../Messages/ChannelMessage.cs | 746 ++ .../EventArgs/ChannelMessageEventArgs.cs | 25 + .../EventArgs/InvalidShortMessageEventArgs.cs | 25 + .../EventArgs/InvalidSysExMessageEventArgs.cs | 25 + .../EventArgs/MetaMessageEventArgs.cs | 25 + .../Messages/EventArgs/MidiEventArgs.cs | 11 + .../EventArgs/ShortMessageEventArgs.cs | 56 + .../EventArgs/SysCommonMessageEventArgs.cs | 25 + .../EventArgs/SysExMessageEventArgs.cs | 25 + .../EventArgs/SysRealtimeMessageEventArgs.cs | 38 + .../Messages/IMidiMessage.cs | 96 + .../Message Builders/ChannelMessageBuilder.cs | 256 + .../Message Builders/IMessageBuilder.cs | 51 + .../Message Builders/KeySignatureBuilder.cs | 424 + .../Message Builders/MetaTextBuilder.cs | 416 + .../SongPositionPointerBuilder.cs | 266 + .../SysCommonMessageBuilder.cs | 233 + .../Message Builders/TempoChangeBuilder.cs | 242 + .../Message Builders/TimeSignatureBuilder.cs | 277 + .../Messages/MessageDispatcher.cs | 186 + .../Messages/MessagesHierarchy.cd | 120 + .../Messages/MetaMessage.cs | 513 + .../MidiEvents/InputDeviceMidiEvents.cs | 127 + .../Messages/MidiEvents/MergeMidiEvents.cs | 151 + .../Messages/MidiEvents/MidiEvents.cs | 53 + .../MidiEvents/OutputDeviceEventSink.cs | 118 + .../Messages/ShortMessage.cs | 261 + .../Messages/SysCommonMessage.cs | 257 + .../Messages/SysExMessage.cs | 266 + .../Messages/SysRealtimeMessage.cs | 227 + .../MidiNoteConverter.cs | 156 + .../Processing/ChannelChaser.cs | 167 + .../Processing/ChannelStopper.cs | 211 + .../Processing/ChasedEventArgs.cs | 24 + .../Processing/StoppedEventArgs.cs | 24 + .../Sequencing/MidiEvent.cs | 148 + .../Sequencing/MidiFileProperties.cs | 384 + .../Sequencing/RecordingSession.cs | 97 + .../Sequencing/Sequence.cs | 847 ++ .../Sequencing/Sequencer.cs | 386 + .../Track Classes/Track.Iterators.cs | 135 + .../Sequencing/Track Classes/Track.Test.cs | 128 + .../Sequencing/Track Classes/Track.cs | 607 + .../Sequencing/Track Classes/TrackReader.cs | 448 + .../Sequencing/Track Classes/TrackWriter.cs | 256 + .../Sanford.Multimedia.Timers/ITimer.cs | 95 + .../Sanford.Multimedia.Timers/ThreadTimer.cs | 387 + .../ThreadTimerQueue.cs | 175 + .../Sanford.Multimedia.Timers/Time.cs | 109 + .../Sanford.Multimedia.Timers/Timer.cs | 719 + .../Sanford.Multimedia.Timers/TimerFactory.cs | 58 + .../Sanford.Multimedia/Device.cs | 166 + .../Sanford.Multimedia/DeviceException.cs | 117 + .../Sanford.Multimedia/ErrorEventArgs.cs | 24 + .../Sanford.Multimedia/Key.cs | 73 + .../Sanford.Multimedia/Note.cs | 127 + .../Sanford.Threading/AsyncResult.cs | 177 + .../DelegateQueue.AsyncResult.cs | 121 + .../DelegateQueue/DelegateQueue.cs | 843 ++ .../DelegateQueue/PostCompletedEventArgs.cs | 59 + .../DelegateScheduler/DelegateScheduler.cs | 568 + .../DelegateScheduler/Task.cs | 176 + .../InvokeCompletedEventArgs.cs | 81 + .../docs/Sanford.Multimedia.Midi.XML | 5378 ++++++++ SoundFont2/SF2.cs | 150 + SoundFont2/SF2Chunks.cs | 1105 ++ SoundFont2/SF2Types.cs | 142 + SoundFont2/SoundFont2.csproj | 26 + VG Music Studio.backup/AlphaDream.yaml | 201 + VG Music Studio.backup/Config.yaml | 137 + VG Music Studio.backup/Core/ADPCMDecoder.cs | 90 + VG Music Studio.backup/Core/Assembler.cs | 351 + VG Music Studio.backup/Core/Config.cs | 90 + VG Music Studio.backup/Core/Engine.cs | 92 + .../Core/GBA/AlphaDream/Channel.cs | 237 + .../Core/GBA/AlphaDream/Commands.cs | 113 + .../Core/GBA/AlphaDream/Config.cs | 220 + .../Core/GBA/AlphaDream/Enums.cs | 16 + .../Core/GBA/AlphaDream/Mixer.cs | 137 + .../Core/GBA/AlphaDream/Player.cs | 696 + .../Core/GBA/AlphaDream/SoundFontSaver_DLS.cs | 180 + .../Core/GBA/AlphaDream/SoundFontSaver_SF2.cs | 97 + .../Core/GBA/AlphaDream/Structs.cs | 33 + .../Core/GBA/AlphaDream/Track.cs | 69 + .../Core/GBA/MP2K/Channel.cs | 777 ++ .../Core/GBA/MP2K/Commands.cs | 193 + .../Core/GBA/MP2K/Config.cs | 242 + VG Music Studio.backup/Core/GBA/MP2K/Enums.cs | 72 + VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs | 275 + .../Core/GBA/MP2K/Player.cs | 1510 +++ .../Core/GBA/MP2K/Structs.cs | 73 + VG Music Studio.backup/Core/GBA/MP2K/Track.cs | 174 + VG Music Studio.backup/Core/GBA/MP2K/Utils.cs | 174 + VG Music Studio.backup/Core/GBA/Utils.cs | 13 + VG Music Studio.backup/Core/GlobalConfig.cs | 109 + VG Music Studio.backup/Core/Mixer.cs | 87 + .../Core/NDS/DSE/Channel.cs | 368 + .../Core/NDS/DSE/Commands.cs | 130 + VG Music Studio.backup/Core/NDS/DSE/Config.cs | 45 + VG Music Studio.backup/Core/NDS/DSE/Enums.cs | 22 + VG Music Studio.backup/Core/NDS/DSE/Mixer.cs | 220 + VG Music Studio.backup/Core/NDS/DSE/Player.cs | 1040 ++ VG Music Studio.backup/Core/NDS/DSE/SMD.cs | 61 + VG Music Studio.backup/Core/NDS/DSE/SWD.cs | 471 + VG Music Studio.backup/Core/NDS/DSE/Track.cs | 72 + VG Music Studio.backup/Core/NDS/DSE/Utils.cs | 53 + .../Core/NDS/SDAT/Channel.cs | 387 + .../Core/NDS/SDAT/Commands.cs | 439 + .../Core/NDS/SDAT/Config.cs | 40 + VG Music Studio.backup/Core/NDS/SDAT/Enums.cs | 40 + .../Core/NDS/SDAT/FileHeader.cs | 31 + VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs | 252 + .../Core/NDS/SDAT/Player.cs | 1680 +++ VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs | 184 + VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs | 234 + VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs | 28 + VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs | 65 + VG Music Studio.backup/Core/NDS/SDAT/Track.cs | 193 + VG Music Studio.backup/Core/NDS/SDAT/Utils.cs | 345 + VG Music Studio.backup/Core/NDS/Utils.cs | 7 + VG Music Studio.backup/Core/Player.cs | 36 + VG Music Studio.backup/Core/SongEvent.cs | 24 + VG Music Studio.backup/Core/VGMSDebug.cs | 112 + .../Dependencies/DLS2.dll | Bin .../Dependencies/Sanford.Multimedia.Midi.dll | Bin .../Dependencies/SoundFont2.dll | Bin VG Music Studio.backup/MP2K.yaml | 1443 ++ VG Music Studio.backup/MPlayDef.s | 465 + VG Music Studio.backup/Program.cs | 30 + .../Properties/AssemblyInfo.cs | 0 VG Music Studio.backup/Properties/Icon.ico | Bin 0 -> 3238 bytes VG Music Studio.backup/Properties/Icon16.png | Bin 0 -> 359 bytes VG Music Studio.backup/Properties/Icon24.png | Bin 0 -> 476 bytes VG Music Studio.backup/Properties/Icon32.png | Bin 0 -> 596 bytes VG Music Studio.backup/Properties/Icon48.png | Bin 0 -> 870 bytes VG Music Studio.backup/Properties/Icon528.png | Bin 0 -> 10133 bytes VG Music Studio.backup/Properties/Next.ico | Bin 0 -> 140062 bytes VG Music Studio.backup/Properties/Next.png | Bin 0 -> 4026 bytes VG Music Studio.backup/Properties/Pause.ico | Bin 0 -> 118062 bytes VG Music Studio.backup/Properties/Pause.png | Bin 0 -> 1450 bytes VG Music Studio.backup/Properties/Play.ico | Bin 0 -> 140062 bytes VG Music Studio.backup/Properties/Play.png | Bin 0 -> 2298 bytes .../Properties/Playlist.png | Bin 0 -> 3045 bytes .../Properties/Previous.ico | Bin 0 -> 140062 bytes .../Properties/Previous.png | Bin 0 -> 3708 bytes .../Properties/Resources.Designer.cs | 133 + .../Properties/Resources.resx | 142 + .../Properties/Settings.Designer.cs | 26 + .../Properties/Settings.settings | 7 + VG Music Studio.backup/Properties/Song.png | Bin 0 -> 2328 bytes .../Properties/Strings.Designer.cs | 747 ++ .../Properties/Strings.es.resx | 348 + .../Properties/Strings.it.resx | 348 + .../Properties/Strings.resx | 376 + VG Music Studio.backup/UI/ColorSlider.cs | 485 + .../UI/FlexibleMessageBox.cs | 697 + VG Music Studio.backup/UI/ImageComboBox.cs | 62 + VG Music Studio.backup/UI/MainForm.cs | 836 ++ VG Music Studio.backup/UI/PianoControl.cs | 183 + VG Music Studio.backup/UI/SongInfoControl.cs | 354 + VG Music Studio.backup/UI/Theme.cs | 165 + VG Music Studio.backup/UI/TrackViewer.cs | 113 + VG Music Studio.backup/UI/ValueTextBox.cs | 104 + .../Util/BetterExceptions.cs | 24 + VG Music Studio.backup/Util/HSLColor.cs | 161 + VG Music Studio.backup/Util/SampleUtils.cs | 16 + VG Music Studio.backup/Util/TimeBarrier.cs | 60 + VG Music Studio.backup/Util/Utils.cs | 153 + VG Music Studio.backup/VG Music Studio.csproj | 433 + VG Music Studio.backup/app.config | 11 + VG Music Studio.backup/midi2agb.exe | Bin 0 -> 3404655 bytes VG Music Studio.backup/upgrade.backup | 1 + VG Music Studio.sln | 36 +- .../Core/GBA/AlphaDream/SoundFontSaver_DLS.cs | 29 +- .../Core/GBA/AlphaDream/SoundFontSaver_SF2.cs | 12 +- VG Music Studio/Core/GBA/MP2K/Player.cs | 7 +- VG Music Studio/Core/Mixer.cs | 29 +- VG Music Studio/Core/VGMSDebug.cs | 2 +- .../Properties/Strings.Designer.cs | 13 +- VG Music Studio/Properties/Strings.resx | 4 + VG Music Studio/UI/MainForm.cs | 135 +- VG Music Studio/VG Music Studio.csproj | 217 +- VG Music Studio/packages.config | 9 - 418 files changed, 115706 insertions(+), 289 deletions(-) create mode 100644 DLS2/Chunks/Chunk.cs create mode 100644 DLS2/Chunks/CollectionHeaderChunk.cs create mode 100644 DLS2/Chunks/ConditionalChunk.cs create mode 100644 DLS2/Chunks/DLSIDChunk.cs create mode 100644 DLS2/Chunks/DataChunk.cs create mode 100644 DLS2/Chunks/FormatChunk.cs create mode 100644 DLS2/Chunks/InfoSubChunk.cs create mode 100644 DLS2/Chunks/InstrumentHeaderChunk.cs create mode 100644 DLS2/Chunks/Level1ArticulatorChunk.cs create mode 100644 DLS2/Chunks/Level2ArticulatorChunk.cs create mode 100644 DLS2/Chunks/ListChunk.cs create mode 100644 DLS2/Chunks/PoolTableChunk.cs create mode 100644 DLS2/Chunks/RawDataChunk.cs create mode 100644 DLS2/Chunks/RegionHeaderChunk.cs create mode 100644 DLS2/Chunks/UnsupportedChunk.cs create mode 100644 DLS2/Chunks/VersionChunk.cs create mode 100644 DLS2/Chunks/WaveLinkChunk.cs create mode 100644 DLS2/Chunks/WaveSampleChunk.cs create mode 100644 DLS2/DLS.cs create mode 100644 DLS2/DLS2.csproj create mode 100644 DLS2/Enums/Level1ArticulatorEnums.cs create mode 100644 DLS2/Enums/Level2ArticulatorEnums.cs create mode 100644 DLS2/Enums/WaveFormat.cs create mode 100644 DLS2/Enums/WaveLinkChannels.cs create mode 100644 DLS2/Enums/WaveLinkOptions.cs create mode 100644 DLS2/Enums/WaveSampleLoop.cs create mode 100644 DLS2/Enums/WaveSampleOptions.cs create mode 100644 DLS2/Structs/ConnectionBlock.cs create mode 100644 DLS2/Structs/DLSID.cs create mode 100644 DLS2/Structs/MIDILocale.cs create mode 100644 DLS2/Structs/Range.cs create mode 100644 DLS2/Structs/WaveInfo.cs create mode 100644 DLS2/Structs/WaveSampleLoop.cs create mode 100644 EndianBinaryIO/Attributes.cs create mode 100644 EndianBinaryIO/EndianBinaryIO.csproj create mode 100644 EndianBinaryIO/EndianBinaryReader.cs create mode 100644 EndianBinaryIO/EndianBinaryWriter.cs create mode 100644 EndianBinaryIO/EndianBitConverter.cs create mode 100644 EndianBinaryIO/Enums.cs create mode 100644 EndianBinaryIO/IBinarySerializable.cs create mode 100644 EndianBinaryIO/Utils.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/.gitattributes create mode 100644 Legacy Repo (.NET Framework)/DLS2/.gitignore create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2.sln create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Chunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/CollectionHeaderChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ConditionalChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DLSIDChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DataChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/FormatChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InfoSubChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level1ArticulatorChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level2ArticulatorChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ListChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/PoolTableChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RawDataChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RegionHeaderChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/UnsupportedChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/VersionChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveLinkChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveSampleChunk.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/DLS2.csproj create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level1ArticulatorEnums.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level2ArticulatorEnums.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveFormat.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkChannels.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkOptions.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleLoop.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleOptions.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/ConnectionBlock.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/DLSID.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/MIDILocale.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/Range.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveInfo.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveSampleLoop.cs create mode 100644 Legacy Repo (.NET Framework)/DLS2/LICENSE.md create mode 100644 Legacy Repo (.NET Framework)/SoundFont2/.gitattributes create mode 100644 Legacy Repo (.NET Framework)/SoundFont2/.gitignore create mode 100644 Legacy Repo (.NET Framework)/SoundFont2/LICENSE.md create mode 100644 Legacy Repo (.NET Framework)/SoundFont2/README.md create mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2.sln create mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2.cs create mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Chunks.cs create mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Types.cs create mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SoundFont2.csproj create mode 160000 Legacy Repo (.NET Framework)/VGMusicStudio create mode 100644 MigrationBackup/655cc60b/VG Music Studio/NuGetUpgradeLog.html create mode 100644 MigrationBackup/655cc60b/VG Music Studio/VG Music Studio.csproj create mode 100644 MigrationBackup/655cc60b/VG Music Studio/packages.config create mode 100644 ObjectListView/CellEditing/CellEditKeyEngine.cs create mode 100644 ObjectListView/CellEditing/CellEditors.cs create mode 100644 ObjectListView/CellEditing/EditorRegistry.cs create mode 100644 ObjectListView/CustomDictionary.xml create mode 100644 ObjectListView/DataListView.cs create mode 100644 ObjectListView/DataTreeListView.cs create mode 100644 ObjectListView/DragDrop/DragSource.cs create mode 100644 ObjectListView/DragDrop/DropSink.cs create mode 100644 ObjectListView/DragDrop/OLVDataObject.cs create mode 100644 ObjectListView/FastDataListView.cs create mode 100644 ObjectListView/FastObjectListView.cs create mode 100644 ObjectListView/Filtering/Cluster.cs create mode 100644 ObjectListView/Filtering/ClusteringStrategy.cs create mode 100644 ObjectListView/Filtering/ClustersFromGroupsStrategy.cs create mode 100644 ObjectListView/Filtering/DateTimeClusteringStrategy.cs create mode 100644 ObjectListView/Filtering/FilterMenuBuilder.cs create mode 100644 ObjectListView/Filtering/Filters.cs create mode 100644 ObjectListView/Filtering/FlagClusteringStrategy.cs create mode 100644 ObjectListView/Filtering/ICluster.cs create mode 100644 ObjectListView/Filtering/IClusteringStrategy.cs create mode 100644 ObjectListView/Filtering/TextMatchFilter.cs create mode 100644 ObjectListView/FullClassDiagram.cd create mode 100644 ObjectListView/Implementation/Attributes.cs create mode 100644 ObjectListView/Implementation/Comparers.cs create mode 100644 ObjectListView/Implementation/DataSourceAdapter.cs create mode 100644 ObjectListView/Implementation/Delegates.cs create mode 100644 ObjectListView/Implementation/DragSource.cs create mode 100644 ObjectListView/Implementation/DropSink.cs create mode 100644 ObjectListView/Implementation/Enums.cs create mode 100644 ObjectListView/Implementation/Events.cs create mode 100644 ObjectListView/Implementation/GroupingParameters.cs create mode 100644 ObjectListView/Implementation/Groups.cs create mode 100644 ObjectListView/Implementation/Munger.cs create mode 100644 ObjectListView/Implementation/NativeMethods.cs create mode 100644 ObjectListView/Implementation/NullableDictionary.cs create mode 100644 ObjectListView/Implementation/OLVListItem.cs create mode 100644 ObjectListView/Implementation/OLVListSubItem.cs create mode 100644 ObjectListView/Implementation/OlvListViewHitTestInfo.cs create mode 100644 ObjectListView/Implementation/TreeDataSourceAdapter.cs create mode 100644 ObjectListView/Implementation/VirtualGroups.cs create mode 100644 ObjectListView/Implementation/VirtualListDataSource.cs create mode 100644 ObjectListView/OLVColumn.cs create mode 100644 ObjectListView/ObjectListView.DesignTime.cs create mode 100644 ObjectListView/ObjectListView.FxCop create mode 100644 ObjectListView/ObjectListView.cs create mode 100644 ObjectListView/ObjectListView.shfb create mode 100644 ObjectListView/ObjectListView2005.csproj create mode 100644 ObjectListView/ObjectListView2008.csproj create mode 100644 ObjectListView/ObjectListView2008.ncrunchproject create mode 100644 ObjectListView/ObjectListView2010.csproj create mode 100644 ObjectListView/ObjectListView2010.ncrunchproject create mode 100644 ObjectListView/ObjectListView2012.csproj create mode 100644 ObjectListView/ObjectListView2012.ncrunchproject create mode 100644 ObjectListView/ObjectListView2012.nuspec create mode 100644 ObjectListView/ObjectListView2012.sln.DotSettings create mode 100644 ObjectListView/ObjectListView2012.v2.ncrunchproject create mode 100644 ObjectListView/ObjectListView2019.csproj create mode 100644 ObjectListView/ObjectListView2019.nuspec create mode 100644 ObjectListView/Properties/AssemblyInfo.cs create mode 100644 ObjectListView/Properties/Resources.Designer.cs create mode 100644 ObjectListView/Properties/Resources.resx create mode 100644 ObjectListView/Rendering/Adornments.cs create mode 100644 ObjectListView/Rendering/Decorations.cs create mode 100644 ObjectListView/Rendering/Overlays.cs create mode 100644 ObjectListView/Rendering/Renderers.cs create mode 100644 ObjectListView/Rendering/Styles.cs create mode 100644 ObjectListView/Rendering/TreeRenderer.cs create mode 100644 ObjectListView/Resources/clear-filter.png create mode 100644 ObjectListView/Resources/coffee.jpg create mode 100644 ObjectListView/Resources/filter-icons3.png create mode 100644 ObjectListView/Resources/filter.png create mode 100644 ObjectListView/Resources/sort-ascending.png create mode 100644 ObjectListView/Resources/sort-descending.png create mode 100644 ObjectListView/SubControls/GlassPanelForm.cs create mode 100644 ObjectListView/SubControls/HeaderControl.cs create mode 100644 ObjectListView/SubControls/ToolStripCheckedListBox.cs create mode 100644 ObjectListView/SubControls/ToolTipControl.cs create mode 100644 ObjectListView/TreeListView.cs create mode 100644 ObjectListView/Utilities/ColumnSelectionForm.Designer.cs create mode 100644 ObjectListView/Utilities/ColumnSelectionForm.cs create mode 100644 ObjectListView/Utilities/ColumnSelectionForm.resx create mode 100644 ObjectListView/Utilities/Generator.cs create mode 100644 ObjectListView/Utilities/OLVExporter.cs create mode 100644 ObjectListView/Utilities/TypedObjectListView.cs create mode 100644 ObjectListView/VirtualObjectListView.cs create mode 100644 ObjectListView/olv-keyfile.snk create mode 100644 Sanford.Multimedia.Midi.Core/Icon_-_General_MIDI.png create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.1.2.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.2.0.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.2.1.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.3.0.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.0.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.1.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.2.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.3.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.5.0.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.6.0.nuspec create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Deque.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Enumerator.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Node.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Synchronized.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/ICommand.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoManager.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Commands.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Test.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/AvlEnumerator.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/AvlNode.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/IAvlNode.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/NullAvlNode.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/Array.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/ArrayList.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalEnumerator.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalTopNode.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalTreeNode.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RandomAccessList.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/SortedList.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/Stack.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/PriorityQueue.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Collections/SkipList.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi.Core.csproj create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/IClock.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/MidiInternalClock.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/PpqnClock.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Construction.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Events.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Fields.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Messaging.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Properties.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.PublicMethods.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Win32.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/MidiInCaps.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDevice.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDeviceException.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiHeader.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiHeaderBuilder.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/MidiOutCaps.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/NoOpEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDevice.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDeviceBase.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputStream.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/GeneralMidi.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ChannelMessage.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ChannelMessageEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidShortMessageEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidSysExMessageEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MetaMessageEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MidiEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ShortMessageEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysCommonMessageEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysExMessageEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysRealtimeMessageEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/IMidiMessage.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/ChannelMessageBuilder.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/IMessageBuilder.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/KeySignatureBuilder.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/MetaTextBuilder.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/SongPositionPointerBuilder.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/SysCommonMessageBuilder.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TempoChangeBuilder.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TimeSignatureBuilder.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessageDispatcher.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessagesHierarchy.cd create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MetaMessage.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/InputDeviceMidiEvents.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MergeMidiEvents.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MidiEvents.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/OutputDeviceEventSink.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ShortMessage.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysCommonMessage.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysExMessage.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysRealtimeMessage.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/MidiNoteConverter.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelChaser.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelStopper.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChasedEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/StoppedEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiEvent.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiFileProperties.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/RecordingSession.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequence.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequencer.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Iterators.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Test.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/TrackReader.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/TrackWriter.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ITimer.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ThreadTimer.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ThreadTimerQueue.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Time.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Timer.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/TimerFactory.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Device.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia/DeviceException.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia/ErrorEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Key.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Note.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Threading/AsyncResult.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.AsyncResult.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/PostCompletedEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/DelegateScheduler.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/Task.cs create mode 100644 Sanford.Multimedia.Midi.Core/Sanford.Threading/InvokeCompletedEventArgs.cs create mode 100644 Sanford.Multimedia.Midi.Core/docs/Sanford.Multimedia.Midi.XML create mode 100644 SoundFont2/SF2.cs create mode 100644 SoundFont2/SF2Chunks.cs create mode 100644 SoundFont2/SF2Types.cs create mode 100644 SoundFont2/SoundFont2.csproj create mode 100644 VG Music Studio.backup/AlphaDream.yaml create mode 100644 VG Music Studio.backup/Config.yaml create mode 100644 VG Music Studio.backup/Core/ADPCMDecoder.cs create mode 100644 VG Music Studio.backup/Core/Assembler.cs create mode 100644 VG Music Studio.backup/Core/Config.cs create mode 100644 VG Music Studio.backup/Core/Engine.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Channel.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Commands.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Config.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Enums.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Mixer.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Player.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Structs.cs create mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Track.cs create mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Channel.cs create mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Commands.cs create mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Config.cs create mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Enums.cs create mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs create mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Player.cs create mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Structs.cs create mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Track.cs create mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Utils.cs create mode 100644 VG Music Studio.backup/Core/GBA/Utils.cs create mode 100644 VG Music Studio.backup/Core/GlobalConfig.cs create mode 100644 VG Music Studio.backup/Core/Mixer.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/Channel.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/Commands.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/Config.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/Enums.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/Mixer.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/Player.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/SMD.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/SWD.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/Track.cs create mode 100644 VG Music Studio.backup/Core/NDS/DSE/Utils.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Channel.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Commands.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Config.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Enums.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/FileHeader.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Player.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Track.cs create mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Utils.cs create mode 100644 VG Music Studio.backup/Core/NDS/Utils.cs create mode 100644 VG Music Studio.backup/Core/Player.cs create mode 100644 VG Music Studio.backup/Core/SongEvent.cs create mode 100644 VG Music Studio.backup/Core/VGMSDebug.cs rename {VG Music Studio => VG Music Studio.backup}/Dependencies/DLS2.dll (100%) rename {VG Music Studio => VG Music Studio.backup}/Dependencies/Sanford.Multimedia.Midi.dll (100%) rename {VG Music Studio => VG Music Studio.backup}/Dependencies/SoundFont2.dll (100%) create mode 100644 VG Music Studio.backup/MP2K.yaml create mode 100644 VG Music Studio.backup/MPlayDef.s create mode 100644 VG Music Studio.backup/Program.cs rename {VG Music Studio => VG Music Studio.backup}/Properties/AssemblyInfo.cs (100%) create mode 100644 VG Music Studio.backup/Properties/Icon.ico create mode 100644 VG Music Studio.backup/Properties/Icon16.png create mode 100644 VG Music Studio.backup/Properties/Icon24.png create mode 100644 VG Music Studio.backup/Properties/Icon32.png create mode 100644 VG Music Studio.backup/Properties/Icon48.png create mode 100644 VG Music Studio.backup/Properties/Icon528.png create mode 100644 VG Music Studio.backup/Properties/Next.ico create mode 100644 VG Music Studio.backup/Properties/Next.png create mode 100644 VG Music Studio.backup/Properties/Pause.ico create mode 100644 VG Music Studio.backup/Properties/Pause.png create mode 100644 VG Music Studio.backup/Properties/Play.ico create mode 100644 VG Music Studio.backup/Properties/Play.png create mode 100644 VG Music Studio.backup/Properties/Playlist.png create mode 100644 VG Music Studio.backup/Properties/Previous.ico create mode 100644 VG Music Studio.backup/Properties/Previous.png create mode 100644 VG Music Studio.backup/Properties/Resources.Designer.cs create mode 100644 VG Music Studio.backup/Properties/Resources.resx create mode 100644 VG Music Studio.backup/Properties/Settings.Designer.cs create mode 100644 VG Music Studio.backup/Properties/Settings.settings create mode 100644 VG Music Studio.backup/Properties/Song.png create mode 100644 VG Music Studio.backup/Properties/Strings.Designer.cs create mode 100644 VG Music Studio.backup/Properties/Strings.es.resx create mode 100644 VG Music Studio.backup/Properties/Strings.it.resx create mode 100644 VG Music Studio.backup/Properties/Strings.resx create mode 100644 VG Music Studio.backup/UI/ColorSlider.cs create mode 100644 VG Music Studio.backup/UI/FlexibleMessageBox.cs create mode 100644 VG Music Studio.backup/UI/ImageComboBox.cs create mode 100644 VG Music Studio.backup/UI/MainForm.cs create mode 100644 VG Music Studio.backup/UI/PianoControl.cs create mode 100644 VG Music Studio.backup/UI/SongInfoControl.cs create mode 100644 VG Music Studio.backup/UI/Theme.cs create mode 100644 VG Music Studio.backup/UI/TrackViewer.cs create mode 100644 VG Music Studio.backup/UI/ValueTextBox.cs create mode 100644 VG Music Studio.backup/Util/BetterExceptions.cs create mode 100644 VG Music Studio.backup/Util/HSLColor.cs create mode 100644 VG Music Studio.backup/Util/SampleUtils.cs create mode 100644 VG Music Studio.backup/Util/TimeBarrier.cs create mode 100644 VG Music Studio.backup/Util/Utils.cs create mode 100644 VG Music Studio.backup/VG Music Studio.csproj create mode 100644 VG Music Studio.backup/app.config create mode 100644 VG Music Studio.backup/midi2agb.exe create mode 100644 VG Music Studio.backup/upgrade.backup delete mode 100644 VG Music Studio/packages.config diff --git a/DLS2/Chunks/Chunk.cs b/DLS2/Chunks/Chunk.cs new file mode 100644 index 00000000..6b4b775d --- /dev/null +++ b/DLS2/Chunks/Chunk.cs @@ -0,0 +1,98 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.DLS2 +{ + public abstract class DLSChunk + { + /// Length 4 + public string ChunkName { get; } + /// Size in bytes + protected internal uint Size { get; protected set; } + + protected DLSChunk(string name) + { + ChunkName = name; + } + protected DLSChunk(string name, EndianBinaryReader reader) + { + ChunkName = name; + Size = reader.ReadUInt32(); + } + + protected long GetEndOffset(EndianBinaryReader reader) + { + return reader.BaseStream.Position + Size; + } + protected void EatRemainingBytes(EndianBinaryReader reader, long endOffset) + { + if (reader.BaseStream.Position > endOffset) + { + throw new InvalidDataException(); + } + reader.BaseStream.Position = endOffset; + } + + internal abstract void UpdateSize(); + + internal virtual void Write(EndianBinaryWriter writer) + { + UpdateSize(); + writer.Write(ChunkName, 4); + writer.Write(Size); + } + + internal static List GetAllChunks(EndianBinaryReader reader, long endOffset) + { + var chunks = new List(); + while (reader.BaseStream.Position < endOffset) + { + chunks.Add(SwitchNextChunk(reader)); + } + if (reader.BaseStream.Position > endOffset) + { + throw new InvalidDataException(); + } + return chunks; + } + private static DLSChunk SwitchNextChunk(EndianBinaryReader reader) + { + string str = reader.ReadString(4, false); + switch (str) + { + case "art1": return new Level1ArticulatorChunk(reader); + case "art2": return new Level2ArticulatorChunk(reader); + case "colh": return new CollectionHeaderChunk(reader); + case "data": return new DataChunk(reader); + case "dlid": return new DLSIDChunk(reader); + case "fmt ": return new FormatChunk(reader); + case "insh": return new InstrumentHeaderChunk(reader); + case "LIST": return new ListChunk(reader); + case "ptbl": return new PoolTableChunk(reader); + case "rgnh": return new RegionHeaderChunk(reader); + case "wlnk": return new WaveLinkChunk(reader); + case "wsmp": return new WaveSampleChunk(reader); + // InfoSubChunks + case "IARL": + case "IART": + case "ICMS": + case "ICMD": + case "ICOP": + case "ICRD": + case "IENG": + case "IGNR": + case "IKEY": + case "IMED": + case "INAM": + case "IPRD": + case "ISBJ": + case "ISFT": + case "ISRC": + case "ISRF": + case "ITCH": return new InfoSubChunk(str, reader); + default: return new UnsupportedChunk(str, reader); + } + } + } +} diff --git a/DLS2/Chunks/CollectionHeaderChunk.cs b/DLS2/Chunks/CollectionHeaderChunk.cs new file mode 100644 index 00000000..0f5256e1 --- /dev/null +++ b/DLS2/Chunks/CollectionHeaderChunk.cs @@ -0,0 +1,29 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + // Collection Header Chunk - Page 40 of spec + public sealed class CollectionHeaderChunk : DLSChunk + { + public uint NumInstruments { get; internal set; } + + internal CollectionHeaderChunk() : base("colh") { } + public CollectionHeaderChunk(EndianBinaryReader reader) : base("colh", reader) + { + long endOffset = GetEndOffset(reader); + NumInstruments = reader.ReadUInt32(); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4; // NumInstruments + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(NumInstruments); + } + } +} diff --git a/DLS2/Chunks/ConditionalChunk.cs b/DLS2/Chunks/ConditionalChunk.cs new file mode 100644 index 00000000..c96c20ed --- /dev/null +++ b/DLS2/Chunks/ConditionalChunk.cs @@ -0,0 +1,36 @@ +namespace Kermalis.DLS2 +{ + // ALL TODO: + + /*public enum DLSConditional : ushort + { + And = 1, + Or = 2, + Xor = 3, + Add = 4, + Subtract = 5, + Multiply = 6, + Divide = 7, + LogicalAnd = 8, + LogicalOr = 9, + Lt = 10, + Le = 11, + Gt = 12, + Ge = 13, + Eq = 14, + Not = 15, + Const = 16, + Query = 17, + QuerySupported = 18 + } + + // Conditional Chunk - Page 42 of spec + internal sealed class ConditionalChunk : DLSChunk + { + public ConditionalChunk(EndianBinaryReader reader) : base("cdl ", reader) + { + DLSConditional cond = reader.ReadEnum(); + + } + }*/ +} diff --git a/DLS2/Chunks/DLSIDChunk.cs b/DLS2/Chunks/DLSIDChunk.cs new file mode 100644 index 00000000..b9b8aab5 --- /dev/null +++ b/DLS2/Chunks/DLSIDChunk.cs @@ -0,0 +1,45 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + // DLSID Chunk - Page 40 of spec + public sealed class DLSIDChunk : DLSChunk + { + private DLSID _dlsid; + public DLSID DLSID + { + get => _dlsid; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _dlsid = value; + } + } + + public DLSIDChunk(DLSID id) : base("dlid") + { + DLSID = id; + } + public DLSIDChunk(EndianBinaryReader reader) : base("dlid", reader) + { + long endOffset = GetEndOffset(reader); + DLSID = new DLSID(reader); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 16; // DLSID + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + DLSID.Write(writer); + } + } +} diff --git a/DLS2/Chunks/DataChunk.cs b/DLS2/Chunks/DataChunk.cs new file mode 100644 index 00000000..f924db3e --- /dev/null +++ b/DLS2/Chunks/DataChunk.cs @@ -0,0 +1,10 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class DataChunk : RawDataChunk + { + public DataChunk(byte[] data) : base("data", data) { } + internal DataChunk(EndianBinaryReader reader) : base("data", reader) { } + } +} diff --git a/DLS2/Chunks/FormatChunk.cs b/DLS2/Chunks/FormatChunk.cs new file mode 100644 index 00000000..7a7779e1 --- /dev/null +++ b/DLS2/Chunks/FormatChunk.cs @@ -0,0 +1,105 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.DLS2 +{ + public abstract class FormatInfo + { + public ushort BitsPerSample { get; set; } + + internal abstract void Write(EndianBinaryWriter writer); + } + public sealed class PCMInfo : FormatInfo + { + internal PCMInfo() { } + internal PCMInfo(EndianBinaryReader reader) + { + BitsPerSample = reader.ReadUInt16(); + } + + internal override void Write(EndianBinaryWriter writer) + { + writer.Write(BitsPerSample); + } + } + // Untested! + public sealed class ExtensibleInfo : FormatInfo + { + public ushort ExtraInfo { get; set; } + public uint ChannelMask { get; set; } + public DLSID SubFormat { get; set; } + + internal ExtensibleInfo() + { + SubFormat = new DLSID(); + } + internal ExtensibleInfo(EndianBinaryReader reader) + { + BitsPerSample = reader.ReadUInt16(); + ushort byteSize = reader.ReadUInt16(); + if (byteSize != 22) + { + throw new InvalidDataException(); + } + ExtraInfo = reader.ReadUInt16(); + ChannelMask = reader.ReadUInt32(); + SubFormat = new DLSID(reader); + } + + internal override void Write(EndianBinaryWriter writer) + { + writer.Write(BitsPerSample); + writer.Write(22u); + writer.Write(ExtraInfo); + writer.Write(ChannelMask); + SubFormat.Write(writer); + } + } + + // Format Chunk - Page 57 of spec + public sealed class FormatChunk : DLSChunk + { + public WaveInfo WaveInfo { get; } + public FormatInfo FormatInfo { get; } + + public FormatChunk(WaveFormat format) : base("fmt ") + { + WaveInfo = new WaveInfo() { FormatTag = format }; + if (format == WaveFormat.Extensible) + { + FormatInfo = new ExtensibleInfo(); + } + else + { + FormatInfo = new PCMInfo(); + } + } + internal FormatChunk(EndianBinaryReader reader) : base("fmt ", reader) + { + long endOffset = GetEndOffset(reader); + WaveInfo = new WaveInfo(reader); + if (WaveInfo.FormatTag == WaveFormat.Extensible) + { + FormatInfo = new ExtensibleInfo(reader); + } + else + { + FormatInfo = new PCMInfo(reader); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 14 // WaveFormat + + (WaveInfo.FormatTag == DLS2.WaveFormat.Extensible ? 26u : 2u); // FormatInfo + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + WaveInfo.Write(writer); + FormatInfo.Write(writer); + } + } +} diff --git a/DLS2/Chunks/InfoSubChunk.cs b/DLS2/Chunks/InfoSubChunk.cs new file mode 100644 index 00000000..71a629a5 --- /dev/null +++ b/DLS2/Chunks/InfoSubChunk.cs @@ -0,0 +1,53 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Linq; + +namespace Kermalis.DLS2 +{ + public sealed class InfoSubChunk : DLSChunk + { + private string _text; + public string Text + { + get => _text; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + if (value.Any(c => c > sbyte.MaxValue)) + { + throw new ArgumentException("Text must be ASCII"); + } + _text = value; + } + } + + public InfoSubChunk(string name, string text) : base(name) + { + Text = text; + } + internal InfoSubChunk(string name, EndianBinaryReader reader) : base(name, reader) + { + long endOffset = GetEndOffset(reader); + _text = reader.ReadStringNullTerminated(); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = (uint)_text.Length + 1; // +1 for \0 + if (Size % 2 != 0) // Align by 2 bytes + { + Size++; + } + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(_text, (int)Size); + } + } +} diff --git a/DLS2/Chunks/InstrumentHeaderChunk.cs b/DLS2/Chunks/InstrumentHeaderChunk.cs new file mode 100644 index 00000000..077070c7 --- /dev/null +++ b/DLS2/Chunks/InstrumentHeaderChunk.cs @@ -0,0 +1,46 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + // Instrument Header Chunk - Page 45 of spec + public sealed class InstrumentHeaderChunk : DLSChunk + { + public uint NumRegions { get; set; } + private MIDILocale _locale; + public MIDILocale Locale + { + get => _locale; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _locale = value; + } + } + + public InstrumentHeaderChunk() : base("insh") { } + internal InstrumentHeaderChunk(EndianBinaryReader reader) : base("insh", reader) + { + long endOffset = GetEndOffset(reader); + NumRegions = reader.ReadUInt32(); + _locale = new MIDILocale(reader); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // NumRegions + + 8; // Locale + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(NumRegions); + _locale.Write(writer); + } + } +} diff --git a/DLS2/Chunks/Level1ArticulatorChunk.cs b/DLS2/Chunks/Level1ArticulatorChunk.cs new file mode 100644 index 00000000..c6b4f260 --- /dev/null +++ b/DLS2/Chunks/Level1ArticulatorChunk.cs @@ -0,0 +1,119 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.DLS2 +{ + // Level 1 Articulator Chunk - Page 46 of spec + public sealed class Level1ArticulatorChunk : DLSChunk, IList, IReadOnlyList + { + private readonly List _connectionBlocks; + + public Level1ArticulatorConnectionBlock this[int index] + { + get => _connectionBlocks[index]; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _connectionBlocks[index] = value; + } + } + public int Count => _connectionBlocks.Count; + public bool IsReadOnly => false; + + public Level1ArticulatorChunk() : base("art1") + { + _connectionBlocks = new List(); + } + internal Level1ArticulatorChunk(EndianBinaryReader reader) : base("art1", reader) + { + long endOffset = GetEndOffset(reader); + uint byteSize = reader.ReadUInt32(); + if (byteSize != 8) + { + throw new InvalidDataException(); + } + uint numConnectionBlocks = reader.ReadUInt32(); + _connectionBlocks = new List((int)numConnectionBlocks); + for (uint i = 0; i < numConnectionBlocks; i++) + { + _connectionBlocks.Add(new Level1ArticulatorConnectionBlock(reader)); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // byteSize + + 4 // _numConnectionBlocks + + (uint)(12 * _connectionBlocks.Count); // _connectionBlocks + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(8u); + writer.Write((uint)_connectionBlocks.Count); + for (int i = 0; i < _connectionBlocks.Count; i++) + { + _connectionBlocks[i].Write(writer); + } + } + + public IEnumerator GetEnumerator() + { + return _connectionBlocks.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _connectionBlocks.GetEnumerator(); + } + + public void Add(Level1ArticulatorConnectionBlock item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + _connectionBlocks.Add(item); + } + public void Clear() + { + _connectionBlocks.Clear(); + } + public void CopyTo(Level1ArticulatorConnectionBlock[] array, int arrayIndex) + { + _connectionBlocks.CopyTo(array, arrayIndex); + } + public bool Contains(Level1ArticulatorConnectionBlock item) + { + return _connectionBlocks.Contains(item); + } + public int IndexOf(Level1ArticulatorConnectionBlock item) + { + return _connectionBlocks.IndexOf(item); + } + public void Insert(int index, Level1ArticulatorConnectionBlock item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + _connectionBlocks.Insert(index, item); + } + public bool Remove(Level1ArticulatorConnectionBlock item) + { + return _connectionBlocks.Remove(item); + } + public void RemoveAt(int index) + { + _connectionBlocks.RemoveAt(index); + } + + } +} diff --git a/DLS2/Chunks/Level2ArticulatorChunk.cs b/DLS2/Chunks/Level2ArticulatorChunk.cs new file mode 100644 index 00000000..71e8b33d --- /dev/null +++ b/DLS2/Chunks/Level2ArticulatorChunk.cs @@ -0,0 +1,119 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.DLS2 +{ + // Level 2 Articulator Chunk - Page 49 of spec + public sealed class Level2ArticulatorChunk : DLSChunk, IList, IReadOnlyList + { + private readonly List _connectionBlocks; + + public Level2ArticulatorConnectionBlock this[int index] + { + get => _connectionBlocks[index]; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _connectionBlocks[index] = value; + } + } + public int Count => _connectionBlocks.Count; + public bool IsReadOnly => false; + + public Level2ArticulatorChunk() : base("art2") + { + _connectionBlocks = new List(); + } + internal Level2ArticulatorChunk(EndianBinaryReader reader) : base("art2", reader) + { + long endOffset = GetEndOffset(reader); + uint byteSize = reader.ReadUInt32(); + if (byteSize != 8) + { + throw new InvalidDataException(); + } + uint numConnectionBlocks = reader.ReadUInt32(); + _connectionBlocks = new List((int)numConnectionBlocks); + for (uint i = 0; i < numConnectionBlocks; i++) + { + _connectionBlocks.Add(new Level2ArticulatorConnectionBlock(reader)); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // byteSize + + 4 // _numConnectionBlocks + + (uint)(12 * _connectionBlocks.Count); // _connectionBlocks + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(8u); + writer.Write((uint)_connectionBlocks.Count); + for (int i = 0; i < _connectionBlocks.Count; i++) + { + _connectionBlocks[i].Write(writer); + } + } + + public IEnumerator GetEnumerator() + { + return _connectionBlocks.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _connectionBlocks.GetEnumerator(); + } + + public void Add(Level2ArticulatorConnectionBlock item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + _connectionBlocks.Add(item); + } + public void Clear() + { + _connectionBlocks.Clear(); + } + public void CopyTo(Level2ArticulatorConnectionBlock[] array, int arrayIndex) + { + _connectionBlocks.CopyTo(array, arrayIndex); + } + public bool Contains(Level2ArticulatorConnectionBlock item) + { + return _connectionBlocks.Contains(item); + } + public int IndexOf(Level2ArticulatorConnectionBlock item) + { + return _connectionBlocks.IndexOf(item); + } + public void Insert(int index, Level2ArticulatorConnectionBlock item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + _connectionBlocks.Insert(index, item); + } + public bool Remove(Level2ArticulatorConnectionBlock item) + { + return _connectionBlocks.Remove(item); + } + public void RemoveAt(int index) + { + _connectionBlocks.RemoveAt(index); + } + + } +} diff --git a/DLS2/Chunks/ListChunk.cs b/DLS2/Chunks/ListChunk.cs new file mode 100644 index 00000000..1c4107ee --- /dev/null +++ b/DLS2/Chunks/ListChunk.cs @@ -0,0 +1,112 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Kermalis.DLS2 +{ + // LIST Chunk - Page 40 of spec + public sealed class ListChunk : DLSChunk, IList, IReadOnlyList + { + /// Length 4 + public string Identifier { get; set; } + private readonly List _children; + + public int Count => _children.Count; + public bool IsReadOnly => false; + public DLSChunk this[int index] + { + get => _children[index]; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _children[index] = value; + } + } + + public ListChunk(string identifier) : base("LIST") + { + Identifier = identifier; + _children = new List(); + } + internal ListChunk(EndianBinaryReader reader) : base("LIST", reader) + { + long endOffset = GetEndOffset(reader); + Identifier = reader.ReadString(4, false); + _children = GetAllChunks(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4; // Identifier + foreach (DLSChunk c in _children) + { + c.UpdateSize(); + Size += c.Size + 8; + } + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(Identifier, 4); + foreach (DLSChunk c in _children) + { + c.Write(writer); + } + } + + public void Add(DLSChunk chunk) + { + if (chunk is null) + { + throw new ArgumentNullException(nameof(chunk)); + } + _children.Add(chunk); + } + public void Clear() + { + _children.Clear(); + } + public bool Contains(DLSChunk chunk) + { + return _children.Contains(chunk); + } + public void CopyTo(DLSChunk[] array, int arrayIndex) + { + _children.CopyTo(array, arrayIndex); + } + public int IndexOf(DLSChunk chunk) + { + return _children.IndexOf(chunk); + } + public void Insert(int index, DLSChunk chunk) + { + if (chunk is null) + { + throw new ArgumentNullException(nameof(chunk)); + } + _children.Insert(index, chunk); + } + public bool Remove(DLSChunk chunk) + { + return _children.Remove(chunk); + } + public void RemoveAt(int index) + { + _children.RemoveAt(index); + } + + public IEnumerator GetEnumerator() + { + return _children.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _children.GetEnumerator(); + } + } +} diff --git a/DLS2/Chunks/PoolTableChunk.cs b/DLS2/Chunks/PoolTableChunk.cs new file mode 100644 index 00000000..07d698cf --- /dev/null +++ b/DLS2/Chunks/PoolTableChunk.cs @@ -0,0 +1,71 @@ +using Kermalis.EndianBinaryIO; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.DLS2 +{ + // Pool Table Chunk - Page 54 of spec + public sealed class PoolTableChunk : DLSChunk, IReadOnlyList + { + private uint _numCues; + private List _poolCues; + + public uint this[int index] => _poolCues[index]; + public int Count => (int)_numCues; + + internal PoolTableChunk() : base("ptbl") + { + _poolCues = new List(); + } + internal PoolTableChunk(EndianBinaryReader reader) : base("ptbl", reader) + { + long endOffset = GetEndOffset(reader); + uint byteSize = reader.ReadUInt32(); + if (byteSize != 8) + { + throw new InvalidDataException(); + } + _numCues = reader.ReadUInt32(); + _poolCues = new List((int)_numCues); + for (uint i = 0; i < _numCues; i++) + { + _poolCues.Add(reader.ReadUInt32()); + } + EatRemainingBytes(reader, endOffset); + } + + internal void UpdateCues(List newCues) + { + _numCues = (uint)newCues.Count; + _poolCues = newCues; + } + + internal override void UpdateSize() + { + Size = 4 // byteSize + + 4 // _numCues + + (4 * _numCues); // _poolCues + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(8u); + writer.Write(_numCues); + for (int i = 0; i < _numCues; i++) + { + writer.Write(_poolCues[i]); + } + } + + public IEnumerator GetEnumerator() + { + return _poolCues.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _poolCues.GetEnumerator(); + } + } +} diff --git a/DLS2/Chunks/RawDataChunk.cs b/DLS2/Chunks/RawDataChunk.cs new file mode 100644 index 00000000..117d24fe --- /dev/null +++ b/DLS2/Chunks/RawDataChunk.cs @@ -0,0 +1,50 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + public abstract class RawDataChunk : DLSChunk + { + private byte[] _data; + public byte[] Data + { + get => _data; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _data = value; + } + } + + protected RawDataChunk(string name, byte[] data) : base(name) + { + Data = data; + } + protected RawDataChunk(string name, EndianBinaryReader reader) : base(name, reader) + { + _data = reader.ReadBytes((int)Size); + } + + internal override void UpdateSize() + { + Size = (uint)_data.Length; + if (Size % 2 != 0) // Align by 2 bytes + { + Size++; + } + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(_data); + for (int i = _data.Length; i < Size; i++) + { + writer.Write((byte)0); + } + } + } +} diff --git a/DLS2/Chunks/RegionHeaderChunk.cs b/DLS2/Chunks/RegionHeaderChunk.cs new file mode 100644 index 00000000..aaabd0b9 --- /dev/null +++ b/DLS2/Chunks/RegionHeaderChunk.cs @@ -0,0 +1,52 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + // Region Header Chunk - Page 45 of spec + public sealed class RegionHeaderChunk : DLSChunk + { + public Range KeyRange { get; set; } + public Range VelocityRange { get; set; } + public ushort Options { get; set; } + public ushort KeyGroup { get; set; } + public ushort Layer { get; set; } + + public RegionHeaderChunk() : base("rgnh") + { + KeyRange = new Range(0, 127); + VelocityRange = new Range(0, 127); + } + internal RegionHeaderChunk(EndianBinaryReader reader) : base("rgnh", reader) + { + long endOffset = GetEndOffset(reader); + KeyRange = new Range(reader); + VelocityRange = new Range(reader); + Options = reader.ReadUInt16(); + KeyGroup = reader.ReadUInt16(); + if (Size >= 14) // Size of 12 is also valid + { + Layer = reader.ReadUInt16(); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // KeyRange + + 4 // VelocityRange + + 2 // Options + + 2 // KeyGroup + + 2; // Layer + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + KeyRange.Write(writer); + VelocityRange.Write(writer); + writer.Write(Options); + writer.Write(KeyGroup); + writer.Write(Layer); + } + } +} diff --git a/DLS2/Chunks/UnsupportedChunk.cs b/DLS2/Chunks/UnsupportedChunk.cs new file mode 100644 index 00000000..acdccb1c --- /dev/null +++ b/DLS2/Chunks/UnsupportedChunk.cs @@ -0,0 +1,10 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class UnsupportedChunk : RawDataChunk + { + public UnsupportedChunk(string name, byte[] data) : base(name, data) { } + internal UnsupportedChunk(string name, EndianBinaryReader reader) : base(name, reader) { } + } +} diff --git a/DLS2/Chunks/VersionChunk.cs b/DLS2/Chunks/VersionChunk.cs new file mode 100644 index 00000000..42302bf5 --- /dev/null +++ b/DLS2/Chunks/VersionChunk.cs @@ -0,0 +1,14 @@ +namespace Kermalis.DLS2 +{ + // TODO: + + /*public sealed class VersionChunk : DLSChunk + { + + + internal VersionChunk(EndianBinaryReader reader) : base("vers", reader) + { + + } + }*/ +} diff --git a/DLS2/Chunks/WaveLinkChunk.cs b/DLS2/Chunks/WaveLinkChunk.cs new file mode 100644 index 00000000..37cb660f --- /dev/null +++ b/DLS2/Chunks/WaveLinkChunk.cs @@ -0,0 +1,43 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class WaveLinkChunk : DLSChunk + { + public WaveLinkOptions Options { get; set; } + public ushort PhaseGroup { get; set; } + public WaveLinkChannels Channels { get; set; } + public uint TableIndex { get; set; } + + public WaveLinkChunk() : base("wlnk") + { + Channels = WaveLinkChannels.Left; + } + internal WaveLinkChunk(EndianBinaryReader reader) : base("wlnk", reader) + { + long endOffset = GetEndOffset(reader); + Options = reader.ReadEnum(); + PhaseGroup = reader.ReadUInt16(); + Channels = reader.ReadEnum(); + TableIndex = reader.ReadUInt32(); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 2 // Options + + 2 // PhaseGroup + + 4 // Channel + + 4; // TableIndex + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(Options); + writer.Write(PhaseGroup); + writer.Write(Channels); + writer.Write(TableIndex); + } + } +} diff --git a/DLS2/Chunks/WaveSampleChunk.cs b/DLS2/Chunks/WaveSampleChunk.cs new file mode 100644 index 00000000..d005392a --- /dev/null +++ b/DLS2/Chunks/WaveSampleChunk.cs @@ -0,0 +1,69 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.DLS2 +{ + public sealed class WaveSampleChunk : DLSChunk + { + public ushort UnityNote { get; set; } + public short FineTune { get; set; } + public int Gain { get; set; } + public WaveSampleOptions Options { get; set; } + + public WaveSampleLoop Loop { get; set; } // Combining "SampleLoops" and the loop list + + public WaveSampleChunk() : base("wsmp") + { + UnityNote = 60; + Loop = null; + } + internal WaveSampleChunk(EndianBinaryReader reader) : base("wsmp", reader) + { + long endOffset = GetEndOffset(reader); + uint byteSize = reader.ReadUInt32(); + if (byteSize != 20) + { + throw new InvalidDataException(); + } + UnityNote = reader.ReadUInt16(); + FineTune = reader.ReadInt16(); + Gain = reader.ReadInt32(); + Options = reader.ReadEnum(); + if (reader.ReadUInt32() == 1) + { + Loop = new WaveSampleLoop(reader); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // byteSize + + 2 // UnityNote + + 2 // FineTune + + 4 // Gain + + 4 // Options + + 4 // DoesLoop + + (Loop is null ? 0u : 16u); // Loop + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(20u); + writer.Write(UnityNote); + writer.Write(FineTune); + writer.Write(Gain); + writer.Write(Options); + if (Loop is null) + { + writer.Write(0u); + } + else + { + writer.Write(1u); + Loop.Write(writer); + } + } + } +} diff --git a/DLS2/DLS.cs b/DLS2/DLS.cs new file mode 100644 index 00000000..d785aca0 --- /dev/null +++ b/DLS2/DLS.cs @@ -0,0 +1,235 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Kermalis.DLS2 +{ + public sealed class DLS : IList, IReadOnlyList + { + private readonly List _chunks; + + public int Count => _chunks.Count; + public bool IsReadOnly => false; + public DLSChunk this[int index] + { + get => _chunks[index]; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _chunks[index] = value; + } + } + + public CollectionHeaderChunk CollectionHeader => GetChunk(); + public ListChunk InstrumentList => GetListChunk("lins"); + public PoolTableChunk PoolTable => GetChunk(); + public ListChunk WavePool => GetListChunk("wvpl"); + + private T GetChunk() where T : DLSChunk + { + return (T)_chunks.Find(c => c is T); + } + private ListChunk GetListChunk(string str) + { + return (ListChunk)_chunks.Find(c => c is ListChunk lc && lc.Identifier == str); + } + +#if DEBUG + public static void Main() + { + //new DLS(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games\M\test.dls"); + //new DLS(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games\M\test2.dls"); + //new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\Arachno SoundFont - Version 1.0.dls"); + //new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\Musyng Kite.dls"); + new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\RSE Corrected Soundfont Revision 17.dls"); + } +#endif + + /// For creating. + public DLS() + { + _chunks = new List() + { + new CollectionHeaderChunk(), + new ListChunk("lins"), + new PoolTableChunk(), + new ListChunk("wvpl"), + }; + } + public DLS(string path) + { + var reader = new EndianBinaryReader(File.Open(path, FileMode.Open)); + { + _chunks = Init(reader); + } + } + public DLS(Stream stream) + { + _chunks = Init(new EndianBinaryReader(stream)); + } + private List Init(EndianBinaryReader reader) + { + string str = reader.ReadString(4, false); + if (str != "RIFF") + { + throw new InvalidDataException("RIFF header was not found at the start of the file."); + } + uint size = reader.ReadUInt32(); + long endOffset = reader.BaseStream.Position + size; + str = reader.ReadString(4, false); + if (str != "DLS ") + { + throw new InvalidDataException("DLS header was not found at the expected offset."); + } + return DLSChunk.GetAllChunks(reader, endOffset); + } + + public void UpdateCollectionHeader() + { + CollectionHeader.NumInstruments = (uint)InstrumentList.Count; + } + /// Updates the pointers in the . Should be called after modifying . + public void UpdatePoolTable() + { + ListChunk wvpl = WavePool; + var newCues = new List(wvpl.Count); + uint cur = 0; + for (int i = 0; i < wvpl.Count; i++) + { + newCues.Add(cur); + DLSChunk c = wvpl[i]; + c.UpdateSize(); + cur += c.Size + 8; + } + PoolTable.UpdateCues(newCues); + } + public void Save(string path) + { + UpdateCollectionHeader(); + UpdatePoolTable(); + + var writer = new EndianBinaryWriter(File.Open(path, FileMode.Create)); + { + writer.Write("RIFF", 4); + writer.Write(UpdateSize()); + writer.Write("DLS ", 4); + foreach (DLSChunk c in _chunks) + { + c.Write(writer); + } + } + } + + public string GetHierarchy() + { + var str = new StringBuilder(); + int tabLevel = 0; + void ApplyTabLevel() + { + for (int t = 0; t < tabLevel; t++) + { + str.Append('\t'); + } + } + void Recursion(IReadOnlyList parent, string listName) + { + ApplyTabLevel(); + str.Append($"{listName} ({parent.Count})"); + tabLevel++; + foreach (DLSChunk c in parent) + { + str.AppendLine(); + if (c is ListChunk lc) + { + Recursion(lc, $"{lc.ChunkName} '{lc.Identifier}'"); + } + else + { + ApplyTabLevel(); + str.Append($"<{c.ChunkName}>"); + if (c is InfoSubChunk ic) + { + str.Append($" [\"{ic.Text}\"]"); + } + else if (c is RawDataChunk dc) + { + str.Append($" [{dc.Data.Length} bytes]"); + } + } + } +#pragma warning disable IDE0059 // Unnecessary assignment of a value + tabLevel--; +#pragma warning restore IDE0059 // Unnecessary assignment of a value + } + Recursion(this, "RIFF 'DLS '"); + return str.ToString(); + } + + private uint UpdateSize() + { + uint size = 4; + foreach (DLSChunk c in _chunks) + { + c.UpdateSize(); + size += c.Size + 8; + } + return size; + } + + public void Add(DLSChunk chunk) + { + if (chunk is null) + { + throw new ArgumentNullException(nameof(chunk)); + } + _chunks.Add(chunk); + } + public void Clear() + { + _chunks.Clear(); + } + public bool Contains(DLSChunk chunk) + { + return _chunks.Contains(chunk); + } + public void CopyTo(DLSChunk[] array, int arrayIndex) + { + _chunks.CopyTo(array, arrayIndex); + } + public int IndexOf(DLSChunk chunk) + { + return _chunks.IndexOf(chunk); + } + public void Insert(int index, DLSChunk chunk) + { + if (chunk is null) + { + throw new ArgumentNullException(nameof(chunk)); + } + _chunks.Insert(index, chunk); + } + public bool Remove(DLSChunk chunk) + { + return _chunks.Remove(chunk); + } + public void RemoveAt(int index) + { + _chunks.RemoveAt(index); + } + + public IEnumerator GetEnumerator() + { + return _chunks.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _chunks.GetEnumerator(); + } + } +} diff --git a/DLS2/DLS2.csproj b/DLS2/DLS2.csproj new file mode 100644 index 00000000..d328f52e --- /dev/null +++ b/DLS2/DLS2.csproj @@ -0,0 +1,30 @@ + + + + Kermalis + + DLS2 + DLS2 + DLS2 + Kermalis.DLS2 + 1.0.0.0 + ..\Build + + + + net5.0 + Exe + + + + netstandard2.0 + Auto + none + false + + + + + + + diff --git a/DLS2/Enums/Level1ArticulatorEnums.cs b/DLS2/Enums/Level1ArticulatorEnums.cs new file mode 100644 index 00000000..0cbe8222 --- /dev/null +++ b/DLS2/Enums/Level1ArticulatorEnums.cs @@ -0,0 +1,44 @@ +namespace Kermalis.DLS2 +{ + public enum Level1ArticulatorSource : ushort + { + None = 0x0, + LFO = 0x1, + KeyOnVelocity = 0x2, + KeyNumber = 0x3, + EG1 = 0x4, + EG2 = 0x5, + PitchWheel = 0x6, + Modulation_CC1 = 0x81, + ChannelVolume_CC7 = 0x87, + Pan_CC10 = 0x8A, + Expression_CC11 = 0x8B, + PitchBendRange_RPN0 = 0x100, + FineTune_RPN1 = 0x101, + CoarseTune_RPN2 = 0x102 + } + + public enum Level1ArticulatorDestination : ushort + { + None = 0x0, + Gain = 0x1, + Pitch = 0x3, + Pan = 0x4, + LFOFrequency = 0x104, + LFOStartDelay = 0x105, + EG1AttackTime = 0x206, + EG1DecayTime = 0x207, + EG1ReleaseTime = 0x209, + EG1SustainLevel = 0x20A, + EG2AttackTime = 0x30A, + EG2DecayTime = 0x30B, + EG2ReleaseTime = 0x30D, + EG2SustainLevel = 0x30E + } + + public enum Level1ArticulatorTransform : byte + { + None = 0x0, + Concave = 0x1 + } +} diff --git a/DLS2/Enums/Level2ArticulatorEnums.cs b/DLS2/Enums/Level2ArticulatorEnums.cs new file mode 100644 index 00000000..4437952e --- /dev/null +++ b/DLS2/Enums/Level2ArticulatorEnums.cs @@ -0,0 +1,69 @@ +namespace Kermalis.DLS2 +{ + public enum Level2ArticulatorSource : ushort + { + None = 0x0, + LFO = 0x1, + KeyOnVelocity = 0x2, + KeyNumber = 0x3, + EG1 = 0x4, + EG2 = 0x5, + PitchWheel = 0x6, + PolyPressure = 0x7, + ChannelPressure = 0x8, + Vibrato = 0x9, + Modulation_CC1 = 0x81, + ChannelVolume_CC7 = 0x87, + Pan_CC10 = 0x8A, + Expression_CC11 = 0x8B, + ChorusSend_CC91 = 0xDB, + Reverb_SendCC93 = 0xDD, + PitchBendRange_RPN0 = 0x100, + FineTune_RPN1 = 0x101, + CoarseTune_RPN2 = 0x102 + } + + public enum Level2ArticulatorDestination : ushort + { + None = 0x0, + Gain = 0x1, + Pitch = 0x3, + Pan = 0x4, + KeyNumber = 0x5, + Left = 0x10, + Right = 0x11, + Center = 0x12, + LFEChannel = 0x13, + LeftRear = 0x14, + RightRear = 0x15, + Chorus = 0x80, + Reverb = 0x81, + LFOFrequency = 0x104, + LFOStartDelay = 0x105, + VIBFrequency = 0x114, + VIBStartDelay = 0x115, + EG1AttackTime = 0x206, + EG1DecayTime = 0x207, + EG1ReleaseTime = 0x209, + EG1SustainLevel = 0x20A, + EG1DelayTime = 0x20B, + EG1HoldTime = 0x20C, + EG1ShutdownTime = 0x20D, + EG2AttackTime = 0x30A, + EG2DecayTime = 0x30B, + EG2ReleaseTime = 0x30D, + EG2SustainLevel = 0x30E, + EG2DelayTime = 0x30F, + EG2HoldTime = 0x310, + FilterCutoff = 0x500, + FilterResonance = 0x501 + } + + public enum Level2ArticulatorTransform : byte + { + None = 0x0, + Concave = 0x1, + Convex = 0x2, + Switch = 0x3 + } +} diff --git a/DLS2/Enums/WaveFormat.cs b/DLS2/Enums/WaveFormat.cs new file mode 100644 index 00000000..4cab7976 --- /dev/null +++ b/DLS2/Enums/WaveFormat.cs @@ -0,0 +1,15 @@ +namespace Kermalis.DLS2 +{ + public enum WaveFormat : ushort + { + Unknown = 0, + PCM = 1, + MSADPCM = 2, + Float = 3, + ALaw = 6, + MuLaw = 7, + DVIADPCM = 17, + IMAADPCM = 17, + Extensible = 0xFFFE + } +} diff --git a/DLS2/Enums/WaveLinkChannels.cs b/DLS2/Enums/WaveLinkChannels.cs new file mode 100644 index 00000000..678bd25b --- /dev/null +++ b/DLS2/Enums/WaveLinkChannels.cs @@ -0,0 +1,28 @@ +using System; + +namespace Kermalis.DLS2 +{ + [Flags] + public enum WaveLinkChannels : uint + { + None = 0, + Left = 1 << 0, + Right = 1 << 1, + Center = 1 << 2, + LowFrequencyEnergy = 1 << 3, + SurroundLeft = 1 << 4, + SurroundRight = 1 << 5, + LeftOfCenter = 1 << 6, + RightOfCenter = 1 << 7, + SurroundCenter = 1 << 8, + SideLeft = 1 << 9, + SideRight = 1 << 10, + Top = 1 << 11, + TopFrontLeft = 1 << 12, + TopFrontCenter = 1 << 13, + TopFrontRight = 1 << 14, + TopRearLeft = 1 << 15, + TopRearCenter = 1 << 16, + TopRearRight = 1 << 17 + } +} diff --git a/DLS2/Enums/WaveLinkOptions.cs b/DLS2/Enums/WaveLinkOptions.cs new file mode 100644 index 00000000..7f47d8c1 --- /dev/null +++ b/DLS2/Enums/WaveLinkOptions.cs @@ -0,0 +1,12 @@ +using System; + +namespace Kermalis.DLS2 +{ + [Flags] + public enum WaveLinkOptions : ushort + { + None = 0, + PhaseMaster = 1 << 0, + MultiChannel = 1 << 1 + } +} diff --git a/DLS2/Enums/WaveSampleLoop.cs b/DLS2/Enums/WaveSampleLoop.cs new file mode 100644 index 00000000..68b63132 --- /dev/null +++ b/DLS2/Enums/WaveSampleLoop.cs @@ -0,0 +1,8 @@ +namespace Kermalis.DLS2 +{ + public enum LoopType : uint + { + Forward = 0, + Release = 1 + } +} diff --git a/DLS2/Enums/WaveSampleOptions.cs b/DLS2/Enums/WaveSampleOptions.cs new file mode 100644 index 00000000..ef87bab4 --- /dev/null +++ b/DLS2/Enums/WaveSampleOptions.cs @@ -0,0 +1,12 @@ +using System; + +namespace Kermalis.DLS2 +{ + [Flags] + public enum WaveSampleOptions : uint + { + None = 0, + NoTruncation = 1 << 0, + NoCompression = 1 << 1 + } +} diff --git a/DLS2/Structs/ConnectionBlock.cs b/DLS2/Structs/ConnectionBlock.cs new file mode 100644 index 00000000..d8a9f75d --- /dev/null +++ b/DLS2/Structs/ConnectionBlock.cs @@ -0,0 +1,163 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + public sealed class Level1ArticulatorConnectionBlock + { + public Level1ArticulatorSource Source { get; set; } + public Level1ArticulatorSource Control { get; set; } + public Level1ArticulatorDestination Destination { get; set; } + public Level1ArticulatorTransform Transform { get; set; } + public int Scale { get; set; } + + public Level1ArticulatorConnectionBlock() { } + internal Level1ArticulatorConnectionBlock(EndianBinaryReader reader) + { + Source = reader.ReadEnum(); + Control = reader.ReadEnum(); + Destination = reader.ReadEnum(); + Transform = reader.ReadEnum(); + Scale = reader.ReadInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Source); + writer.Write(Control); + writer.Write(Destination); + writer.Write(Transform); + writer.Write(Scale); + } + } + + public sealed class Level2ArticulatorConnectionBlock + { + public Level2ArticulatorSource Source { get; set; } + public Level2ArticulatorSource Control { get; set; } + public Level2ArticulatorDestination Destination { get; set; } + public ushort Transform_Raw { get; set; } + public int Scale { get; set; } + + public bool InvertSource + { + get => (Transform_Raw >> 15) != 0; + set + { + if (value) + { + Transform_Raw |= 1 << 15; + } + else + { + Transform_Raw &= unchecked((ushort)~(1 << 15)); + } + } + } + public bool BipolarSource + { + get => ((Transform_Raw >> 14) & 1) != 0; + set + { + if (value) + { + Transform_Raw |= 1 << 14; + } + else + { + Transform_Raw &= unchecked((ushort)~(1 << 14)); + } + } + } + public Level2ArticulatorTransform TransformSource + { + get => (Level2ArticulatorTransform)((Transform_Raw >> 10) & 0xF); + set + { + if (value > (Level2ArticulatorTransform)0xF) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Transform_Raw &= unchecked((ushort)~(0xF << 10)); + Transform_Raw |= (ushort)((ushort)value << 10); + } + } + + public bool InvertDestination + { + get => ((Transform_Raw >> 9) & 1) != 0; + set + { + if (value) + { + Transform_Raw |= 1 << 9; + } + else + { + Transform_Raw &= unchecked((ushort)~(1 << 9)); + } + } + } + public bool BipolarDestination + { + get => ((Transform_Raw >> 8) & 1) != 0; + set + { + if (value) + { + Transform_Raw |= 1 << 8; + } + else + { + Transform_Raw &= unchecked((ushort)~(1 << 8)); + } + } + } + public Level2ArticulatorTransform TransformDestination + { + get => (Level2ArticulatorTransform)((Transform_Raw >> 4) & 0xF); + set + { + if (value > (Level2ArticulatorTransform)0xF) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Transform_Raw &= unchecked((ushort)~(0xF << 4)); + Transform_Raw |= (ushort)((ushort)value << 4); + } + } + + public Level2ArticulatorTransform TransformOutput + { + get => (Level2ArticulatorTransform)(Transform_Raw & 0xF); + set + { + if (value > (Level2ArticulatorTransform)0xF) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Transform_Raw &= unchecked((ushort)~0xF); + Transform_Raw |= (ushort)value; + } + } + + public Level2ArticulatorConnectionBlock() { } + internal Level2ArticulatorConnectionBlock(EndianBinaryReader reader) + { + Source = reader.ReadEnum(); + Control = reader.ReadEnum(); + Destination = reader.ReadEnum(); + Transform_Raw = reader.ReadUInt16(); + Scale = reader.ReadInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Source); + writer.Write(Control); + writer.Write(Destination); + writer.Write(Transform_Raw); + writer.Write(Scale); + } + } +} diff --git a/DLS2/Structs/DLSID.cs b/DLS2/Structs/DLSID.cs new file mode 100644 index 00000000..d955ef70 --- /dev/null +++ b/DLS2/Structs/DLSID.cs @@ -0,0 +1,122 @@ +using Kermalis.EndianBinaryIO; +using System; +#if !DEBUG +using System.Collections.Generic; +#endif +using System.Linq; + +namespace Kermalis.DLS2 +{ + public sealed class DLSID + { + public uint Data1 { get; set; } + public ushort Data2 { get; set; } + public ushort Data3 { get; set; } + public byte[] Data4 { get; } + + public static DLSID Query_GMInHardware { get; } = new DLSID(0x178F2F24, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_GSInHardware { get; } = new DLSID(0x178F2F25, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_XGInHardware { get; } = new DLSID(0x178F2F26, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_SupportsDLS1 { get; } = new DLSID(0x178F2F27, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_SampleMemorySize { get; } = new DLSID(0x178F2F28, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_SamplePlaybackRate { get; } = new DLSID(0x2A91F713, 0xA4BF, 0x11D2, new byte[] { 0xBB, 0xDF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 }); + public static DLSID Query_ManufacturersID { get; } = new DLSID(0xB03E1181, 0x8095, 0x11D2, new byte[] { 0xA1, 0xEF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 }); + public static DLSID Query_ProductID { get; } = new DLSID(0xB03E1182, 0x8095, 0x11D2, new byte[] { 0xA1, 0xEF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 }); + public static DLSID Query_SupportsDLS2 { get; } = new DLSID(0xF14599E5, 0x4689, 0x11D2, new byte[] { 0xAF, 0xA6, 0x00, 0xAA, 0x00, 0x24, 0xD8, 0xB6 }); + + public DLSID() + { + Data4 = new byte[8]; + } + internal DLSID(EndianBinaryReader reader) + { + Data1 = reader.ReadUInt32(); + Data2 = reader.ReadUInt16(); + Data3 = reader.ReadUInt16(); + Data4 = reader.ReadBytes(8); + } + public DLSID(uint data1, ushort data2, ushort data3, byte[] data4) + { + if (data4 is null) + { + throw new ArgumentNullException(nameof(data4)); + } + if (data4.Length != 8) + { + throw new ArgumentOutOfRangeException(nameof(data4.Length)); + } + Data1 = data1; + Data2 = data2; + Data3 = data3; + Data4 = data4; + } + public DLSID(byte[] data) + { + if (data is null) + { + throw new ArgumentNullException(nameof(data)); + } + if (data.Length != 16) + { + throw new ArgumentOutOfRangeException(nameof(data.Length)); + } + Data1 = (uint)EndianBitConverter.BytesToInt32(data, 0, Endianness.LittleEndian); + Data2 = (ushort)EndianBitConverter.BytesToInt16(data, 4, Endianness.LittleEndian); + Data3 = (ushort)EndianBitConverter.BytesToInt16(data, 6, Endianness.LittleEndian); + Data4 = new byte[8]; + for (int i = 0; i < 8; i++) + { + Data4[i] = data[8 + i]; + } + } + + public void Write(EndianBinaryWriter writer) + { + writer.Write(Data1); + writer.Write(Data2); + writer.Write(Data3); + writer.Write(Data4); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(obj, this)) + { + return true; + } + if (obj is DLSID id) + { + return id.Data1 == Data1 && id.Data2 == Data2 && id.Data3 == Data3 && id.Data4.SequenceEqual(Data4); + } + return false; + } + public override int GetHashCode() + { + // .NET Standard does not have this method +#if DEBUG + return HashCode.Combine(Data1, Data2, Data3, Data4); +#else + int hashCode = -0x8CAC62A; + hashCode = hashCode * -0x5AAAAAD7 + Data1.GetHashCode(); + hashCode = hashCode * -0x5AAAAAD7 + Data2.GetHashCode(); + hashCode = hashCode * -0x5AAAAAD7 + Data3.GetHashCode(); + hashCode = hashCode * -0x5AAAAAD7 + EqualityComparer.Default.GetHashCode(Data4); + return hashCode; +#endif + } + public override string ToString() + { + string str = Data1.ToString("X8") + '-' + Data2.ToString("X4") + '-' + Data3.ToString("X4") + '-'; + for (int i = 0; i < 2; i++) + { + str += Data4[i].ToString("X2"); + } + str += '-'; + for (int i = 2; i < 8; i++) + { + str += Data4[i].ToString("X2"); + } + return str; + } + } +} diff --git a/DLS2/Structs/MIDILocale.cs b/DLS2/Structs/MIDILocale.cs new file mode 100644 index 00000000..49cd325b --- /dev/null +++ b/DLS2/Structs/MIDILocale.cs @@ -0,0 +1,87 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + // MIDILOCALE - Page 45 of spec + public sealed class MIDILocale + { + public uint Bank_Raw { get; set; } + public uint Instrument_Raw { get; set; } + + public byte CC32 + { + get => (byte)(Bank_Raw & 0x7F); + set + { + if (value > 0x7F) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Bank_Raw &= unchecked((uint)~0x7F); + Bank_Raw |= value; + } + } + public byte CC0 + { + get => (byte)((Bank_Raw >> 7) & 0x7F); + set + { + if (value > 0x7F) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Bank_Raw &= unchecked((uint)~(0x7F << 7)); + Bank_Raw |= (uint)(value << 7); + } + } + public bool IsDrum + { + get => (Bank_Raw >> 31) != 0; + set + { + if (value) + { + Bank_Raw |= 1u << 31; + } + else + { + Bank_Raw &= ~(1 << 31); + } + } + } + public byte Instrument + { + get => (byte)(Instrument_Raw & 0x7F); + set + { + if (value > 0x7F) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Instrument_Raw &= unchecked((uint)~0x7F); + Instrument_Raw |= value; + } + } + + public MIDILocale() { } + public MIDILocale(byte cc32, byte cc0, bool isDrum, byte instrument) + { + CC32 = cc32; + CC0 = cc0; + IsDrum = isDrum; + Instrument = instrument; + } + internal MIDILocale(EndianBinaryReader reader) + { + Bank_Raw = reader.ReadUInt32(); + Instrument_Raw = reader.ReadUInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Bank_Raw); + writer.Write(Instrument_Raw); + } + } +} diff --git a/DLS2/Structs/Range.cs b/DLS2/Structs/Range.cs new file mode 100644 index 00000000..7248bf8f --- /dev/null +++ b/DLS2/Structs/Range.cs @@ -0,0 +1,28 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class Range + { + public ushort Low { get; set; } + public ushort High { get; set; } + + public Range() { } + public Range(ushort low, ushort high) + { + Low = low; + High = high; + } + internal Range(EndianBinaryReader reader) + { + Low = reader.ReadUInt16(); + High = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Low); + writer.Write(High); + } + } +} diff --git a/DLS2/Structs/WaveInfo.cs b/DLS2/Structs/WaveInfo.cs new file mode 100644 index 00000000..2f1cb25e --- /dev/null +++ b/DLS2/Structs/WaveInfo.cs @@ -0,0 +1,32 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class WaveInfo + { + public WaveFormat FormatTag { get; set; } + public ushort Channels { get; set; } + public uint SamplesPerSec { get; set; } + public uint AvgBytesPerSec { get; set; } + public ushort BlockAlign { get; set; } + + internal WaveInfo() { } + internal WaveInfo(EndianBinaryReader reader) + { + FormatTag = reader.ReadEnum(); + Channels = reader.ReadUInt16(); + SamplesPerSec = reader.ReadUInt32(); + AvgBytesPerSec = reader.ReadUInt32(); + BlockAlign = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(FormatTag); + writer.Write(Channels); + writer.Write(SamplesPerSec); + writer.Write(AvgBytesPerSec); + writer.Write(BlockAlign); + } + } +} diff --git a/DLS2/Structs/WaveSampleLoop.cs b/DLS2/Structs/WaveSampleLoop.cs new file mode 100644 index 00000000..8e98a54d --- /dev/null +++ b/DLS2/Structs/WaveSampleLoop.cs @@ -0,0 +1,33 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.DLS2 +{ + public sealed class WaveSampleLoop + { + public LoopType LoopType { get; set; } + public uint LoopStart { get; set; } + public uint LoopLength { get; set; } + + public WaveSampleLoop() { } + internal WaveSampleLoop(EndianBinaryReader reader) + { + uint byteSize = reader.ReadUInt32(); + if (byteSize != 16) + { + throw new InvalidDataException(); + } + LoopType = reader.ReadEnum(); + LoopStart = reader.ReadUInt32(); + LoopLength = reader.ReadUInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(16u); + writer.Write(LoopType); + writer.Write(LoopStart); + writer.Write(LoopLength); + } + } +} diff --git a/EndianBinaryIO/Attributes.cs b/EndianBinaryIO/Attributes.cs new file mode 100644 index 00000000..8e3e98fe --- /dev/null +++ b/EndianBinaryIO/Attributes.cs @@ -0,0 +1,117 @@ +using System; +using System.Text; + +namespace Kermalis.EndianBinaryIO +{ + public interface IBinaryAttribute + { + T Value { get; } + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class BinaryIgnoreAttribute : Attribute, IBinaryAttribute + { + public bool Value { get; } + + public BinaryIgnoreAttribute(bool ignore = true) + { + Value = ignore; + } + } + [AttributeUsage(AttributeTargets.Property)] + public sealed class BinaryBooleanSizeAttribute : Attribute, IBinaryAttribute + { + public BooleanSize Value { get; } + + public BinaryBooleanSizeAttribute(BooleanSize booleanSize) + { + if (booleanSize >= BooleanSize.MAX) + { + throw new ArgumentOutOfRangeException($"{nameof(BinaryBooleanSizeAttribute)} cannot be created with a size of {booleanSize}."); + } + Value = booleanSize; + } + } + [AttributeUsage(AttributeTargets.Property)] + public sealed class BinaryEncodingAttribute : Attribute, IBinaryAttribute + { + public Encoding Value { get; } + + public BinaryEncodingAttribute(string encodingName) + { + Value = Encoding.GetEncoding(encodingName); + } + public BinaryEncodingAttribute(int encodingCodepage) + { + Value = Encoding.GetEncoding(encodingCodepage); + } + } + [AttributeUsage(AttributeTargets.Property)] + public sealed class BinaryStringNullTerminatedAttribute : Attribute, IBinaryAttribute + { + public bool Value { get; } + + public BinaryStringNullTerminatedAttribute(bool nullTerminated = true) + { + Value = nullTerminated; + } + } + [AttributeUsage(AttributeTargets.Property)] + public sealed class BinaryArrayFixedLengthAttribute : Attribute, IBinaryAttribute + { + public int Value { get; } + + public BinaryArrayFixedLengthAttribute(int length) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException($"{nameof(BinaryArrayFixedLengthAttribute)} cannot be created with a length of {length}. Length must be 0 or greater."); + } + Value = length; + } + } + [AttributeUsage(AttributeTargets.Property)] + public sealed class BinaryArrayVariableLengthAttribute : Attribute, IBinaryAttribute + { + public string Value { get; } + + public BinaryArrayVariableLengthAttribute(string anchor) + { + Value = anchor; + } + } + [AttributeUsage(AttributeTargets.Property)] + public sealed class BinaryStringFixedLengthAttribute : Attribute, IBinaryAttribute + { + public int Value { get; } + + public BinaryStringFixedLengthAttribute(int length) + { + if (length <= 0) + { + throw new ArgumentOutOfRangeException($"{nameof(BinaryStringFixedLengthAttribute)} cannot be created with a length of {length}. Length must be 0 or greater."); + } + Value = length; + } + } + [AttributeUsage(AttributeTargets.Property)] + public sealed class BinaryStringVariableLengthAttribute : Attribute, IBinaryAttribute + { + public string Value { get; } + + public BinaryStringVariableLengthAttribute(string anchor) + { + Value = anchor; + } + } + [AttributeUsage(AttributeTargets.Property)] + public sealed class BinaryStringTrimNullTerminatorsAttribute : Attribute, IBinaryAttribute + { + public bool Value { get; } + + public BinaryStringTrimNullTerminatorsAttribute(bool trim = true) + { + Value = trim; + } + } +} diff --git a/EndianBinaryIO/EndianBinaryIO.csproj b/EndianBinaryIO/EndianBinaryIO.csproj new file mode 100644 index 00000000..043c3bad --- /dev/null +++ b/EndianBinaryIO/EndianBinaryIO.csproj @@ -0,0 +1,39 @@ + + + + net5.0 + Library + Kermalis.EndianBinaryIO + Kermalis + Kermalis + EndianBinaryIO + EndianBinaryIO + EndianBinaryIO + 1.1.2.0 + https://github.com/Kermalis/EndianBinaryIO + git + + true + + A .NET 5 library that can read and write primitives, enums, arrays, and strings to streams and byte arrays using specified endianness, string encoding, and boolean sizes. +Objects can also be read from/written to streams via reflection and attributes. +Project URL ― https://github.com/Kermalis/EndianBinaryIO + https://github.com/Kermalis/EndianBinaryIO + en-001 + Serialization;Reflection;Endianness;LittleEndian;BigEndian;EndianBinaryIO + + + + Auto + none + false + + + + false + DEBUG;TRACE + full + true + + + diff --git a/EndianBinaryIO/EndianBinaryReader.cs b/EndianBinaryIO/EndianBinaryReader.cs new file mode 100644 index 00000000..2112f07d --- /dev/null +++ b/EndianBinaryIO/EndianBinaryReader.cs @@ -0,0 +1,925 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; + +namespace Kermalis.EndianBinaryIO +{ + public class EndianBinaryReader : IDisposable + { + public Stream BaseStream { get; } + private Endianness _endianness; + public Endianness Endianness + { + get => _endianness; + set + { + if (value >= Endianness.MAX) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _endianness = value; + } + } + private BooleanSize _booleanSize; + public BooleanSize BooleanSize + { + get => _booleanSize; + set + { + if (value >= BooleanSize.MAX) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _booleanSize = value; + } + } + public Encoding Encoding { get; set; } + + private byte[] _buffer; + private bool disposedValue; + + //public object Type; + + public EndianBinaryReader(Stream baseStream, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8) + { + if (baseStream is null) + { + throw new ArgumentNullException(nameof(baseStream)); + } + if (!baseStream.CanRead) + { + throw new ArgumentException(nameof(baseStream)); + } + BaseStream = baseStream; + Endianness = endianness; + BooleanSize = booleanSize; + Encoding = Encoding.Default; + } + public EndianBinaryReader(Stream baseStream, Encoding encoding, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8) + { + if (baseStream is null) + { + throw new ArgumentNullException(nameof(baseStream)); + } + if (!baseStream.CanRead) + { + throw new ArgumentException(nameof(baseStream)); + } + BaseStream = baseStream; + Endianness = endianness; + BooleanSize = booleanSize; + Encoding = encoding; + } + + private void ReadBytesIntoBuffer(int byteCount) + { + if (_buffer is null || _buffer.Length < byteCount) + { + _buffer = new byte[byteCount]; + } + if (BaseStream.Read(_buffer, 0, byteCount) != byteCount) + { + throw new EndOfStreamException(); + } + } + private char[] DecodeChars(Encoding encoding, int charCount) + { + Utils.ThrowIfCannotUseEncoding(encoding); + int maxBytes = encoding.GetMaxByteCount(charCount); + byte[] buffer = new byte[maxBytes]; + int amtRead = BaseStream.Read(buffer, 0, maxBytes); // Do not throw EndOfStreamException if there aren't enough bytes at the end of the stream + if (amtRead == 0) + { + throw new EndOfStreamException(); + } + // If the maxBytes would be 4, and the string only takes 2, we'd not have enough bytes, but if it's a proper string it doesn't matter + char[] chars = encoding.GetChars(buffer); + if (chars.Length < charCount) + { + throw new InvalidDataException(); // Too few chars means the decoding went wrong + } + // If we read too many chars, we need to shrink the array + // For example, if we want 1 char and the max bytes is 2, but we manage to read 2 1-byte chars, we'd want to shrink back to 1 char + Array.Resize(ref chars, charCount); + int actualBytes = encoding.GetByteCount(chars); + if (amtRead != actualBytes) + { + BaseStream.Position -= amtRead - actualBytes; // Set the stream back to compensate for the extra bytes we read + } + return chars; + } + + public byte PeekByte() + { + long pos = BaseStream.Position; + byte b = ReadByte(); + BaseStream.Position = pos; + return b; + } + public byte PeekByte(long offset) + { + BaseStream.Position = offset; + return PeekByte(); + } + public byte[] PeekBytes(int count) + { + long pos = BaseStream.Position; + byte[] b = ReadBytes(count); + BaseStream.Position = pos; + return b; + } + public byte[] PeekBytes(int count, long offset) + { + BaseStream.Position = offset; + return PeekBytes(count); + } + public char PeekChar() + { + long pos = BaseStream.Position; + char c = ReadChar(); + BaseStream.Position = pos; + return c; + } + public char PeekChar(long offset) + { + BaseStream.Position = offset; + return PeekChar(); + } + public char PeekChar(Encoding encoding) + { + long pos = BaseStream.Position; + char c = ReadChar(encoding); + BaseStream.Position = pos; + return c; + } + public char PeekChar(Encoding encoding, long offset) + { + BaseStream.Position = offset; + return PeekChar(encoding); + } + + public bool ReadBoolean() + { + return ReadBoolean(BooleanSize); + } + public bool ReadBoolean(long offset) + { + BaseStream.Position = offset; + return ReadBoolean(BooleanSize); + } + public bool ReadBoolean(BooleanSize booleanSize) + { + switch (booleanSize) + { + case BooleanSize.U8: + { + ReadBytesIntoBuffer(1); + return _buffer[0] != 0; + } + case BooleanSize.U16: + { + ReadBytesIntoBuffer(2); + return EndianBitConverter.BytesToInt16(_buffer, 0, Endianness) != 0; + } + case BooleanSize.U32: + { + ReadBytesIntoBuffer(4); + return EndianBitConverter.BytesToInt32(_buffer, 0, Endianness) != 0; + } + default: throw new ArgumentOutOfRangeException(nameof(booleanSize)); + } + } + public bool ReadBoolean(BooleanSize booleanSize, long offset) + { + BaseStream.Position = offset; + return ReadBoolean(booleanSize); + } + public bool[] ReadBooleans(int count) + { + return ReadBooleans(count, BooleanSize); + } + public bool[] ReadBooleans(int count, long offset) + { + BaseStream.Position = offset; + return ReadBooleans(count, BooleanSize); + } + public bool[] ReadBooleans(int count, BooleanSize size) + { + if (!Utils.ValidateReadArraySize(count, out bool[] array)) + { + array = new bool[count]; + for (int i = 0; i < count; i++) + { + array[i] = ReadBoolean(size); + } + } + return array; + } + public bool[] ReadBooleans(int count, BooleanSize size, long offset) + { + BaseStream.Position = offset; + return ReadBooleans(count, size); + } + public byte ReadByte() + { + ReadBytesIntoBuffer(1); + return _buffer[0]; + } + public byte ReadByte(long offset) + { + BaseStream.Position = offset; + return ReadByte(); + } + public byte[] ReadBytes(int count) + { + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + ReadBytesIntoBuffer(count); + array = new byte[count]; + for (int i = 0; i < count; i++) + { + array[i] = _buffer[i]; + } + } + return array; + } + public byte[] ReadBytes(int count, long offset) + { + BaseStream.Position = offset; + return ReadBytes(count); + } + public sbyte ReadSByte() + { + ReadBytesIntoBuffer(1); + return (sbyte)_buffer[0]; + } + public sbyte ReadSByte(long offset) + { + BaseStream.Position = offset; + return ReadSByte(); + } + public sbyte[] ReadSBytes(int count) + { + if (!Utils.ValidateReadArraySize(count, out sbyte[] array)) + { + ReadBytesIntoBuffer(count); + array = new sbyte[count]; + for (int i = 0; i < count; i++) + { + array[i] = (sbyte)_buffer[i]; + } + } + return array; + } + public sbyte[] ReadSBytes(int count, long offset) + { + BaseStream.Position = offset; + return ReadSBytes(count); + } + public char ReadChar() + { + return ReadChar(Encoding); + } + public char ReadChar(long offset) + { + BaseStream.Position = offset; + return ReadChar(); + } + public char ReadChar(Encoding encoding) + { + return DecodeChars(encoding, 1)[0]; + } + public char ReadChar(Encoding encoding, long offset) + { + BaseStream.Position = offset; + return ReadChar(encoding); + } + public char[] ReadChars(int count, bool trimNullTerminators) + { + return ReadChars(count, trimNullTerminators, Encoding); + } + public char[] ReadChars(int count, bool trimNullTerminators, long offset) + { + BaseStream.Position = offset; + return ReadChars(count, trimNullTerminators); + } + public char[] ReadChars(int count, bool trimNullTerminators, Encoding encoding) + { + if (Utils.ValidateReadArraySize(count, out char[] array)) + { + return array; + } + array = DecodeChars(encoding, count); + if (trimNullTerminators) + { + int i = Array.IndexOf(array, '\0'); + if (i != -1) + { + Array.Resize(ref array, i); + } + } + return array; + } + public char[] ReadChars(int count, bool trimNullTerminators, Encoding encoding, long offset) + { + BaseStream.Position = offset; + return ReadChars(count, trimNullTerminators, encoding); + } + public string ReadStringNullTerminated() + { + return ReadStringNullTerminated(Encoding); + } + public string ReadStringNullTerminated(long offset) + { + BaseStream.Position = offset; + return ReadStringNullTerminated(); + } + public string ReadStringNullTerminated(Encoding encoding) + { + string text = string.Empty; + while (true) + { + char c = ReadChar(encoding); + if (c == '\0') + { + break; + } + text += c; + } + return text; + } + public string ReadStringNullTerminated(Encoding encoding, long offset) + { + BaseStream.Position = offset; + return ReadStringNullTerminated(encoding); + } + public string ReadString(int charCount, bool trimNullTerminators) + { + return ReadString(charCount, trimNullTerminators, Encoding); + } + public string ReadString(int charCount, bool trimNullTerminators, long offset) + { + BaseStream.Position = offset; + return ReadString(charCount, trimNullTerminators); + } + public string ReadString(int charCount, bool trimNullTerminators, Encoding encoding) + { + return new string(ReadChars(charCount, trimNullTerminators, encoding)); + } + public string ReadString(int charCount, bool trimNullTerminators, Encoding encoding, long offset) + { + BaseStream.Position = offset; + return ReadString(charCount, trimNullTerminators, encoding); + } + public string[] ReadStringsNullTerminated(int count) + { + return ReadStringsNullTerminated(count, Encoding); + } + public string[] ReadStringsNullTerminated(int count, long offset) + { + BaseStream.Position = offset; + return ReadStringsNullTerminated(count); + } + public string[] ReadStringsNullTerminated(int count, Encoding encoding) + { + if (!Utils.ValidateReadArraySize(count, out string[] array)) + { + array = new string[count]; + for (int i = 0; i < count; i++) + { + array[i] = ReadStringNullTerminated(encoding); + } + } + return array; + } + public string[] ReadStringsNullTerminated(int count, Encoding encoding, long offset) + { + BaseStream.Position = offset; + return ReadStringsNullTerminated(count, encoding); + } + public string[] ReadStrings(int count, int charCount, bool trimNullTerminators) + { + return ReadStrings(count, charCount, trimNullTerminators, Encoding); + } + public string[] ReadStrings(int count, int charCount, bool trimNullTerminators, long offset) + { + BaseStream.Position = offset; + return ReadStrings(count, charCount, trimNullTerminators); + } + public string[] ReadStrings(int count, int charCount, bool trimNullTerminators, Encoding encoding) + { + if (!Utils.ValidateReadArraySize(count, out string[] array)) + { + array = new string[count]; + for (int i = 0; i < count; i++) + { + array[i] = ReadString(charCount, trimNullTerminators, encoding); + } + } + return array; + } + public string[] ReadStrings(int count, int charCount, bool trimNullTerminators, Encoding encoding, long offset) + { + BaseStream.Position = offset; + return ReadStrings(count, charCount, trimNullTerminators, encoding); + } + public short ReadInt16() + { + ReadBytesIntoBuffer(2); + return EndianBitConverter.BytesToInt16(_buffer, 0, Endianness); + } + public short ReadInt16(long offset) + { + BaseStream.Position = offset; + return ReadInt16(); + } + public short[] ReadInt16s(int count) + { + ReadBytesIntoBuffer(count * 2); + return EndianBitConverter.BytesToInt16s(_buffer, 0, count, Endianness); + } + public short[] ReadInt16s(int count, long offset) + { + BaseStream.Position = offset; + return ReadInt16s(count); + } + public ushort ReadUInt16() + { + ReadBytesIntoBuffer(2); + return (ushort)EndianBitConverter.BytesToInt16(_buffer, 0, Endianness); + } + public ushort ReadUInt16(long offset) + { + BaseStream.Position = offset; + return ReadUInt16(); + } + public ushort[] ReadUInt16s(int count) + { + ReadBytesIntoBuffer(count * 2); + return EndianBitConverter.BytesToUInt16s(_buffer, 0, count, Endianness); + } + public ushort[] ReadUInt16s(int count, long offset) + { + BaseStream.Position = offset; + return ReadUInt16s(count); + } + public int ReadInt32() + { + ReadBytesIntoBuffer(4); + return EndianBitConverter.BytesToInt32(_buffer, 0, Endianness); + } + public int ReadInt32(long offset) + { + BaseStream.Position = offset; + return ReadInt32(); + } + public int[] ReadInt32s(int count) + { + ReadBytesIntoBuffer(count * 4); + return EndianBitConverter.BytesToInt32s(_buffer, 0, count, Endianness); + } + public int[] ReadInt32s(int count, long offset) + { + BaseStream.Position = offset; + return ReadInt32s(count); + } + public uint ReadUInt32() + { + ReadBytesIntoBuffer(4); + return (uint)EndianBitConverter.BytesToInt32(_buffer, 0, Endianness); + } + public uint ReadUInt32(long offset) + { + BaseStream.Position = offset; + return ReadUInt32(); + } + public uint[] ReadUInt32s(int count) + { + ReadBytesIntoBuffer(count * 4); + return EndianBitConverter.BytesToUInt32s(_buffer, 0, count, Endianness); + } + public uint[] ReadUInt32s(int count, long offset) + { + BaseStream.Position = offset; + return ReadUInt32s(count); + } + public long ReadInt64() + { + ReadBytesIntoBuffer(8); + return EndianBitConverter.BytesToInt64(_buffer, 0, Endianness); + } + public long ReadInt64(long offset) + { + BaseStream.Position = offset; + return ReadInt64(); + } + public long[] ReadInt64s(int count) + { + ReadBytesIntoBuffer(count * 8); + return EndianBitConverter.BytesToInt64s(_buffer, 0, count, Endianness); + } + public long[] ReadInt64s(int count, long offset) + { + BaseStream.Position = offset; + return ReadInt64s(count); + } + public ulong ReadUInt64() + { + ReadBytesIntoBuffer(8); + return (ulong)EndianBitConverter.BytesToInt64(_buffer, 0, Endianness); + } + public ulong ReadUInt64(long offset) + { + BaseStream.Position = offset; + return ReadUInt64(); + } + public ulong[] ReadUInt64s(int count) + { + ReadBytesIntoBuffer(count * 8); + return EndianBitConverter.BytesToUInt64s(_buffer, 0, count, Endianness); + } + public ulong[] ReadUInt64s(int count, long offset) + { + BaseStream.Position = offset; + return ReadUInt64s(count); + } + public float ReadSingle() + { + ReadBytesIntoBuffer(4); + return EndianBitConverter.BytesToSingle(_buffer, 0, Endianness); + } + public float ReadSingle(long offset) + { + BaseStream.Position = offset; + return ReadSingle(); + } + public float[] ReadSingles(int count) + { + ReadBytesIntoBuffer(count * 4); + return EndianBitConverter.BytesToSingles(_buffer, 0, count, Endianness); + } + public float[] ReadSingles(int count, long offset) + { + BaseStream.Position = offset; + return ReadSingles(count); + } + public double ReadDouble() + { + ReadBytesIntoBuffer(8); + return EndianBitConverter.BytesToDouble(_buffer, 0, Endianness); + } + public double ReadDouble(long offset) + { + BaseStream.Position = offset; + return ReadDouble(); + } + public double[] ReadDoubles(int count) + { + ReadBytesIntoBuffer(count * 8); + return EndianBitConverter.BytesToDoubles(_buffer, 0, count, Endianness); + } + public double[] ReadDoubles(int count, long offset) + { + BaseStream.Position = offset; + return ReadDoubles(count); + } + public decimal ReadDecimal() + { + ReadBytesIntoBuffer(16); + return EndianBitConverter.BytesToDecimal(_buffer, 0, Endianness); + } + public decimal ReadDecimal(long offset) + { + BaseStream.Position = offset; + return ReadDecimal(); + } + public decimal[] ReadDecimals(int count) + { + ReadBytesIntoBuffer(count * 16); + return EndianBitConverter.BytesToDecimals(_buffer, 0, count, Endianness); + } + public decimal[] ReadDecimals(int count, long offset) + { + BaseStream.Position = offset; + return ReadDecimals(count); + } + + // Do not allow writing abstract "Enum" because there is no way to know which underlying type to read + // Yes "struct" restriction on reads + public TEnum ReadEnum() where TEnum : struct, Enum + { + Type enumType = typeof(TEnum); + Type underlyingType = Enum.GetUnderlyingType(enumType); + object value; + switch (Type.GetTypeCode(underlyingType)) + { + case TypeCode.Byte: value = ReadByte(); break; + case TypeCode.SByte: value = ReadSByte(); break; + case TypeCode.Int16: value = ReadInt16(); break; + case TypeCode.UInt16: value = ReadUInt16(); break; + case TypeCode.Int32: value = ReadInt32(); break; + case TypeCode.UInt32: value = ReadUInt32(); break; + case TypeCode.Int64: value = ReadInt64(); break; + case TypeCode.UInt64: value = ReadUInt64(); break; + default: throw new ArgumentOutOfRangeException(nameof(underlyingType)); + } + return (TEnum)Enum.ToObject(enumType, value); + } + public TEnum ReadEnum(long offset) where TEnum : struct, Enum + { + BaseStream.Position = offset; + return ReadEnum(); + } + public TEnum[] ReadEnums(int count) where TEnum : struct, Enum + { + if (!Utils.ValidateReadArraySize(count, out TEnum[] array)) + { + array = new TEnum[count]; + for (int i = 0; i < count; i++) + { + array[i] = ReadEnum(); + } + } + return array; + } + public TEnum[] ReadEnums(int count, long offset) where TEnum : struct, Enum + { + BaseStream.Position = offset; + return ReadEnums(count); + } + + public DateTime ReadDateTime() + { + return DateTime.FromBinary(ReadInt64()); + } + public DateTime ReadDateTime(long offset) + { + BaseStream.Position = offset; + return ReadDateTime(); + } + public DateTime[] ReadDateTimes(int count) + { + if (!Utils.ValidateReadArraySize(count, out DateTime[] array)) + { + array = new DateTime[count]; + for (int i = 0; i < count; i++) + { + array[i] = ReadDateTime(); + } + } + return array; + } + public DateTime[] ReadDateTimes(int count, long offset) + { + BaseStream.Position = offset; + return ReadDateTimes(count); + } + + public T ReadObject() where T : new() + { + return (T)ReadObject(typeof(T)); + } + public object ReadObject(Type objType) + { + Utils.ThrowIfCannotReadWriteType(objType); + object obj = Activator.CreateInstance(objType); + ReadIntoObject(obj); + return obj; + } + public T ReadObject(long offset) where T : new() + { + BaseStream.Position = offset; + return ReadObject(); + } + public object ReadObject(Type objType, long offset) + { + BaseStream.Position = offset; + return ReadObject(objType); + } + public void ReadIntoObject(IBinarySerializable obj) + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + obj.Read(this); + } + public void ReadIntoObject(IBinarySerializable obj, long offset) + { + BaseStream.Position = offset; + ReadIntoObject(obj); + } + public void ReadIntoObject(object obj) + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (obj is IBinarySerializable bs) + { + bs.Read(this); + return; + } + + Type objType = obj.GetType(); + Utils.ThrowIfCannotReadWriteType(objType); + + // Get public non-static properties + foreach (PropertyInfo propertyInfo in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (Utils.AttributeValueOrDefault(propertyInfo, false)) + { + continue; // Skip properties with BinaryIgnoreAttribute + } + + Type propertyType = propertyInfo.PropertyType; + object value; + + if (propertyType.IsArray) + { + int arrayLength = Utils.GetArrayLength(obj, objType, propertyInfo); + // Get array type + Type elementType = propertyType.GetElementType(); + if (arrayLength == 0) + { + value = Array.CreateInstance(elementType, 0); // Create 0 length array regardless of type + } + else + { + if (elementType.IsEnum) + { + elementType = Enum.GetUnderlyingType(elementType); + } + switch (Type.GetTypeCode(elementType)) + { + case TypeCode.Boolean: + { + BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, BooleanSize); + value = ReadBooleans(arrayLength, booleanSize); + break; + } + case TypeCode.Byte: value = ReadBytes(arrayLength); break; + case TypeCode.SByte: value = ReadSBytes(arrayLength); break; + case TypeCode.Char: + { + Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding); + bool trimNullTerminators = Utils.AttributeValueOrDefault(propertyInfo, false); + value = ReadChars(arrayLength, trimNullTerminators, encoding); + break; + } + case TypeCode.Int16: value = ReadInt16s(arrayLength); break; + case TypeCode.UInt16: value = ReadUInt16s(arrayLength); break; + case TypeCode.Int32: value = ReadInt32s(arrayLength); break; + case TypeCode.UInt32: value = ReadUInt32s(arrayLength); break; + case TypeCode.Int64: value = ReadInt64s(arrayLength); break; + case TypeCode.UInt64: value = ReadUInt64s(arrayLength); break; + case TypeCode.Single: value = ReadSingles(arrayLength); break; + case TypeCode.Double: value = ReadDoubles(arrayLength); break; + case TypeCode.Decimal: value = ReadDecimals(arrayLength); break; + case TypeCode.DateTime: value = ReadDateTimes(arrayLength); break; + case TypeCode.String: + { + Utils.GetStringLength(obj, objType, propertyInfo, true, out bool? nullTerminated, out int stringLength); + Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding); + if (nullTerminated == true) + { + value = ReadStringsNullTerminated(arrayLength, encoding); + } + else + { + bool trimNullTerminators = Utils.AttributeValueOrDefault(propertyInfo, false); + value = ReadStrings(arrayLength, stringLength, trimNullTerminators, encoding); + } + break; + } + case TypeCode.Object: + { + value = Array.CreateInstance(elementType, arrayLength); + if (typeof(IBinarySerializable).IsAssignableFrom(elementType)) + { + for (int i = 0; i < arrayLength; i++) + { + var serializable = (IBinarySerializable)Activator.CreateInstance(elementType); + serializable.Read(this); + ((Array)value).SetValue(serializable, i); + } + } + else // Element's type is not supported so try to read the array's objects + { + for (int i = 0; i < arrayLength; i++) + { + object elementObj = ReadObject(elementType); + ((Array)value).SetValue(elementObj, i); + } + } + break; + } + default: throw new ArgumentOutOfRangeException(nameof(elementType)); + } + } + } + else + { + if (propertyType.IsEnum) + { + propertyType = Enum.GetUnderlyingType(propertyType); + } + switch (Type.GetTypeCode(propertyType)) + { + case TypeCode.Boolean: + { + BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, BooleanSize); + value = ReadBoolean(booleanSize); + break; + } + case TypeCode.Byte: value = ReadByte(); break; + case TypeCode.SByte: value = ReadSByte(); break; + case TypeCode.Char: + { + Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding); + value = ReadChar(encoding); + break; + } + case TypeCode.Int16: value = ReadInt16(); break; + case TypeCode.UInt16: value = ReadUInt16(); break; + case TypeCode.Int32: value = ReadInt32(); break; + case TypeCode.UInt32: value = ReadUInt32(); break; + case TypeCode.Int64: value = ReadInt64(); break; + case TypeCode.UInt64: value = ReadUInt64(); break; + case TypeCode.Single: value = ReadSingle(); break; + case TypeCode.Double: value = ReadDouble(); break; + case TypeCode.Decimal: value = ReadDecimal(); break; + case TypeCode.DateTime: value = ReadDateTime(); break; + case TypeCode.String: + { + Utils.GetStringLength(obj, objType, propertyInfo, true, out bool? nullTerminated, out int stringLength); + Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding); + if (nullTerminated == true) + { + value = ReadStringNullTerminated(encoding); + } + else + { + bool trimNullTerminators = Utils.AttributeValueOrDefault(propertyInfo, false); + value = ReadString(stringLength, trimNullTerminators, encoding); + } + break; + } + case TypeCode.Object: + { + if (typeof(IBinarySerializable).IsAssignableFrom(propertyType)) + { + value = Activator.CreateInstance(propertyType); + ((IBinarySerializable)value).Read(this); + } + else // The property's type is not supported so try to read the object + { + value = ReadObject(propertyType); + } + break; + } + default: throw new ArgumentOutOfRangeException(nameof(propertyType)); + } + } + + // Set the value into the property + propertyInfo.SetValue(obj, value); + } + } + public void ReadIntoObject(object obj, long offset) + { + BaseStream.Position = offset; + ReadIntoObject(obj); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~EndianBinaryReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/EndianBinaryIO/EndianBinaryWriter.cs b/EndianBinaryIO/EndianBinaryWriter.cs new file mode 100644 index 00000000..7800f286 --- /dev/null +++ b/EndianBinaryIO/EndianBinaryWriter.cs @@ -0,0 +1,926 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; + +namespace Kermalis.EndianBinaryIO +{ + public class EndianBinaryWriter + { + public Stream BaseStream { get; } + private Endianness _endianness; + public Endianness Endianness + { + get => _endianness; + set + { + if (value >= Endianness.MAX) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _endianness = value; + } + } + private BooleanSize _booleanSize; + public BooleanSize BooleanSize + { + get => _booleanSize; + set + { + if (value >= BooleanSize.MAX) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _booleanSize = value; + } + } + public Encoding Encoding { get; set; } + + private byte[] _buffer; + + public EndianBinaryWriter(Stream baseStream, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8) + { + if (baseStream is null) + { + throw new ArgumentNullException(nameof(baseStream)); + } + if (!baseStream.CanWrite) + { + throw new ArgumentException(nameof(baseStream)); + } + BaseStream = baseStream; + Endianness = endianness; + BooleanSize = booleanSize; + Encoding = Encoding.Default; + } + public EndianBinaryWriter(Stream baseStream, Encoding encoding, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8) + { + if (baseStream is null) + { + throw new ArgumentNullException(nameof(baseStream)); + } + if (!baseStream.CanWrite) + { + throw new ArgumentException(nameof(baseStream)); + } + BaseStream = baseStream; + Endianness = endianness; + BooleanSize = booleanSize; + Encoding = encoding; + } + + private void SetBufferSize(int size) + { + if (_buffer is null || _buffer.Length < size) + { + _buffer = new byte[size]; + } + } + private void WriteBytesFromBuffer(int byteCount) + { + BaseStream.Write(_buffer, 0, byteCount); + } + + public void Write(bool value) + { + Write(value, BooleanSize); + } + public void Write(bool value, long offset) + { + BaseStream.Position = offset; + Write(value, BooleanSize); + } + public void Write(bool value, BooleanSize booleanSize) + { + switch (booleanSize) + { + case BooleanSize.U8: + { + SetBufferSize(1); + _buffer[0] = value ? (byte)1 : (byte)0; + WriteBytesFromBuffer(1); + break; + } + case BooleanSize.U16: + { + _buffer = EndianBitConverter.Int16ToBytes(value ? (short)1 : (short)0, Endianness); + WriteBytesFromBuffer(2); + break; + } + case BooleanSize.U32: + { + _buffer = EndianBitConverter.Int32ToBytes(value ? 1 : 0, Endianness); + WriteBytesFromBuffer(4); + break; + } + default: throw new ArgumentOutOfRangeException(nameof(booleanSize)); + } + } + public void Write(bool value, BooleanSize booleanSize, long offset) + { + BaseStream.Position = offset; + Write(value, booleanSize); + } + public void Write(bool[] value) + { + Write(value, 0, value.Length, BooleanSize); + } + public void Write(bool[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length, BooleanSize); + } + public void Write(bool[] value, BooleanSize booleanSize) + { + Write(value, 0, value.Length, booleanSize); + } + public void Write(bool[] value, BooleanSize booleanSize, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length, booleanSize); + } + public void Write(bool[] value, int startIndex, int count) + { + Write(value, startIndex, count, BooleanSize); + } + public void Write(bool[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count, BooleanSize); + } + public void Write(bool[] value, int startIndex, int count, BooleanSize booleanSize) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return; + } + for (int i = startIndex; i < count; i++) + { + Write(value[i], booleanSize); + } + } + public void Write(bool[] value, int startIndex, int count, BooleanSize booleanSize, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count, booleanSize); + } + public void Write(byte value) + { + SetBufferSize(1); + _buffer[0] = value; + WriteBytesFromBuffer(1); + } + public void Write(byte value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(byte[] value) + { + Write(value, 0, value.Length); + } + public void Write(byte[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(byte[] value, int startIndex, int count) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return; + } + SetBufferSize(count); + for (int i = 0; i < count; i++) + { + _buffer[i] = value[i + startIndex]; + } + WriteBytesFromBuffer(count); + } + public void Write(byte[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(sbyte value) + { + SetBufferSize(1); + _buffer[0] = (byte)value; + WriteBytesFromBuffer(1); + } + public void Write(sbyte value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(sbyte[] value) + { + Write(value, 0, value.Length); + } + public void Write(sbyte[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(sbyte[] value, int startIndex, int count) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return; + } + SetBufferSize(count); + for (int i = 0; i < count; i++) + { + _buffer[i] = (byte)value[i + startIndex]; + } + WriteBytesFromBuffer(count); + } + public void Write(sbyte[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(char value) + { + Write(value, Encoding); + } + public void Write(char value, long offset) + { + BaseStream.Position = offset; + Write(value, Encoding); + } + public void Write(char value, Encoding encoding) + { + Utils.ThrowIfCannotUseEncoding(encoding); + _buffer = encoding.GetBytes(new[] { value }); + WriteBytesFromBuffer(_buffer.Length); + } + public void Write(char value, Encoding encoding, long offset) + { + BaseStream.Position = offset; + Write(value, encoding); + } + public void Write(char[] value) + { + Write(value, 0, value.Length, Encoding); + } + public void Write(char[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length, Encoding); + } + public void Write(char[] value, Encoding encoding) + { + Write(value, 0, value.Length, encoding); + } + public void Write(char[] value, Encoding encoding, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length, encoding); + } + public void Write(char[] value, int startIndex, int count) + { + Write(value, startIndex, count, Encoding); + } + public void Write(char[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count, Encoding); + } + public void Write(char[] value, int startIndex, int count, Encoding encoding) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return; + } + Utils.ThrowIfCannotUseEncoding(encoding); + _buffer = encoding.GetBytes(value, startIndex, count); + WriteBytesFromBuffer(_buffer.Length); + } + public void Write(char[] value, int startIndex, int count, Encoding encoding, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count, encoding); + } + public void Write(string value, bool nullTerminated) + { + Write(value, nullTerminated, Encoding); + } + public void Write(string value, bool nullTerminated, long offset) + { + BaseStream.Position = offset; + Write(value, nullTerminated, Encoding); + } + public void Write(string value, bool nullTerminated, Encoding encoding) + { + Write(value.ToCharArray(), encoding); + if (nullTerminated) + { + Write('\0', encoding); + } + } + public void Write(string value, bool nullTerminated, Encoding encoding, long offset) + { + BaseStream.Position = offset; + Write(value, nullTerminated, encoding); + } + public void Write(string value, int charCount) + { + Write(value, charCount, Encoding); + } + public void Write(string value, int charCount, long offset) + { + BaseStream.Position = offset; + Write(value, charCount, Encoding); + } + public void Write(string value, int charCount, Encoding encoding) + { + Utils.TruncateString(value, charCount, out char[] chars); + Write(chars, encoding); + } + public void Write(string value, int charCount, Encoding encoding, long offset) + { + BaseStream.Position = offset; + Write(value, charCount, encoding); + } + public void Write(string[] value, int startIndex, int count, bool nullTerminated) + { + Write(value, startIndex, count, nullTerminated, Encoding); + } + public void Write(string[] value, int startIndex, int count, bool nullTerminated, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count, nullTerminated, Encoding); + } + public void Write(string[] value, int startIndex, int count, bool nullTerminated, Encoding encoding) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return; + } + for (int i = 0; i < count; i++) + { + Write(value[i + startIndex], nullTerminated, encoding); + } + } + public void Write(string[] value, int startIndex, int count, bool nullTerminated, Encoding encoding, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count, nullTerminated, encoding); + } + public void Write(string[] value, int startIndex, int count, int charCount) + { + Write(value, startIndex, count, charCount, Encoding); + } + public void Write(string[] value, int startIndex, int count, int charCount, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count, charCount, Encoding); + } + public void Write(string[] value, int startIndex, int count, int charCount, Encoding encoding) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return; + } + for (int i = 0; i < count; i++) + { + Write(value[i + startIndex], charCount, encoding); + } + } + public void Write(string[] value, int startIndex, int count, int charCount, Encoding encoding, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count, charCount, encoding); + } + public void Write(short value) + { + _buffer = EndianBitConverter.Int16ToBytes(value, Endianness); + WriteBytesFromBuffer(2); + } + public void Write(short value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(short[] value) + { + Write(value, 0, value.Length); + } + public void Write(short[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(short[] value, int startIndex, int count) + { + _buffer = EndianBitConverter.Int16sToBytes(value, startIndex, count, Endianness); + WriteBytesFromBuffer(count * 2); + } + public void Write(short[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(ushort value) + { + _buffer = EndianBitConverter.Int16ToBytes((short)value, Endianness); + WriteBytesFromBuffer(2); + } + public void Write(ushort value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(ushort[] value) + { + Write(value, 0, value.Length); + } + public void Write(ushort[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(ushort[] value, int startIndex, int count) + { + _buffer = EndianBitConverter.UInt16sToBytes(value, startIndex, count, Endianness); + WriteBytesFromBuffer(count * 2); + } + public void Write(ushort[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(int value) + { + _buffer = EndianBitConverter.Int32ToBytes(value, Endianness); + WriteBytesFromBuffer(4); + } + public void Write(int value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(int[] value) + { + Write(value, 0, value.Length); + } + public void Write(int[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(int[] value, int startIndex, int count) + { + _buffer = EndianBitConverter.Int32sToBytes(value, startIndex, count, Endianness); + WriteBytesFromBuffer(count * 4); + } + public void Write(int[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(uint value) + { + _buffer = EndianBitConverter.Int32ToBytes((int)value, Endianness); + WriteBytesFromBuffer(4); + } + public void Write(uint value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(uint[] value) + { + Write(value, 0, value.Length); + } + public void Write(uint[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(uint[] value, int startIndex, int count) + { + _buffer = EndianBitConverter.UInt32sToBytes(value, startIndex, count, Endianness); + WriteBytesFromBuffer(count * 4); + } + public void Write(uint[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(long value) + { + _buffer = EndianBitConverter.Int64ToBytes(value, Endianness); + WriteBytesFromBuffer(8); + } + public void Write(long value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(long[] value) + { + Write(value, 0, value.Length); + } + public void Write(long[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(long[] value, int startIndex, int count) + { + _buffer = EndianBitConverter.Int64sToBytes(value, startIndex, count, Endianness); + WriteBytesFromBuffer(count * 8); + } + public void Write(long[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(ulong value) + { + _buffer = EndianBitConverter.Int64ToBytes((long)value, Endianness); + WriteBytesFromBuffer(8); + } + public void Write(ulong value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(ulong[] value) + { + Write(value, 0, value.Length); + } + public void Write(ulong[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(ulong[] value, int startIndex, int count) + { + _buffer = EndianBitConverter.UInt64sToBytes(value, startIndex, count, Endianness); + WriteBytesFromBuffer(count * 8); + } + public void Write(ulong[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(float value) + { + _buffer = EndianBitConverter.SingleToBytes(value, Endianness); + WriteBytesFromBuffer(4); + } + public void Write(float value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(float[] value) + { + Write(value, 0, value.Length); + } + public void Write(float[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(float[] value, int startIndex, int count) + { + _buffer = EndianBitConverter.SinglesToBytes(value, startIndex, count, Endianness); + WriteBytesFromBuffer(count * 4); + } + public void Write(float[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(double value) + { + _buffer = EndianBitConverter.DoubleToBytes(value, Endianness); + WriteBytesFromBuffer(8); + } + public void Write(double value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(double[] value) + { + Write(value, 0, value.Length); + } + public void Write(double[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(double[] value, int startIndex, int count) + { + _buffer = EndianBitConverter.DoublesToBytes(value, startIndex, count, Endianness); + WriteBytesFromBuffer(count * 8); + } + public void Write(double[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + public void Write(decimal value) + { + _buffer = EndianBitConverter.DecimalToBytes(value, Endianness); + WriteBytesFromBuffer(16); + } + public void Write(decimal value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(decimal[] value) + { + Write(value, 0, value.Length); + } + public void Write(decimal[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(decimal[] value, int startIndex, int count) + { + _buffer = EndianBitConverter.DecimalsToBytes(value, startIndex, count, Endianness); + WriteBytesFromBuffer(count * 16); + } + public void Write(decimal[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + + // #13 - Handle "Enum" abstract type so we get the correct type in that case + // For example, writer.Write((Enum)Enum.Parse(enumType, value)) + // No "struct" restriction on writes + public void Write(TEnum value) where TEnum : Enum + { + Type underlyingType = Enum.GetUnderlyingType(value.GetType()); + switch (Type.GetTypeCode(underlyingType)) + { + case TypeCode.Byte: Write(Convert.ToByte(value)); break; + case TypeCode.SByte: Write(Convert.ToSByte(value)); break; + case TypeCode.Int16: Write(Convert.ToInt16(value)); break; + case TypeCode.UInt16: Write(Convert.ToUInt16(value)); break; + case TypeCode.Int32: Write(Convert.ToInt32(value)); break; + case TypeCode.UInt32: Write(Convert.ToUInt32(value)); break; + case TypeCode.Int64: Write(Convert.ToInt64(value)); break; + case TypeCode.UInt64: Write(Convert.ToUInt64(value)); break; + default: throw new ArgumentOutOfRangeException(nameof(underlyingType)); + } + } + public void Write(TEnum value, long offset) where TEnum : Enum + { + BaseStream.Position = offset; + Write(value); + } + public void Write(TEnum[] value) where TEnum : Enum + { + Write(value, 0, value.Length); + } + public void Write(TEnum[] value, long offset) where TEnum : Enum + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(TEnum[] value, int startIndex, int count) where TEnum : Enum + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return; + } + for (int i = 0; i < count; i++) + { + Write(value[i + startIndex]); + } + } + public void Write(TEnum[] value, int startIndex, int count, long offset) where TEnum : Enum + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + + public void Write(DateTime value) + { + Write(value.ToBinary()); + } + public void Write(DateTime value, long offset) + { + BaseStream.Position = offset; + Write(value); + } + public void Write(DateTime[] value) + { + Write(value, 0, value.Length); + } + public void Write(DateTime[] value, long offset) + { + BaseStream.Position = offset; + Write(value, 0, value.Length); + } + public void Write(DateTime[] value, int startIndex, int count) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return; + } + for (int i = 0; i < count; i++) + { + Write(value[i + startIndex]); + } + } + public void Write(DateTime[] value, int startIndex, int count, long offset) + { + BaseStream.Position = offset; + Write(value, startIndex, count); + } + + public void Write(IBinarySerializable obj) + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + obj.Write(this); + } + public void Write(IBinarySerializable obj, long offset) + { + BaseStream.Position = offset; + Write(obj); + } + public void Write(object obj) + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + if (obj is IBinarySerializable bs) + { + bs.Write(this); + return; + } + + Type objType = obj.GetType(); + Utils.ThrowIfCannotReadWriteType(objType); + + // Get public non-static properties + foreach (PropertyInfo propertyInfo in objType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (Utils.AttributeValueOrDefault(propertyInfo, false)) + { + continue; // Skip properties with BinaryIgnoreAttribute + } + + Type propertyType = propertyInfo.PropertyType; + object value = propertyInfo.GetValue(obj); + + if (propertyType.IsArray) + { + int arrayLength = Utils.GetArrayLength(obj, objType, propertyInfo); + if (arrayLength != 0) // Do not need to do anything for length 0 + { + // Get array type + Type elementType = propertyType.GetElementType(); + if (elementType.IsEnum) + { + elementType = Enum.GetUnderlyingType(elementType); + } + switch (Type.GetTypeCode(elementType)) + { + case TypeCode.Boolean: + { + BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, BooleanSize); + Write((bool[])value, 0, arrayLength, booleanSize); + break; + } + case TypeCode.Byte: Write((byte[])value, 0, arrayLength); break; + case TypeCode.SByte: Write((sbyte[])value, 0, arrayLength); break; + case TypeCode.Char: + { + Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding); + Write((char[])value, 0, arrayLength, encoding); + break; + } + case TypeCode.Int16: Write((short[])value, 0, arrayLength); break; + case TypeCode.UInt16: Write((ushort[])value, 0, arrayLength); break; + case TypeCode.Int32: Write((int[])value, 0, arrayLength); break; + case TypeCode.UInt32: Write((uint[])value, 0, arrayLength); break; + case TypeCode.Int64: Write((long[])value, 0, arrayLength); break; + case TypeCode.UInt64: Write((ulong[])value, 0, arrayLength); break; + case TypeCode.Single: Write((float[])value, 0, arrayLength); break; + case TypeCode.Double: Write((double[])value, 0, arrayLength); break; + case TypeCode.Decimal: Write((decimal[])value, 0, arrayLength); break; + case TypeCode.DateTime: Write((DateTime[])value, 0, arrayLength); break; + case TypeCode.String: + { + Utils.GetStringLength(obj, objType, propertyInfo, false, out bool? nullTerminated, out int stringLength); + Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding); + if (nullTerminated.HasValue) + { + Write((string[])value, 0, arrayLength, nullTerminated.Value, encoding); + } + else + { + Write((string[])value, 0, arrayLength, stringLength, encoding); + } + break; + } + case TypeCode.Object: + { + if (typeof(IBinarySerializable).IsAssignableFrom(elementType)) + { + for (int i = 0; i < arrayLength; i++) + { + var serializable = (IBinarySerializable)((Array)value).GetValue(i); + serializable.Write(this); + } + } + else // Element's type is not supported so try to write the array's objects + { + for (int i = 0; i < arrayLength; i++) + { + object elementObj = ((Array)value).GetValue(i); + Write(elementObj); + } + } + break; + } + default: throw new ArgumentOutOfRangeException(nameof(elementType)); + } + } + } + else + { + if (propertyType.IsEnum) + { + propertyType = Enum.GetUnderlyingType(propertyType); + } + switch (Type.GetTypeCode(propertyType)) + { + case TypeCode.Boolean: + { + BooleanSize booleanSize = Utils.AttributeValueOrDefault(propertyInfo, BooleanSize); + Write((bool)value, booleanSize); + break; + } + case TypeCode.Byte: Write((byte)value); break; + case TypeCode.SByte: Write((sbyte)value); break; + case TypeCode.Char: + { + Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding); + Write((char)value, encoding); + break; + } + case TypeCode.Int16: Write((short)value); break; + case TypeCode.UInt16: Write((ushort)value); break; + case TypeCode.Int32: Write((int)value); break; + case TypeCode.UInt32: Write((uint)value); break; + case TypeCode.Int64: Write((long)value); break; + case TypeCode.UInt64: Write((ulong)value); break; + case TypeCode.Single: Write((float)value); break; + case TypeCode.Double: Write((double)value); break; + case TypeCode.Decimal: Write((decimal)value); break; + case TypeCode.DateTime: Write((DateTime)value); break; + case TypeCode.String: + { + Utils.GetStringLength(obj, objType, propertyInfo, false, out bool? nullTerminated, out int stringLength); + Encoding encoding = Utils.AttributeValueOrDefault(propertyInfo, Encoding); + if (nullTerminated.HasValue) + { + Write((string)value, nullTerminated.Value, encoding); + } + else + { + Write((string)value, stringLength, encoding); + } + break; + } + case TypeCode.Object: + { + if (typeof(IBinarySerializable).IsAssignableFrom(propertyType)) + { + ((IBinarySerializable)value).Write(this); + } + else // property's type is not supported so try to write the object + { + Write(value); + } + break; + } + default: throw new ArgumentOutOfRangeException(nameof(propertyType)); + } + } + } + } + public void Write(object obj, long offset) + { + BaseStream.Position = offset; + Write(obj); + } + } +} diff --git a/EndianBinaryIO/EndianBitConverter.cs b/EndianBinaryIO/EndianBitConverter.cs new file mode 100644 index 00000000..cb007434 --- /dev/null +++ b/EndianBinaryIO/EndianBitConverter.cs @@ -0,0 +1,587 @@ +using System; + +namespace Kermalis.EndianBinaryIO +{ + public static class EndianBitConverter + { + public static Endianness SystemEndianness { get; } = BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian; + + public static unsafe byte[] Int16ToBytes(short value, Endianness targetEndianness) + { + byte[] bytes = new byte[2]; + fixed (byte* b = bytes) + { + *(short*)b = value; + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(bytes, 0, 1, 2); + } + return bytes; + } + public static unsafe byte[] Int16sToBytes(short[] value, int startIndex, int count, Endianness targetEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + array = new byte[2 * count]; + fixed (byte* b = array) + { + for (int i = 0; i < count; i++) + { + ((short*)b)[i] = value[startIndex + i]; + } + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(array, 0, count, 2); + } + } + return array; + } + public static unsafe byte[] UInt16sToBytes(ushort[] value, int startIndex, int count, Endianness targetEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + array = new byte[2 * count]; + fixed (byte* b = array) + { + for (int i = 0; i < count; i++) + { + ((ushort*)b)[i] = value[startIndex + i]; + } + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(array, 0, count, 2); + } + } + return array; + } + public static unsafe byte[] Int32ToBytes(int value, Endianness targetEndianness) + { + byte[] bytes = new byte[4]; + fixed (byte* b = bytes) + { + *(int*)b = value; + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(bytes, 0, 1, 4); + } + return bytes; + } + public static unsafe byte[] Int32sToBytes(int[] value, int startIndex, int count, Endianness targetEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + array = new byte[4 * count]; + fixed (byte* b = array) + { + for (int i = 0; i < count; i++) + { + ((int*)b)[i] = value[startIndex + i]; + } + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(array, 0, count, 4); + } + } + return array; + } + public static unsafe byte[] UInt32sToBytes(uint[] value, int startIndex, int count, Endianness targetEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + array = new byte[4 * count]; + fixed (byte* b = array) + { + for (int i = 0; i < count; i++) + { + ((uint*)b)[i] = value[startIndex + i]; + } + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(array, 0, count, 4); + } + } + return array; + } + public static unsafe byte[] Int64ToBytes(long value, Endianness targetEndianness) + { + byte[] bytes = new byte[8]; + fixed (byte* b = bytes) + { + *(long*)b = value; + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(bytes, 0, 1, 8); + } + return bytes; + } + public static unsafe byte[] Int64sToBytes(long[] value, int startIndex, int count, Endianness targetEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + array = new byte[8 * count]; + fixed (byte* b = array) + { + for (int i = 0; i < count; i++) + { + ((long*)b)[i] = value[startIndex + i]; + } + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(array, 0, count, 8); + } + } + return array; + } + public static unsafe byte[] UInt64sToBytes(ulong[] value, int startIndex, int count, Endianness targetEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + array = new byte[8 * count]; + fixed (byte* b = array) + { + for (int i = 0; i < count; i++) + { + ((ulong*)b)[i] = value[startIndex + i]; + } + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(array, 0, count, 8); + } + } + return array; + } + public static unsafe byte[] SingleToBytes(float value, Endianness targetEndianness) + { + byte[] bytes = new byte[4]; + fixed (byte* b = bytes) + { + *(float*)b = value; + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(bytes, 0, 1, 4); + } + return bytes; + } + public static unsafe byte[] SinglesToBytes(float[] value, int startIndex, int count, Endianness targetEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + array = new byte[4 * count]; + fixed (byte* b = array) + { + for (int i = 0; i < count; i++) + { + ((float*)b)[i] = value[startIndex + i]; + } + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(array, 0, count, 4); + } + } + return array; + } + public static unsafe byte[] DoubleToBytes(double value, Endianness targetEndianness) + { + byte[] bytes = new byte[8]; + fixed (byte* b = bytes) + { + *(double*)b = value; + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(bytes, 0, 1, 8); + } + return bytes; + } + public static unsafe byte[] DoublesToBytes(double[] value, int startIndex, int count, Endianness targetEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + array = new byte[8 * count]; + fixed (byte* b = array) + { + for (int i = 0; i < count; i++) + { + ((double*)b)[i] = value[startIndex + i]; + } + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(array, 0, count, 8); + } + } + return array; + } + public static unsafe byte[] DecimalToBytes(decimal value, Endianness targetEndianness) + { + byte[] bytes = new byte[16]; + fixed (byte* b = bytes) + { + *(decimal*)b = value; + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(bytes, 0, 1, 16); + } + return bytes; + } + public static unsafe byte[] DecimalsToBytes(decimal[] value, int startIndex, int count, Endianness targetEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out byte[] array)) + { + array = new byte[16 * count]; + fixed (byte* b = array) + { + for (int i = 0; i < count; i++) + { + ((decimal*)b)[i] = value[startIndex + i]; + } + } + if (SystemEndianness != targetEndianness) + { + FlipPrimitives(array, 0, count, 16); + } + } + return array; + } + + public static unsafe short BytesToInt16(byte[] value, int startIndex, Endianness sourceEndianness) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, 1, 2); + } + fixed (byte* b = &value[startIndex]) + { + return *(short*)b; + } + } + public static unsafe short[] BytesToInt16s(byte[] value, int startIndex, int count, Endianness sourceEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 2)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out short[] array)) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, count, 2); + } + array = new short[count]; + fixed (byte* b = &value[startIndex]) + { + for (int i = 0; i < count; i++) + { + array[i] = ((short*)b)[i]; + } + } + } + return array; + } + public static unsafe ushort[] BytesToUInt16s(byte[] value, int startIndex, int count, Endianness sourceEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 2)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out ushort[] array)) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, count, 2); + } + array = new ushort[count]; + fixed (byte* b = &value[startIndex]) + { + for (int i = 0; i < count; i++) + { + array[i] = ((ushort*)b)[i]; + } + } + } + return array; + } + public static unsafe int BytesToInt32(byte[] value, int startIndex, Endianness sourceEndianness) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, 1, 4); + } + fixed (byte* b = &value[startIndex]) + { + return *(int*)b; + } + } + public static unsafe int[] BytesToInt32s(byte[] value, int startIndex, int count, Endianness sourceEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 4)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out int[] array)) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, count, 4); + } + array = new int[count]; + fixed (byte* b = &value[startIndex]) + { + for (int i = 0; i < count; i++) + { + array[i] = ((int*)b)[i]; + } + } + } + return array; + } + public static unsafe uint[] BytesToUInt32s(byte[] value, int startIndex, int count, Endianness sourceEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 4)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out uint[] array)) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, count, 4); + } + array = new uint[count]; + fixed (byte* b = &value[startIndex]) + { + for (int i = 0; i < count; i++) + { + array[i] = ((uint*)b)[i]; + } + } + } + return array; + } + public static unsafe long BytesToInt64(byte[] value, int startIndex, Endianness sourceEndianness) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, 1, 8); + } + fixed (byte* b = &value[startIndex]) + { + return *(long*)b; + } + } + public static unsafe long[] BytesToInt64s(byte[] value, int startIndex, int count, Endianness sourceEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 8)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out long[] array)) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, count, 8); + } + array = new long[count]; + fixed (byte* b = &value[startIndex]) + { + for (int i = 0; i < count; i++) + { + array[i] = ((long*)b)[i]; + } + } + } + return array; + } + public static unsafe ulong[] BytesToUInt64s(byte[] value, int startIndex, int count, Endianness sourceEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 8)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out ulong[] array)) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, count, 8); + } + array = new ulong[count]; + fixed (byte* b = &value[startIndex]) + { + for (int i = 0; i < count; i++) + { + array[i] = ((ulong*)b)[i]; + } + } + } + return array; + } + public static unsafe float BytesToSingle(byte[] value, int startIndex, Endianness sourceEndianness) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, 1, 4); + } + fixed (byte* b = &value[startIndex]) + { + return *(float*)b; + } + } + public static unsafe float[] BytesToSingles(byte[] value, int startIndex, int count, Endianness sourceEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 4)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out float[] array)) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, count, 4); + } + array = new float[count]; + fixed (byte* b = &value[startIndex]) + { + for (int i = 0; i < count; i++) + { + array[i] = ((float*)b)[i]; + } + } + } + return array; + } + public static unsafe double BytesToDouble(byte[] value, int startIndex, Endianness sourceEndianness) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, 1, 8); + } + fixed (byte* b = &value[startIndex]) + { + return *(double*)b; + } + } + public static unsafe double[] BytesToDoubles(byte[] value, int startIndex, int count, Endianness sourceEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 8)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out double[] array)) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, count, 8); + } + array = new double[count]; + fixed (byte* b = &value[startIndex]) + { + for (int i = 0; i < count; i++) + { + array[i] = ((double*)b)[i]; + } + } + } + return array; + } + public static unsafe decimal BytesToDecimal(byte[] value, int startIndex, Endianness sourceEndianness) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, 1, 16); + } + fixed (byte* b = &value[startIndex]) + { + return *(decimal*)b; + } + } + public static unsafe decimal[] BytesToDecimals(byte[] value, int startIndex, int count, Endianness sourceEndianness) + { + if (Utils.ValidateArrayIndexAndCount(value, startIndex, count * 16)) + { + return Array.Empty(); + } + if (!Utils.ValidateReadArraySize(count, out decimal[] array)) + { + if (SystemEndianness != sourceEndianness) + { + FlipPrimitives(value, startIndex, count, 16); + } + array = new decimal[count]; + fixed (byte* b = &value[startIndex]) + { + for (int i = 0; i < count; i++) + { + array[i] = ((decimal*)b)[i]; + } + } + } + return array; + } + + private static void FlipPrimitives(byte[] buffer, int startIndex, int primitiveCount, int primitiveSize) + { + int byteCount = primitiveCount * primitiveSize; + for (int i = startIndex; i < byteCount + startIndex; i += primitiveSize) + { + int a = i; + int b = i + primitiveSize - 1; + while (a < b) + { + byte by = buffer[a]; + buffer[a] = buffer[b]; + buffer[b] = by; + a++; + b--; + } + } + } + } +} diff --git a/EndianBinaryIO/Enums.cs b/EndianBinaryIO/Enums.cs new file mode 100644 index 00000000..5a93b91e --- /dev/null +++ b/EndianBinaryIO/Enums.cs @@ -0,0 +1,18 @@ +namespace Kermalis.EndianBinaryIO +{ + public enum BooleanSize : byte + { + U8, + U16, + U32, + MAX + } + + public enum Endianness : byte + { + LittleEndian, + BigEndian, + MAX + } +} + diff --git a/EndianBinaryIO/IBinarySerializable.cs b/EndianBinaryIO/IBinarySerializable.cs new file mode 100644 index 00000000..69cbfcfb --- /dev/null +++ b/EndianBinaryIO/IBinarySerializable.cs @@ -0,0 +1,8 @@ +namespace Kermalis.EndianBinaryIO +{ + public interface IBinarySerializable + { + void Read(EndianBinaryReader r); + void Write(EndianBinaryWriter w); + } +} diff --git a/EndianBinaryIO/Utils.cs b/EndianBinaryIO/Utils.cs new file mode 100644 index 00000000..bdaa7aae --- /dev/null +++ b/EndianBinaryIO/Utils.cs @@ -0,0 +1,178 @@ +using System; +using System.Reflection; +using System.Text; + +namespace Kermalis.EndianBinaryIO +{ + internal sealed class Utils + { + public static void TruncateString(string str, int charCount, out char[] toArray) + { + toArray = new char[charCount]; + int numCharsToCopy = Math.Min(charCount, str.Length); + for (int i = 0; i < numCharsToCopy; i++) + { + toArray[i] = str[i]; + } + } + + public static bool TryGetAttribute(PropertyInfo propertyInfo, out TAttribute attribute) where TAttribute : Attribute + { + object[] attributes = propertyInfo.GetCustomAttributes(typeof(TAttribute), true); + if (attributes.Length == 1) + { + attribute = (TAttribute)attributes[0]; + return true; + } + attribute = null; + return false; + } + public static TValue GetAttributeValue(TAttribute attribute) where TAttribute : Attribute, IBinaryAttribute + { + return (TValue)typeof(TAttribute).GetProperty(nameof(IBinaryAttribute.Value)).GetValue(attribute); + } + public static TValue AttributeValueOrDefault(PropertyInfo propertyInfo, TValue defaultValue) where TAttribute : Attribute, IBinaryAttribute + { + if (TryGetAttribute(propertyInfo, out TAttribute attribute)) + { + return GetAttributeValue(attribute); + } + return defaultValue; + } + + public static void ThrowIfCannotReadWriteType(Type type) + { + if (type.IsArray || type.IsEnum || type.IsInterface || type.IsPointer || type.IsPrimitive) + { + throw new ArgumentException(nameof(type), $"Cannot read/write \"{type.FullName}\" objects."); + } + } + public static void ThrowIfCannotUseEncoding(Encoding encoding) + { + if (encoding is null) + { + throw new ArgumentNullException(nameof(encoding), "EndianBinaryIO could not read/write chars because an encoding was null; make sure you pass one in properly."); + } + } + + // Returns true if count is 0 + public static bool ValidateReadArraySize(int count, out T[] array) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException($"Invalid array length ({count})"); + } + if (count == 0) + { + array = Array.Empty(); + return true; + } + array = null; + return false; + } + // Returns true if count is 0 + public static bool ValidateArrayIndexAndCount(Array array, int startIndex, int count) + { + if (array is null) + { + throw new ArgumentNullException(nameof(array)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException($"Invalid array length ({count})"); + } + if (startIndex + count > array.Length) + { + throw new ArgumentOutOfRangeException($"Invalid array index + count (StartIndex: {startIndex}, Count: {count}, Array Length: {array.Length})"); + } + return count == 0; + } + + public static int GetArrayLength(object obj, Type objType, PropertyInfo propertyInfo) + { + int Validate(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException($"An array property in \"{objType.FullName}\" has an invalid length attribute ({value})"); + } + return value; + } + + if (TryGetAttribute(propertyInfo, out BinaryArrayFixedLengthAttribute fixedLenAttribute)) + { + if (propertyInfo.IsDefined(typeof(BinaryArrayVariableLengthAttribute))) + { + throw new ArgumentException($"An array property in \"{objType.FullName}\" has two array length attributes. Only one should be provided."); + } + return Validate(GetAttributeValue(fixedLenAttribute)); + } + if (TryGetAttribute(propertyInfo, out BinaryArrayVariableLengthAttribute varLenAttribute)) + { + string anchorName = GetAttributeValue(varLenAttribute); + PropertyInfo anchor = objType.GetProperty(anchorName, BindingFlags.Instance | BindingFlags.Public); + if (anchor is null) + { + throw new MissingMemberException($"An array property in \"{objType.FullName}\" has an invalid {nameof(BinaryArrayVariableLengthAttribute)} ({anchorName})."); + } + return Validate(Convert.ToInt32(anchor.GetValue(obj))); + } + throw new MissingMemberException($"An array property in \"{objType.FullName}\" is missing an array length attribute. One should be provided."); + } + public static void GetStringLength(object obj, Type objType, PropertyInfo propertyInfo, bool forReads, out bool? nullTerminated, out int stringLength) + { + int Validate(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException($"A string property in \"{objType.FullName}\" has an invalid length attribute ({value})"); + } + return value; + } + + if (TryGetAttribute(propertyInfo, out BinaryStringNullTerminatedAttribute nullTermAttribute)) + { + if (propertyInfo.IsDefined(typeof(BinaryStringFixedLengthAttribute)) || propertyInfo.IsDefined(typeof(BinaryStringVariableLengthAttribute))) + { + throw new ArgumentException($"A string property in \"{objType.FullName}\" has a string length attribute and a {nameof(BinaryStringNullTerminatedAttribute)}; cannot use both."); + } + if (propertyInfo.IsDefined(typeof(BinaryStringTrimNullTerminatorsAttribute))) + { + throw new ArgumentException($"A string property in \"{objType.FullName}\" has a {nameof(BinaryStringNullTerminatedAttribute)} and a {nameof(BinaryStringTrimNullTerminatorsAttribute)}; cannot use both."); + } + bool nt = GetAttributeValue(nullTermAttribute); + if (forReads && !nt) // Not forcing BinaryStringNullTerminatedAttribute to be treated as true since you may only write objects without reading them. + { + throw new ArgumentException($"A string property in \"{objType.FullName}\" has a {nameof(BinaryStringNullTerminatedAttribute)} that's set to false." + + $" Must use null termination or provide a string length when reading."); + } + nullTerminated = nt; + stringLength = -1; + return; + } + if (TryGetAttribute(propertyInfo, out BinaryStringFixedLengthAttribute fixedLenAttribute)) + { + if (propertyInfo.IsDefined(typeof(BinaryStringVariableLengthAttribute))) + { + throw new ArgumentException($"A string property in \"{objType.FullName}\" has two string length attributes. Only one should be provided."); + } + nullTerminated = null; + stringLength = Validate(GetAttributeValue(fixedLenAttribute)); + return; + } + if (TryGetAttribute(propertyInfo, out BinaryStringVariableLengthAttribute varLenAttribute)) + { + string anchorName = GetAttributeValue(varLenAttribute); + PropertyInfo anchor = objType.GetProperty(anchorName, BindingFlags.Instance | BindingFlags.Public); + if (anchor is null) + { + throw new MissingMemberException($"A string property in \"{objType.FullName}\" has an invalid {nameof(BinaryStringVariableLengthAttribute)} ({anchorName})."); + } + nullTerminated = null; + stringLength = Validate(Convert.ToInt32(anchor.GetValue(obj))); + return; + } + throw new MissingMemberException($"A string property in \"{objType.FullName}\" is missing a string length attribute and has no {nameof(BinaryStringNullTerminatedAttribute)}. One should be provided."); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/.gitattributes b/Legacy Repo (.NET Framework)/DLS2/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/Legacy Repo (.NET Framework)/DLS2/.gitignore b/Legacy Repo (.NET Framework)/DLS2/.gitignore new file mode 100644 index 00000000..80921d31 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/.gitignore @@ -0,0 +1,262 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +Build/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2.sln b/Legacy Repo (.NET Framework)/DLS2/DLS2.sln new file mode 100644 index 00000000..57d9d798 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DLS2", "DLS2\DLS2.csproj", "{AA962588-E769-4587-8211-AD1D901EAF2C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AA962588-E769-4587-8211-AD1D901EAF2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA962588-E769-4587-8211-AD1D901EAF2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA962588-E769-4587-8211-AD1D901EAF2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA962588-E769-4587-8211-AD1D901EAF2C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E5515F80-0D53-4618-9ED5-10A59FB4A7A0} + EndGlobalSection +EndGlobal diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Chunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Chunk.cs new file mode 100644 index 00000000..6b4b775d --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Chunk.cs @@ -0,0 +1,98 @@ +using Kermalis.EndianBinaryIO; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.DLS2 +{ + public abstract class DLSChunk + { + /// Length 4 + public string ChunkName { get; } + /// Size in bytes + protected internal uint Size { get; protected set; } + + protected DLSChunk(string name) + { + ChunkName = name; + } + protected DLSChunk(string name, EndianBinaryReader reader) + { + ChunkName = name; + Size = reader.ReadUInt32(); + } + + protected long GetEndOffset(EndianBinaryReader reader) + { + return reader.BaseStream.Position + Size; + } + protected void EatRemainingBytes(EndianBinaryReader reader, long endOffset) + { + if (reader.BaseStream.Position > endOffset) + { + throw new InvalidDataException(); + } + reader.BaseStream.Position = endOffset; + } + + internal abstract void UpdateSize(); + + internal virtual void Write(EndianBinaryWriter writer) + { + UpdateSize(); + writer.Write(ChunkName, 4); + writer.Write(Size); + } + + internal static List GetAllChunks(EndianBinaryReader reader, long endOffset) + { + var chunks = new List(); + while (reader.BaseStream.Position < endOffset) + { + chunks.Add(SwitchNextChunk(reader)); + } + if (reader.BaseStream.Position > endOffset) + { + throw new InvalidDataException(); + } + return chunks; + } + private static DLSChunk SwitchNextChunk(EndianBinaryReader reader) + { + string str = reader.ReadString(4, false); + switch (str) + { + case "art1": return new Level1ArticulatorChunk(reader); + case "art2": return new Level2ArticulatorChunk(reader); + case "colh": return new CollectionHeaderChunk(reader); + case "data": return new DataChunk(reader); + case "dlid": return new DLSIDChunk(reader); + case "fmt ": return new FormatChunk(reader); + case "insh": return new InstrumentHeaderChunk(reader); + case "LIST": return new ListChunk(reader); + case "ptbl": return new PoolTableChunk(reader); + case "rgnh": return new RegionHeaderChunk(reader); + case "wlnk": return new WaveLinkChunk(reader); + case "wsmp": return new WaveSampleChunk(reader); + // InfoSubChunks + case "IARL": + case "IART": + case "ICMS": + case "ICMD": + case "ICOP": + case "ICRD": + case "IENG": + case "IGNR": + case "IKEY": + case "IMED": + case "INAM": + case "IPRD": + case "ISBJ": + case "ISFT": + case "ISRC": + case "ISRF": + case "ITCH": return new InfoSubChunk(str, reader); + default: return new UnsupportedChunk(str, reader); + } + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/CollectionHeaderChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/CollectionHeaderChunk.cs new file mode 100644 index 00000000..0f5256e1 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/CollectionHeaderChunk.cs @@ -0,0 +1,29 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + // Collection Header Chunk - Page 40 of spec + public sealed class CollectionHeaderChunk : DLSChunk + { + public uint NumInstruments { get; internal set; } + + internal CollectionHeaderChunk() : base("colh") { } + public CollectionHeaderChunk(EndianBinaryReader reader) : base("colh", reader) + { + long endOffset = GetEndOffset(reader); + NumInstruments = reader.ReadUInt32(); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4; // NumInstruments + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(NumInstruments); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ConditionalChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ConditionalChunk.cs new file mode 100644 index 00000000..c96c20ed --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ConditionalChunk.cs @@ -0,0 +1,36 @@ +namespace Kermalis.DLS2 +{ + // ALL TODO: + + /*public enum DLSConditional : ushort + { + And = 1, + Or = 2, + Xor = 3, + Add = 4, + Subtract = 5, + Multiply = 6, + Divide = 7, + LogicalAnd = 8, + LogicalOr = 9, + Lt = 10, + Le = 11, + Gt = 12, + Ge = 13, + Eq = 14, + Not = 15, + Const = 16, + Query = 17, + QuerySupported = 18 + } + + // Conditional Chunk - Page 42 of spec + internal sealed class ConditionalChunk : DLSChunk + { + public ConditionalChunk(EndianBinaryReader reader) : base("cdl ", reader) + { + DLSConditional cond = reader.ReadEnum(); + + } + }*/ +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DLSIDChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DLSIDChunk.cs new file mode 100644 index 00000000..b9b8aab5 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DLSIDChunk.cs @@ -0,0 +1,45 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + // DLSID Chunk - Page 40 of spec + public sealed class DLSIDChunk : DLSChunk + { + private DLSID _dlsid; + public DLSID DLSID + { + get => _dlsid; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _dlsid = value; + } + } + + public DLSIDChunk(DLSID id) : base("dlid") + { + DLSID = id; + } + public DLSIDChunk(EndianBinaryReader reader) : base("dlid", reader) + { + long endOffset = GetEndOffset(reader); + DLSID = new DLSID(reader); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 16; // DLSID + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + DLSID.Write(writer); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DataChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DataChunk.cs new file mode 100644 index 00000000..f924db3e --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DataChunk.cs @@ -0,0 +1,10 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class DataChunk : RawDataChunk + { + public DataChunk(byte[] data) : base("data", data) { } + internal DataChunk(EndianBinaryReader reader) : base("data", reader) { } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/FormatChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/FormatChunk.cs new file mode 100644 index 00000000..7a7779e1 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/FormatChunk.cs @@ -0,0 +1,105 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.DLS2 +{ + public abstract class FormatInfo + { + public ushort BitsPerSample { get; set; } + + internal abstract void Write(EndianBinaryWriter writer); + } + public sealed class PCMInfo : FormatInfo + { + internal PCMInfo() { } + internal PCMInfo(EndianBinaryReader reader) + { + BitsPerSample = reader.ReadUInt16(); + } + + internal override void Write(EndianBinaryWriter writer) + { + writer.Write(BitsPerSample); + } + } + // Untested! + public sealed class ExtensibleInfo : FormatInfo + { + public ushort ExtraInfo { get; set; } + public uint ChannelMask { get; set; } + public DLSID SubFormat { get; set; } + + internal ExtensibleInfo() + { + SubFormat = new DLSID(); + } + internal ExtensibleInfo(EndianBinaryReader reader) + { + BitsPerSample = reader.ReadUInt16(); + ushort byteSize = reader.ReadUInt16(); + if (byteSize != 22) + { + throw new InvalidDataException(); + } + ExtraInfo = reader.ReadUInt16(); + ChannelMask = reader.ReadUInt32(); + SubFormat = new DLSID(reader); + } + + internal override void Write(EndianBinaryWriter writer) + { + writer.Write(BitsPerSample); + writer.Write(22u); + writer.Write(ExtraInfo); + writer.Write(ChannelMask); + SubFormat.Write(writer); + } + } + + // Format Chunk - Page 57 of spec + public sealed class FormatChunk : DLSChunk + { + public WaveInfo WaveInfo { get; } + public FormatInfo FormatInfo { get; } + + public FormatChunk(WaveFormat format) : base("fmt ") + { + WaveInfo = new WaveInfo() { FormatTag = format }; + if (format == WaveFormat.Extensible) + { + FormatInfo = new ExtensibleInfo(); + } + else + { + FormatInfo = new PCMInfo(); + } + } + internal FormatChunk(EndianBinaryReader reader) : base("fmt ", reader) + { + long endOffset = GetEndOffset(reader); + WaveInfo = new WaveInfo(reader); + if (WaveInfo.FormatTag == WaveFormat.Extensible) + { + FormatInfo = new ExtensibleInfo(reader); + } + else + { + FormatInfo = new PCMInfo(reader); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 14 // WaveFormat + + (WaveInfo.FormatTag == DLS2.WaveFormat.Extensible ? 26u : 2u); // FormatInfo + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + WaveInfo.Write(writer); + FormatInfo.Write(writer); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InfoSubChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InfoSubChunk.cs new file mode 100644 index 00000000..71a629a5 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InfoSubChunk.cs @@ -0,0 +1,53 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Linq; + +namespace Kermalis.DLS2 +{ + public sealed class InfoSubChunk : DLSChunk + { + private string _text; + public string Text + { + get => _text; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + if (value.Any(c => c > sbyte.MaxValue)) + { + throw new ArgumentException("Text must be ASCII"); + } + _text = value; + } + } + + public InfoSubChunk(string name, string text) : base(name) + { + Text = text; + } + internal InfoSubChunk(string name, EndianBinaryReader reader) : base(name, reader) + { + long endOffset = GetEndOffset(reader); + _text = reader.ReadStringNullTerminated(); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = (uint)_text.Length + 1; // +1 for \0 + if (Size % 2 != 0) // Align by 2 bytes + { + Size++; + } + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(_text, (int)Size); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs new file mode 100644 index 00000000..077070c7 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs @@ -0,0 +1,46 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + // Instrument Header Chunk - Page 45 of spec + public sealed class InstrumentHeaderChunk : DLSChunk + { + public uint NumRegions { get; set; } + private MIDILocale _locale; + public MIDILocale Locale + { + get => _locale; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _locale = value; + } + } + + public InstrumentHeaderChunk() : base("insh") { } + internal InstrumentHeaderChunk(EndianBinaryReader reader) : base("insh", reader) + { + long endOffset = GetEndOffset(reader); + NumRegions = reader.ReadUInt32(); + _locale = new MIDILocale(reader); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // NumRegions + + 8; // Locale + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(NumRegions); + _locale.Write(writer); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level1ArticulatorChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level1ArticulatorChunk.cs new file mode 100644 index 00000000..c6b4f260 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level1ArticulatorChunk.cs @@ -0,0 +1,119 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.DLS2 +{ + // Level 1 Articulator Chunk - Page 46 of spec + public sealed class Level1ArticulatorChunk : DLSChunk, IList, IReadOnlyList + { + private readonly List _connectionBlocks; + + public Level1ArticulatorConnectionBlock this[int index] + { + get => _connectionBlocks[index]; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _connectionBlocks[index] = value; + } + } + public int Count => _connectionBlocks.Count; + public bool IsReadOnly => false; + + public Level1ArticulatorChunk() : base("art1") + { + _connectionBlocks = new List(); + } + internal Level1ArticulatorChunk(EndianBinaryReader reader) : base("art1", reader) + { + long endOffset = GetEndOffset(reader); + uint byteSize = reader.ReadUInt32(); + if (byteSize != 8) + { + throw new InvalidDataException(); + } + uint numConnectionBlocks = reader.ReadUInt32(); + _connectionBlocks = new List((int)numConnectionBlocks); + for (uint i = 0; i < numConnectionBlocks; i++) + { + _connectionBlocks.Add(new Level1ArticulatorConnectionBlock(reader)); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // byteSize + + 4 // _numConnectionBlocks + + (uint)(12 * _connectionBlocks.Count); // _connectionBlocks + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(8u); + writer.Write((uint)_connectionBlocks.Count); + for (int i = 0; i < _connectionBlocks.Count; i++) + { + _connectionBlocks[i].Write(writer); + } + } + + public IEnumerator GetEnumerator() + { + return _connectionBlocks.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _connectionBlocks.GetEnumerator(); + } + + public void Add(Level1ArticulatorConnectionBlock item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + _connectionBlocks.Add(item); + } + public void Clear() + { + _connectionBlocks.Clear(); + } + public void CopyTo(Level1ArticulatorConnectionBlock[] array, int arrayIndex) + { + _connectionBlocks.CopyTo(array, arrayIndex); + } + public bool Contains(Level1ArticulatorConnectionBlock item) + { + return _connectionBlocks.Contains(item); + } + public int IndexOf(Level1ArticulatorConnectionBlock item) + { + return _connectionBlocks.IndexOf(item); + } + public void Insert(int index, Level1ArticulatorConnectionBlock item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + _connectionBlocks.Insert(index, item); + } + public bool Remove(Level1ArticulatorConnectionBlock item) + { + return _connectionBlocks.Remove(item); + } + public void RemoveAt(int index) + { + _connectionBlocks.RemoveAt(index); + } + + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level2ArticulatorChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level2ArticulatorChunk.cs new file mode 100644 index 00000000..71e8b33d --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level2ArticulatorChunk.cs @@ -0,0 +1,119 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.DLS2 +{ + // Level 2 Articulator Chunk - Page 49 of spec + public sealed class Level2ArticulatorChunk : DLSChunk, IList, IReadOnlyList + { + private readonly List _connectionBlocks; + + public Level2ArticulatorConnectionBlock this[int index] + { + get => _connectionBlocks[index]; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _connectionBlocks[index] = value; + } + } + public int Count => _connectionBlocks.Count; + public bool IsReadOnly => false; + + public Level2ArticulatorChunk() : base("art2") + { + _connectionBlocks = new List(); + } + internal Level2ArticulatorChunk(EndianBinaryReader reader) : base("art2", reader) + { + long endOffset = GetEndOffset(reader); + uint byteSize = reader.ReadUInt32(); + if (byteSize != 8) + { + throw new InvalidDataException(); + } + uint numConnectionBlocks = reader.ReadUInt32(); + _connectionBlocks = new List((int)numConnectionBlocks); + for (uint i = 0; i < numConnectionBlocks; i++) + { + _connectionBlocks.Add(new Level2ArticulatorConnectionBlock(reader)); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // byteSize + + 4 // _numConnectionBlocks + + (uint)(12 * _connectionBlocks.Count); // _connectionBlocks + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(8u); + writer.Write((uint)_connectionBlocks.Count); + for (int i = 0; i < _connectionBlocks.Count; i++) + { + _connectionBlocks[i].Write(writer); + } + } + + public IEnumerator GetEnumerator() + { + return _connectionBlocks.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _connectionBlocks.GetEnumerator(); + } + + public void Add(Level2ArticulatorConnectionBlock item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + _connectionBlocks.Add(item); + } + public void Clear() + { + _connectionBlocks.Clear(); + } + public void CopyTo(Level2ArticulatorConnectionBlock[] array, int arrayIndex) + { + _connectionBlocks.CopyTo(array, arrayIndex); + } + public bool Contains(Level2ArticulatorConnectionBlock item) + { + return _connectionBlocks.Contains(item); + } + public int IndexOf(Level2ArticulatorConnectionBlock item) + { + return _connectionBlocks.IndexOf(item); + } + public void Insert(int index, Level2ArticulatorConnectionBlock item) + { + if (item is null) + { + throw new ArgumentNullException(nameof(item)); + } + _connectionBlocks.Insert(index, item); + } + public bool Remove(Level2ArticulatorConnectionBlock item) + { + return _connectionBlocks.Remove(item); + } + public void RemoveAt(int index) + { + _connectionBlocks.RemoveAt(index); + } + + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ListChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ListChunk.cs new file mode 100644 index 00000000..1c4107ee --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ListChunk.cs @@ -0,0 +1,112 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Kermalis.DLS2 +{ + // LIST Chunk - Page 40 of spec + public sealed class ListChunk : DLSChunk, IList, IReadOnlyList + { + /// Length 4 + public string Identifier { get; set; } + private readonly List _children; + + public int Count => _children.Count; + public bool IsReadOnly => false; + public DLSChunk this[int index] + { + get => _children[index]; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _children[index] = value; + } + } + + public ListChunk(string identifier) : base("LIST") + { + Identifier = identifier; + _children = new List(); + } + internal ListChunk(EndianBinaryReader reader) : base("LIST", reader) + { + long endOffset = GetEndOffset(reader); + Identifier = reader.ReadString(4, false); + _children = GetAllChunks(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4; // Identifier + foreach (DLSChunk c in _children) + { + c.UpdateSize(); + Size += c.Size + 8; + } + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(Identifier, 4); + foreach (DLSChunk c in _children) + { + c.Write(writer); + } + } + + public void Add(DLSChunk chunk) + { + if (chunk is null) + { + throw new ArgumentNullException(nameof(chunk)); + } + _children.Add(chunk); + } + public void Clear() + { + _children.Clear(); + } + public bool Contains(DLSChunk chunk) + { + return _children.Contains(chunk); + } + public void CopyTo(DLSChunk[] array, int arrayIndex) + { + _children.CopyTo(array, arrayIndex); + } + public int IndexOf(DLSChunk chunk) + { + return _children.IndexOf(chunk); + } + public void Insert(int index, DLSChunk chunk) + { + if (chunk is null) + { + throw new ArgumentNullException(nameof(chunk)); + } + _children.Insert(index, chunk); + } + public bool Remove(DLSChunk chunk) + { + return _children.Remove(chunk); + } + public void RemoveAt(int index) + { + _children.RemoveAt(index); + } + + public IEnumerator GetEnumerator() + { + return _children.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _children.GetEnumerator(); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/PoolTableChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/PoolTableChunk.cs new file mode 100644 index 00000000..07d698cf --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/PoolTableChunk.cs @@ -0,0 +1,71 @@ +using Kermalis.EndianBinaryIO; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace Kermalis.DLS2 +{ + // Pool Table Chunk - Page 54 of spec + public sealed class PoolTableChunk : DLSChunk, IReadOnlyList + { + private uint _numCues; + private List _poolCues; + + public uint this[int index] => _poolCues[index]; + public int Count => (int)_numCues; + + internal PoolTableChunk() : base("ptbl") + { + _poolCues = new List(); + } + internal PoolTableChunk(EndianBinaryReader reader) : base("ptbl", reader) + { + long endOffset = GetEndOffset(reader); + uint byteSize = reader.ReadUInt32(); + if (byteSize != 8) + { + throw new InvalidDataException(); + } + _numCues = reader.ReadUInt32(); + _poolCues = new List((int)_numCues); + for (uint i = 0; i < _numCues; i++) + { + _poolCues.Add(reader.ReadUInt32()); + } + EatRemainingBytes(reader, endOffset); + } + + internal void UpdateCues(List newCues) + { + _numCues = (uint)newCues.Count; + _poolCues = newCues; + } + + internal override void UpdateSize() + { + Size = 4 // byteSize + + 4 // _numCues + + (4 * _numCues); // _poolCues + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(8u); + writer.Write(_numCues); + for (int i = 0; i < _numCues; i++) + { + writer.Write(_poolCues[i]); + } + } + + public IEnumerator GetEnumerator() + { + return _poolCues.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _poolCues.GetEnumerator(); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RawDataChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RawDataChunk.cs new file mode 100644 index 00000000..117d24fe --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RawDataChunk.cs @@ -0,0 +1,50 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + public abstract class RawDataChunk : DLSChunk + { + private byte[] _data; + public byte[] Data + { + get => _data; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _data = value; + } + } + + protected RawDataChunk(string name, byte[] data) : base(name) + { + Data = data; + } + protected RawDataChunk(string name, EndianBinaryReader reader) : base(name, reader) + { + _data = reader.ReadBytes((int)Size); + } + + internal override void UpdateSize() + { + Size = (uint)_data.Length; + if (Size % 2 != 0) // Align by 2 bytes + { + Size++; + } + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(_data); + for (int i = _data.Length; i < Size; i++) + { + writer.Write((byte)0); + } + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RegionHeaderChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RegionHeaderChunk.cs new file mode 100644 index 00000000..aaabd0b9 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RegionHeaderChunk.cs @@ -0,0 +1,52 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + // Region Header Chunk - Page 45 of spec + public sealed class RegionHeaderChunk : DLSChunk + { + public Range KeyRange { get; set; } + public Range VelocityRange { get; set; } + public ushort Options { get; set; } + public ushort KeyGroup { get; set; } + public ushort Layer { get; set; } + + public RegionHeaderChunk() : base("rgnh") + { + KeyRange = new Range(0, 127); + VelocityRange = new Range(0, 127); + } + internal RegionHeaderChunk(EndianBinaryReader reader) : base("rgnh", reader) + { + long endOffset = GetEndOffset(reader); + KeyRange = new Range(reader); + VelocityRange = new Range(reader); + Options = reader.ReadUInt16(); + KeyGroup = reader.ReadUInt16(); + if (Size >= 14) // Size of 12 is also valid + { + Layer = reader.ReadUInt16(); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // KeyRange + + 4 // VelocityRange + + 2 // Options + + 2 // KeyGroup + + 2; // Layer + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + KeyRange.Write(writer); + VelocityRange.Write(writer); + writer.Write(Options); + writer.Write(KeyGroup); + writer.Write(Layer); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/UnsupportedChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/UnsupportedChunk.cs new file mode 100644 index 00000000..acdccb1c --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/UnsupportedChunk.cs @@ -0,0 +1,10 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class UnsupportedChunk : RawDataChunk + { + public UnsupportedChunk(string name, byte[] data) : base(name, data) { } + internal UnsupportedChunk(string name, EndianBinaryReader reader) : base(name, reader) { } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/VersionChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/VersionChunk.cs new file mode 100644 index 00000000..42302bf5 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/VersionChunk.cs @@ -0,0 +1,14 @@ +namespace Kermalis.DLS2 +{ + // TODO: + + /*public sealed class VersionChunk : DLSChunk + { + + + internal VersionChunk(EndianBinaryReader reader) : base("vers", reader) + { + + } + }*/ +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveLinkChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveLinkChunk.cs new file mode 100644 index 00000000..37cb660f --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveLinkChunk.cs @@ -0,0 +1,43 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class WaveLinkChunk : DLSChunk + { + public WaveLinkOptions Options { get; set; } + public ushort PhaseGroup { get; set; } + public WaveLinkChannels Channels { get; set; } + public uint TableIndex { get; set; } + + public WaveLinkChunk() : base("wlnk") + { + Channels = WaveLinkChannels.Left; + } + internal WaveLinkChunk(EndianBinaryReader reader) : base("wlnk", reader) + { + long endOffset = GetEndOffset(reader); + Options = reader.ReadEnum(); + PhaseGroup = reader.ReadUInt16(); + Channels = reader.ReadEnum(); + TableIndex = reader.ReadUInt32(); + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 2 // Options + + 2 // PhaseGroup + + 4 // Channel + + 4; // TableIndex + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(Options); + writer.Write(PhaseGroup); + writer.Write(Channels); + writer.Write(TableIndex); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveSampleChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveSampleChunk.cs new file mode 100644 index 00000000..d005392a --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveSampleChunk.cs @@ -0,0 +1,69 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.DLS2 +{ + public sealed class WaveSampleChunk : DLSChunk + { + public ushort UnityNote { get; set; } + public short FineTune { get; set; } + public int Gain { get; set; } + public WaveSampleOptions Options { get; set; } + + public WaveSampleLoop Loop { get; set; } // Combining "SampleLoops" and the loop list + + public WaveSampleChunk() : base("wsmp") + { + UnityNote = 60; + Loop = null; + } + internal WaveSampleChunk(EndianBinaryReader reader) : base("wsmp", reader) + { + long endOffset = GetEndOffset(reader); + uint byteSize = reader.ReadUInt32(); + if (byteSize != 20) + { + throw new InvalidDataException(); + } + UnityNote = reader.ReadUInt16(); + FineTune = reader.ReadInt16(); + Gain = reader.ReadInt32(); + Options = reader.ReadEnum(); + if (reader.ReadUInt32() == 1) + { + Loop = new WaveSampleLoop(reader); + } + EatRemainingBytes(reader, endOffset); + } + + internal override void UpdateSize() + { + Size = 4 // byteSize + + 2 // UnityNote + + 2 // FineTune + + 4 // Gain + + 4 // Options + + 4 // DoesLoop + + (Loop is null ? 0u : 16u); // Loop + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(20u); + writer.Write(UnityNote); + writer.Write(FineTune); + writer.Write(Gain); + writer.Write(Options); + if (Loop is null) + { + writer.Write(0u); + } + else + { + writer.Write(1u); + Loop.Write(writer); + } + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs new file mode 100644 index 00000000..6a763662 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs @@ -0,0 +1,235 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Kermalis.DLS2 +{ + public sealed class DLS : IList, IReadOnlyList + { + private readonly List _chunks; + + public int Count => _chunks.Count; + public bool IsReadOnly => false; + public DLSChunk this[int index] + { + get => _chunks[index]; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + _chunks[index] = value; + } + } + + public CollectionHeaderChunk CollectionHeader => GetChunk(); + public ListChunk InstrumentList => GetListChunk("lins"); + public PoolTableChunk PoolTable => GetChunk(); + public ListChunk WavePool => GetListChunk("wvpl"); + + private T GetChunk() where T : DLSChunk + { + return (T)_chunks.Find(c => c is T); + } + private ListChunk GetListChunk(string str) + { + return (ListChunk)_chunks.Find(c => c is ListChunk lc && lc.Identifier == str); + } + +#if DEBUG + public static void Main() + { + //new DLS(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games\M\test.dls"); + //new DLS(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games\M\test2.dls"); + //new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\Arachno SoundFont - Version 1.0.dls"); + //new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\Musyng Kite.dls"); + new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\RSE Corrected Soundfont Revision 17.dls"); + } +#endif + + /// For creating. + public DLS() + { + _chunks = new List() + { + new CollectionHeaderChunk(), + new ListChunk("lins"), + new PoolTableChunk(), + new ListChunk("wvpl"), + }; + } + public DLS(string path) + { + using (var reader = new EndianBinaryReader(File.Open(path, FileMode.Open))) + { + _chunks = Init(reader); + } + } + public DLS(Stream stream) + { + _chunks = Init(new EndianBinaryReader(stream)); + } + private List Init(EndianBinaryReader reader) + { + string str = reader.ReadString(4, false); + if (str != "RIFF") + { + throw new InvalidDataException("RIFF header was not found at the start of the file."); + } + uint size = reader.ReadUInt32(); + long endOffset = reader.BaseStream.Position + size; + str = reader.ReadString(4, false); + if (str != "DLS ") + { + throw new InvalidDataException("DLS header was not found at the expected offset."); + } + return DLSChunk.GetAllChunks(reader, endOffset); + } + + public void UpdateCollectionHeader() + { + CollectionHeader.NumInstruments = (uint)InstrumentList.Count; + } + /// Updates the pointers in the . Should be called after modifying . + public void UpdatePoolTable() + { + ListChunk wvpl = WavePool; + var newCues = new List(wvpl.Count); + uint cur = 0; + for (int i = 0; i < wvpl.Count; i++) + { + newCues.Add(cur); + DLSChunk c = wvpl[i]; + c.UpdateSize(); + cur += c.Size + 8; + } + PoolTable.UpdateCues(newCues); + } + public void Save(string path) + { + UpdateCollectionHeader(); + UpdatePoolTable(); + + using (var writer = new EndianBinaryWriter(File.Open(path, FileMode.Create))) + { + writer.Write("RIFF", 4); + writer.Write(UpdateSize()); + writer.Write("DLS ", 4); + foreach (DLSChunk c in _chunks) + { + c.Write(writer); + } + } + } + + public string GetHierarchy() + { + var str = new StringBuilder(); + int tabLevel = 0; + void ApplyTabLevel() + { + for (int t = 0; t < tabLevel; t++) + { + str.Append('\t'); + } + } + void Recursion(IReadOnlyList parent, string listName) + { + ApplyTabLevel(); + str.Append($"{listName} ({parent.Count})"); + tabLevel++; + foreach (DLSChunk c in parent) + { + str.AppendLine(); + if (c is ListChunk lc) + { + Recursion(lc, $"{lc.ChunkName} '{lc.Identifier}'"); + } + else + { + ApplyTabLevel(); + str.Append($"<{c.ChunkName}>"); + if (c is InfoSubChunk ic) + { + str.Append($" [\"{ic.Text}\"]"); + } + else if (c is RawDataChunk dc) + { + str.Append($" [{dc.Data.Length} bytes]"); + } + } + } +#pragma warning disable IDE0059 // Unnecessary assignment of a value + tabLevel--; +#pragma warning restore IDE0059 // Unnecessary assignment of a value + } + Recursion(this, "RIFF 'DLS '"); + return str.ToString(); + } + + private uint UpdateSize() + { + uint size = 4; + foreach (DLSChunk c in _chunks) + { + c.UpdateSize(); + size += c.Size + 8; + } + return size; + } + + public void Add(DLSChunk chunk) + { + if (chunk is null) + { + throw new ArgumentNullException(nameof(chunk)); + } + _chunks.Add(chunk); + } + public void Clear() + { + _chunks.Clear(); + } + public bool Contains(DLSChunk chunk) + { + return _chunks.Contains(chunk); + } + public void CopyTo(DLSChunk[] array, int arrayIndex) + { + _chunks.CopyTo(array, arrayIndex); + } + public int IndexOf(DLSChunk chunk) + { + return _chunks.IndexOf(chunk); + } + public void Insert(int index, DLSChunk chunk) + { + if (chunk is null) + { + throw new ArgumentNullException(nameof(chunk)); + } + _chunks.Insert(index, chunk); + } + public bool Remove(DLSChunk chunk) + { + return _chunks.Remove(chunk); + } + public void RemoveAt(int index) + { + _chunks.RemoveAt(index); + } + + public IEnumerator GetEnumerator() + { + return _chunks.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return _chunks.GetEnumerator(); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS2.csproj b/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS2.csproj new file mode 100644 index 00000000..3f9657eb --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS2.csproj @@ -0,0 +1,30 @@ + + + + Kermalis + + DLS2 + DLS2 + DLS2 + Kermalis.DLS2 + 1.0.0.0 + ..\Build + + + + netcoreapp3.1 + Exe + + + + netstandard2.0 + Auto + none + false + + + + + + + diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level1ArticulatorEnums.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level1ArticulatorEnums.cs new file mode 100644 index 00000000..0cbe8222 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level1ArticulatorEnums.cs @@ -0,0 +1,44 @@ +namespace Kermalis.DLS2 +{ + public enum Level1ArticulatorSource : ushort + { + None = 0x0, + LFO = 0x1, + KeyOnVelocity = 0x2, + KeyNumber = 0x3, + EG1 = 0x4, + EG2 = 0x5, + PitchWheel = 0x6, + Modulation_CC1 = 0x81, + ChannelVolume_CC7 = 0x87, + Pan_CC10 = 0x8A, + Expression_CC11 = 0x8B, + PitchBendRange_RPN0 = 0x100, + FineTune_RPN1 = 0x101, + CoarseTune_RPN2 = 0x102 + } + + public enum Level1ArticulatorDestination : ushort + { + None = 0x0, + Gain = 0x1, + Pitch = 0x3, + Pan = 0x4, + LFOFrequency = 0x104, + LFOStartDelay = 0x105, + EG1AttackTime = 0x206, + EG1DecayTime = 0x207, + EG1ReleaseTime = 0x209, + EG1SustainLevel = 0x20A, + EG2AttackTime = 0x30A, + EG2DecayTime = 0x30B, + EG2ReleaseTime = 0x30D, + EG2SustainLevel = 0x30E + } + + public enum Level1ArticulatorTransform : byte + { + None = 0x0, + Concave = 0x1 + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level2ArticulatorEnums.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level2ArticulatorEnums.cs new file mode 100644 index 00000000..4437952e --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level2ArticulatorEnums.cs @@ -0,0 +1,69 @@ +namespace Kermalis.DLS2 +{ + public enum Level2ArticulatorSource : ushort + { + None = 0x0, + LFO = 0x1, + KeyOnVelocity = 0x2, + KeyNumber = 0x3, + EG1 = 0x4, + EG2 = 0x5, + PitchWheel = 0x6, + PolyPressure = 0x7, + ChannelPressure = 0x8, + Vibrato = 0x9, + Modulation_CC1 = 0x81, + ChannelVolume_CC7 = 0x87, + Pan_CC10 = 0x8A, + Expression_CC11 = 0x8B, + ChorusSend_CC91 = 0xDB, + Reverb_SendCC93 = 0xDD, + PitchBendRange_RPN0 = 0x100, + FineTune_RPN1 = 0x101, + CoarseTune_RPN2 = 0x102 + } + + public enum Level2ArticulatorDestination : ushort + { + None = 0x0, + Gain = 0x1, + Pitch = 0x3, + Pan = 0x4, + KeyNumber = 0x5, + Left = 0x10, + Right = 0x11, + Center = 0x12, + LFEChannel = 0x13, + LeftRear = 0x14, + RightRear = 0x15, + Chorus = 0x80, + Reverb = 0x81, + LFOFrequency = 0x104, + LFOStartDelay = 0x105, + VIBFrequency = 0x114, + VIBStartDelay = 0x115, + EG1AttackTime = 0x206, + EG1DecayTime = 0x207, + EG1ReleaseTime = 0x209, + EG1SustainLevel = 0x20A, + EG1DelayTime = 0x20B, + EG1HoldTime = 0x20C, + EG1ShutdownTime = 0x20D, + EG2AttackTime = 0x30A, + EG2DecayTime = 0x30B, + EG2ReleaseTime = 0x30D, + EG2SustainLevel = 0x30E, + EG2DelayTime = 0x30F, + EG2HoldTime = 0x310, + FilterCutoff = 0x500, + FilterResonance = 0x501 + } + + public enum Level2ArticulatorTransform : byte + { + None = 0x0, + Concave = 0x1, + Convex = 0x2, + Switch = 0x3 + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveFormat.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveFormat.cs new file mode 100644 index 00000000..4cab7976 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveFormat.cs @@ -0,0 +1,15 @@ +namespace Kermalis.DLS2 +{ + public enum WaveFormat : ushort + { + Unknown = 0, + PCM = 1, + MSADPCM = 2, + Float = 3, + ALaw = 6, + MuLaw = 7, + DVIADPCM = 17, + IMAADPCM = 17, + Extensible = 0xFFFE + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkChannels.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkChannels.cs new file mode 100644 index 00000000..678bd25b --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkChannels.cs @@ -0,0 +1,28 @@ +using System; + +namespace Kermalis.DLS2 +{ + [Flags] + public enum WaveLinkChannels : uint + { + None = 0, + Left = 1 << 0, + Right = 1 << 1, + Center = 1 << 2, + LowFrequencyEnergy = 1 << 3, + SurroundLeft = 1 << 4, + SurroundRight = 1 << 5, + LeftOfCenter = 1 << 6, + RightOfCenter = 1 << 7, + SurroundCenter = 1 << 8, + SideLeft = 1 << 9, + SideRight = 1 << 10, + Top = 1 << 11, + TopFrontLeft = 1 << 12, + TopFrontCenter = 1 << 13, + TopFrontRight = 1 << 14, + TopRearLeft = 1 << 15, + TopRearCenter = 1 << 16, + TopRearRight = 1 << 17 + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkOptions.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkOptions.cs new file mode 100644 index 00000000..7f47d8c1 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkOptions.cs @@ -0,0 +1,12 @@ +using System; + +namespace Kermalis.DLS2 +{ + [Flags] + public enum WaveLinkOptions : ushort + { + None = 0, + PhaseMaster = 1 << 0, + MultiChannel = 1 << 1 + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleLoop.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleLoop.cs new file mode 100644 index 00000000..68b63132 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleLoop.cs @@ -0,0 +1,8 @@ +namespace Kermalis.DLS2 +{ + public enum LoopType : uint + { + Forward = 0, + Release = 1 + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleOptions.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleOptions.cs new file mode 100644 index 00000000..ef87bab4 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleOptions.cs @@ -0,0 +1,12 @@ +using System; + +namespace Kermalis.DLS2 +{ + [Flags] + public enum WaveSampleOptions : uint + { + None = 0, + NoTruncation = 1 << 0, + NoCompression = 1 << 1 + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/ConnectionBlock.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/ConnectionBlock.cs new file mode 100644 index 00000000..d8a9f75d --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/ConnectionBlock.cs @@ -0,0 +1,163 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + public sealed class Level1ArticulatorConnectionBlock + { + public Level1ArticulatorSource Source { get; set; } + public Level1ArticulatorSource Control { get; set; } + public Level1ArticulatorDestination Destination { get; set; } + public Level1ArticulatorTransform Transform { get; set; } + public int Scale { get; set; } + + public Level1ArticulatorConnectionBlock() { } + internal Level1ArticulatorConnectionBlock(EndianBinaryReader reader) + { + Source = reader.ReadEnum(); + Control = reader.ReadEnum(); + Destination = reader.ReadEnum(); + Transform = reader.ReadEnum(); + Scale = reader.ReadInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Source); + writer.Write(Control); + writer.Write(Destination); + writer.Write(Transform); + writer.Write(Scale); + } + } + + public sealed class Level2ArticulatorConnectionBlock + { + public Level2ArticulatorSource Source { get; set; } + public Level2ArticulatorSource Control { get; set; } + public Level2ArticulatorDestination Destination { get; set; } + public ushort Transform_Raw { get; set; } + public int Scale { get; set; } + + public bool InvertSource + { + get => (Transform_Raw >> 15) != 0; + set + { + if (value) + { + Transform_Raw |= 1 << 15; + } + else + { + Transform_Raw &= unchecked((ushort)~(1 << 15)); + } + } + } + public bool BipolarSource + { + get => ((Transform_Raw >> 14) & 1) != 0; + set + { + if (value) + { + Transform_Raw |= 1 << 14; + } + else + { + Transform_Raw &= unchecked((ushort)~(1 << 14)); + } + } + } + public Level2ArticulatorTransform TransformSource + { + get => (Level2ArticulatorTransform)((Transform_Raw >> 10) & 0xF); + set + { + if (value > (Level2ArticulatorTransform)0xF) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Transform_Raw &= unchecked((ushort)~(0xF << 10)); + Transform_Raw |= (ushort)((ushort)value << 10); + } + } + + public bool InvertDestination + { + get => ((Transform_Raw >> 9) & 1) != 0; + set + { + if (value) + { + Transform_Raw |= 1 << 9; + } + else + { + Transform_Raw &= unchecked((ushort)~(1 << 9)); + } + } + } + public bool BipolarDestination + { + get => ((Transform_Raw >> 8) & 1) != 0; + set + { + if (value) + { + Transform_Raw |= 1 << 8; + } + else + { + Transform_Raw &= unchecked((ushort)~(1 << 8)); + } + } + } + public Level2ArticulatorTransform TransformDestination + { + get => (Level2ArticulatorTransform)((Transform_Raw >> 4) & 0xF); + set + { + if (value > (Level2ArticulatorTransform)0xF) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Transform_Raw &= unchecked((ushort)~(0xF << 4)); + Transform_Raw |= (ushort)((ushort)value << 4); + } + } + + public Level2ArticulatorTransform TransformOutput + { + get => (Level2ArticulatorTransform)(Transform_Raw & 0xF); + set + { + if (value > (Level2ArticulatorTransform)0xF) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Transform_Raw &= unchecked((ushort)~0xF); + Transform_Raw |= (ushort)value; + } + } + + public Level2ArticulatorConnectionBlock() { } + internal Level2ArticulatorConnectionBlock(EndianBinaryReader reader) + { + Source = reader.ReadEnum(); + Control = reader.ReadEnum(); + Destination = reader.ReadEnum(); + Transform_Raw = reader.ReadUInt16(); + Scale = reader.ReadInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Source); + writer.Write(Control); + writer.Write(Destination); + writer.Write(Transform_Raw); + writer.Write(Scale); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/DLSID.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/DLSID.cs new file mode 100644 index 00000000..d955ef70 --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/DLSID.cs @@ -0,0 +1,122 @@ +using Kermalis.EndianBinaryIO; +using System; +#if !DEBUG +using System.Collections.Generic; +#endif +using System.Linq; + +namespace Kermalis.DLS2 +{ + public sealed class DLSID + { + public uint Data1 { get; set; } + public ushort Data2 { get; set; } + public ushort Data3 { get; set; } + public byte[] Data4 { get; } + + public static DLSID Query_GMInHardware { get; } = new DLSID(0x178F2F24, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_GSInHardware { get; } = new DLSID(0x178F2F25, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_XGInHardware { get; } = new DLSID(0x178F2F26, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_SupportsDLS1 { get; } = new DLSID(0x178F2F27, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_SampleMemorySize { get; } = new DLSID(0x178F2F28, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); + public static DLSID Query_SamplePlaybackRate { get; } = new DLSID(0x2A91F713, 0xA4BF, 0x11D2, new byte[] { 0xBB, 0xDF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 }); + public static DLSID Query_ManufacturersID { get; } = new DLSID(0xB03E1181, 0x8095, 0x11D2, new byte[] { 0xA1, 0xEF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 }); + public static DLSID Query_ProductID { get; } = new DLSID(0xB03E1182, 0x8095, 0x11D2, new byte[] { 0xA1, 0xEF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 }); + public static DLSID Query_SupportsDLS2 { get; } = new DLSID(0xF14599E5, 0x4689, 0x11D2, new byte[] { 0xAF, 0xA6, 0x00, 0xAA, 0x00, 0x24, 0xD8, 0xB6 }); + + public DLSID() + { + Data4 = new byte[8]; + } + internal DLSID(EndianBinaryReader reader) + { + Data1 = reader.ReadUInt32(); + Data2 = reader.ReadUInt16(); + Data3 = reader.ReadUInt16(); + Data4 = reader.ReadBytes(8); + } + public DLSID(uint data1, ushort data2, ushort data3, byte[] data4) + { + if (data4 is null) + { + throw new ArgumentNullException(nameof(data4)); + } + if (data4.Length != 8) + { + throw new ArgumentOutOfRangeException(nameof(data4.Length)); + } + Data1 = data1; + Data2 = data2; + Data3 = data3; + Data4 = data4; + } + public DLSID(byte[] data) + { + if (data is null) + { + throw new ArgumentNullException(nameof(data)); + } + if (data.Length != 16) + { + throw new ArgumentOutOfRangeException(nameof(data.Length)); + } + Data1 = (uint)EndianBitConverter.BytesToInt32(data, 0, Endianness.LittleEndian); + Data2 = (ushort)EndianBitConverter.BytesToInt16(data, 4, Endianness.LittleEndian); + Data3 = (ushort)EndianBitConverter.BytesToInt16(data, 6, Endianness.LittleEndian); + Data4 = new byte[8]; + for (int i = 0; i < 8; i++) + { + Data4[i] = data[8 + i]; + } + } + + public void Write(EndianBinaryWriter writer) + { + writer.Write(Data1); + writer.Write(Data2); + writer.Write(Data3); + writer.Write(Data4); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(obj, this)) + { + return true; + } + if (obj is DLSID id) + { + return id.Data1 == Data1 && id.Data2 == Data2 && id.Data3 == Data3 && id.Data4.SequenceEqual(Data4); + } + return false; + } + public override int GetHashCode() + { + // .NET Standard does not have this method +#if DEBUG + return HashCode.Combine(Data1, Data2, Data3, Data4); +#else + int hashCode = -0x8CAC62A; + hashCode = hashCode * -0x5AAAAAD7 + Data1.GetHashCode(); + hashCode = hashCode * -0x5AAAAAD7 + Data2.GetHashCode(); + hashCode = hashCode * -0x5AAAAAD7 + Data3.GetHashCode(); + hashCode = hashCode * -0x5AAAAAD7 + EqualityComparer.Default.GetHashCode(Data4); + return hashCode; +#endif + } + public override string ToString() + { + string str = Data1.ToString("X8") + '-' + Data2.ToString("X4") + '-' + Data3.ToString("X4") + '-'; + for (int i = 0; i < 2; i++) + { + str += Data4[i].ToString("X2"); + } + str += '-'; + for (int i = 2; i < 8; i++) + { + str += Data4[i].ToString("X2"); + } + return str; + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/MIDILocale.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/MIDILocale.cs new file mode 100644 index 00000000..49cd325b --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/MIDILocale.cs @@ -0,0 +1,87 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.DLS2 +{ + // MIDILOCALE - Page 45 of spec + public sealed class MIDILocale + { + public uint Bank_Raw { get; set; } + public uint Instrument_Raw { get; set; } + + public byte CC32 + { + get => (byte)(Bank_Raw & 0x7F); + set + { + if (value > 0x7F) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Bank_Raw &= unchecked((uint)~0x7F); + Bank_Raw |= value; + } + } + public byte CC0 + { + get => (byte)((Bank_Raw >> 7) & 0x7F); + set + { + if (value > 0x7F) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Bank_Raw &= unchecked((uint)~(0x7F << 7)); + Bank_Raw |= (uint)(value << 7); + } + } + public bool IsDrum + { + get => (Bank_Raw >> 31) != 0; + set + { + if (value) + { + Bank_Raw |= 1u << 31; + } + else + { + Bank_Raw &= ~(1 << 31); + } + } + } + public byte Instrument + { + get => (byte)(Instrument_Raw & 0x7F); + set + { + if (value > 0x7F) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + Instrument_Raw &= unchecked((uint)~0x7F); + Instrument_Raw |= value; + } + } + + public MIDILocale() { } + public MIDILocale(byte cc32, byte cc0, bool isDrum, byte instrument) + { + CC32 = cc32; + CC0 = cc0; + IsDrum = isDrum; + Instrument = instrument; + } + internal MIDILocale(EndianBinaryReader reader) + { + Bank_Raw = reader.ReadUInt32(); + Instrument_Raw = reader.ReadUInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Bank_Raw); + writer.Write(Instrument_Raw); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/Range.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/Range.cs new file mode 100644 index 00000000..7248bf8f --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/Range.cs @@ -0,0 +1,28 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class Range + { + public ushort Low { get; set; } + public ushort High { get; set; } + + public Range() { } + public Range(ushort low, ushort high) + { + Low = low; + High = high; + } + internal Range(EndianBinaryReader reader) + { + Low = reader.ReadUInt16(); + High = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Low); + writer.Write(High); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveInfo.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveInfo.cs new file mode 100644 index 00000000..2f1cb25e --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveInfo.cs @@ -0,0 +1,32 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.DLS2 +{ + public sealed class WaveInfo + { + public WaveFormat FormatTag { get; set; } + public ushort Channels { get; set; } + public uint SamplesPerSec { get; set; } + public uint AvgBytesPerSec { get; set; } + public ushort BlockAlign { get; set; } + + internal WaveInfo() { } + internal WaveInfo(EndianBinaryReader reader) + { + FormatTag = reader.ReadEnum(); + Channels = reader.ReadUInt16(); + SamplesPerSec = reader.ReadUInt32(); + AvgBytesPerSec = reader.ReadUInt32(); + BlockAlign = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(FormatTag); + writer.Write(Channels); + writer.Write(SamplesPerSec); + writer.Write(AvgBytesPerSec); + writer.Write(BlockAlign); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveSampleLoop.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveSampleLoop.cs new file mode 100644 index 00000000..8e98a54d --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveSampleLoop.cs @@ -0,0 +1,33 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.DLS2 +{ + public sealed class WaveSampleLoop + { + public LoopType LoopType { get; set; } + public uint LoopStart { get; set; } + public uint LoopLength { get; set; } + + public WaveSampleLoop() { } + internal WaveSampleLoop(EndianBinaryReader reader) + { + uint byteSize = reader.ReadUInt32(); + if (byteSize != 16) + { + throw new InvalidDataException(); + } + LoopType = reader.ReadEnum(); + LoopStart = reader.ReadUInt32(); + LoopLength = reader.ReadUInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(16u); + writer.Write(LoopType); + writer.Write(LoopStart); + writer.Write(LoopLength); + } + } +} diff --git a/Legacy Repo (.NET Framework)/DLS2/LICENSE.md b/Legacy Repo (.NET Framework)/DLS2/LICENSE.md new file mode 100644 index 00000000..c0a4d4aa --- /dev/null +++ b/Legacy Repo (.NET Framework)/DLS2/LICENSE.md @@ -0,0 +1,23 @@ +The zlib/libpng License +======================= + +Copyright (c) 2016 gocha + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/SoundFont2/.gitattributes b/Legacy Repo (.NET Framework)/SoundFont2/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/Legacy Repo (.NET Framework)/SoundFont2/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/Legacy Repo (.NET Framework)/SoundFont2/.gitignore b/Legacy Repo (.NET Framework)/SoundFont2/.gitignore new file mode 100644 index 00000000..80921d31 --- /dev/null +++ b/Legacy Repo (.NET Framework)/SoundFont2/.gitignore @@ -0,0 +1,262 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +Build/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/SoundFont2/LICENSE.md b/Legacy Repo (.NET Framework)/SoundFont2/LICENSE.md new file mode 100644 index 00000000..c0a4d4aa --- /dev/null +++ b/Legacy Repo (.NET Framework)/SoundFont2/LICENSE.md @@ -0,0 +1,23 @@ +The zlib/libpng License +======================= + +Copyright (c) 2016 gocha + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/SoundFont2/README.md b/Legacy Repo (.NET Framework)/SoundFont2/README.md new file mode 100644 index 00000000..73c5404e --- /dev/null +++ b/Legacy Repo (.NET Framework)/SoundFont2/README.md @@ -0,0 +1,11 @@ +# Kermalis's C# SoundFont 2 library + +A C# library that can read and write SF2 sound bank files. + +---- +# To Do: +* Prevent loading the terminal data +* Find a way to "public seal" SF2Chunk and SF2ListChunk +* Getting info such as samples, modulators and generators +* Separate chunks into their own files +* Certain generators must be in a specified index (KeyRange, VelRange, SampleID and Instrument) according to spec \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2.sln b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2.sln new file mode 100644 index 00000000..b8d3f51d --- /dev/null +++ b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2035 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoundFont2", "SoundFont2\SoundFont2.csproj", "{3C554511-03EA-41F0-AA5D-BABB9C330A33}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3C554511-03EA-41F0-AA5D-BABB9C330A33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C554511-03EA-41F0-AA5D-BABB9C330A33}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A6CED513-A215-4312-8C16-BAF58AA39F72} + EndGlobalSection +EndGlobal diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2.cs b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2.cs new file mode 100644 index 00000000..1811dc15 --- /dev/null +++ b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2.cs @@ -0,0 +1,152 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.SoundFont2 +{ + public sealed class SF2 + { + private uint _size; + public InfoListChunk InfoChunk { get; } + public SdtaListChunk SoundChunk { get; } + public PdtaListChunk HydraChunk { get; } + + /// For creating + public SF2() + { + InfoChunk = new InfoListChunk(this); + SoundChunk = new SdtaListChunk(this); + HydraChunk = new PdtaListChunk(this); + } + + /// For reading + public SF2(string path) + { + using (var reader = new EndianBinaryReader(File.Open(path, FileMode.Open))) + { + string str = reader.ReadString(4, false); + if (str != "RIFF") + { + throw new InvalidDataException("RIFF header was not found at the start of the file."); + } + _size = reader.ReadUInt32(); + str = reader.ReadString(4, false); + if (str != "sfbk") + { + throw new InvalidDataException("sfbk header was not found at the expected offset."); + } + InfoChunk = new InfoListChunk(this, reader); + SoundChunk = new SdtaListChunk(this, reader); + HydraChunk = new PdtaListChunk(this, reader); + } + } + + public void Save(string path) + { + using (var writer = new EndianBinaryWriter(File.Open(path, FileMode.Create))) + { + AddTerminals(); + + writer.Write("RIFF", 4); + writer.Write(_size); + writer.Write("sfbk", 4); + + InfoChunk.Write(writer); + SoundChunk.Write(writer); + HydraChunk.Write(writer); + } + } + + + /// Returns sample index + public uint AddSample(short[] pcm16, string name, bool bLoop, uint loopPos, uint sampleRate, byte originalKey, sbyte pitchCorrection) + { + uint start = SoundChunk.SMPLSubChunk.AddSample(pcm16, bLoop, loopPos); + // If the sample is looped the standard requires us to add the 8 bytes from the start of the loop to the end + uint end, loopEnd, loopStart; + + uint len = (uint)pcm16.Length; + if (bLoop) + { + end = start + len + 8; + loopStart = start + loopPos; loopEnd = start + len; + } + else + { + end = start + len; + loopStart = 0; loopEnd = 0; + } + + return AddSampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection); + } + /// Returns instrument index + public uint AddInstrument(string name) + { + return HydraChunk.INSTSubChunk.AddInstrument(new SF2Instrument(name, (ushort)HydraChunk.IBAGSubChunk.Count)); + } + public void AddInstrumentBag() + { + HydraChunk.IBAGSubChunk.AddBag(new SF2Bag(this, false)); + } + public void AddInstrumentModulator() + { + HydraChunk.IMODSubChunk.AddModulator(new SF2ModulatorList()); + } + public void AddInstrumentGenerator() + { + HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList()); + } + public void AddInstrumentGenerator(SF2Generator generator, SF2GeneratorAmount amount) + { + HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); + } + public void AddPreset(string name, ushort preset, ushort bank) + { + HydraChunk.PHDRSubChunk.AddPreset(new SF2PresetHeader(name, preset, bank, (ushort)HydraChunk.PBAGSubChunk.Count)); + } + public void AddPresetBag() + { + HydraChunk.PBAGSubChunk.AddBag(new SF2Bag(this, true)); + } + public void AddPresetModulator() + { + HydraChunk.PMODSubChunk.AddModulator(new SF2ModulatorList()); + } + public void AddPresetGenerator() + { + HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList()); + } + public void AddPresetGenerator(SF2Generator generator, SF2GeneratorAmount amount) + { + HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); + } + + private uint AddSampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) + { + return HydraChunk.SHDRSubChunk.AddSample(new SF2SampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection)); + } + private void AddTerminals() + { + AddSampleHeader("EOS", 0, 0, 0, 0, 0, 0, 0); + AddInstrument("EOI"); + AddInstrumentBag(); + AddInstrumentGenerator(); + AddInstrumentModulator(); + AddPreset("EOP", 0xFF, 0xFF); + AddPresetBag(); + AddPresetGenerator(); + AddPresetModulator(); + } + + internal void UpdateSize() + { + if (InfoChunk == null || SoundChunk == null || HydraChunk == null) + { + return; + } + _size = 4 + + InfoChunk.UpdateSize() + 8 + + SoundChunk.UpdateSize() + 8 + + HydraChunk.UpdateSize() + 8; + } + } +} diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Chunks.cs b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Chunks.cs new file mode 100644 index 00000000..96013042 --- /dev/null +++ b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Chunks.cs @@ -0,0 +1,1105 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; + +namespace Kermalis.SoundFont2 +{ + public class SF2Chunk + { + protected readonly SF2 _sf2; + + /// Length 4 + public string ChunkName { get; } + /// Size in bytes + public uint Size { get; protected set; } + + protected SF2Chunk(SF2 inSf2, string name) + { + _sf2 = inSf2; + ChunkName = name; + } + protected SF2Chunk(SF2 inSf2, EndianBinaryReader reader) + { + _sf2 = inSf2; + ChunkName = reader.ReadString(4, false); + Size = reader.ReadUInt32(); + } + + internal virtual void Write(EndianBinaryWriter writer) + { + writer.Write(ChunkName, 4); + writer.Write(Size); + } + } + + public abstract class SF2ListChunk : SF2Chunk + { + ///Length 4 + public string ListChunkName { get; } + + protected SF2ListChunk(SF2 inSf2, string name) : base(inSf2, "LIST") + { + ListChunkName = name; + Size = 4; + } + protected SF2ListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + ListChunkName = reader.ReadString(4, false); + } + + internal abstract uint UpdateSize(); + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(ListChunkName, 4); + } + } + + public sealed class SF2PresetHeader + { + public const uint Size = 38; + + /// Length 20 + public string PresetName { get; set; } + public ushort Preset { get; set; } + public ushort Bank { get; set; } + public ushort PresetBagIndex { get; set; } + // Reserved for future implementations + private readonly uint _library; + private readonly uint _genre; + private readonly uint _morphology; + + internal SF2PresetHeader(string name, ushort preset, ushort bank, ushort index) + { + PresetName = name; + Preset = preset; + Bank = bank; + PresetBagIndex = index; + } + internal SF2PresetHeader(EndianBinaryReader reader) + { + PresetName = reader.ReadString(20, true); + Preset = reader.ReadUInt16(); + Bank = reader.ReadUInt16(); + PresetBagIndex = reader.ReadUInt16(); + _library = reader.ReadUInt32(); + _genre = reader.ReadUInt32(); + _morphology = reader.ReadUInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(PresetName, 20); + writer.Write(Preset); + writer.Write(Bank); + writer.Write(PresetBagIndex); + writer.Write(_library); + writer.Write(_genre); + writer.Write(_morphology); + } + + public override string ToString() + { + return $"Preset Header - Bank = {Bank}" + + $",\nPreset = {Preset}" + + $",\nName = \"{PresetName}\""; + } + } + + /// Covers sfPresetBag and sfInstBag + public sealed class SF2Bag + { + public const uint Size = 4; + + /// Index in list of generators + public ushort GeneratorIndex { get; set; } + /// Index in list of modulators + public ushort ModulatorIndex { get; set; } + + internal SF2Bag(SF2 inSf2, bool isPresetBag) + { + if (isPresetBag) + { + GeneratorIndex = (ushort)inSf2.HydraChunk.PGENSubChunk.Count; + ModulatorIndex = (ushort)inSf2.HydraChunk.PMODSubChunk.Count; + } + else + { + GeneratorIndex = (ushort)inSf2.HydraChunk.IGENSubChunk.Count; + ModulatorIndex = (ushort)inSf2.HydraChunk.IMODSubChunk.Count; + } + } + internal SF2Bag(EndianBinaryReader reader) + { + GeneratorIndex = reader.ReadUInt16(); + ModulatorIndex = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(GeneratorIndex); + writer.Write(ModulatorIndex); + } + + public override string ToString() + { + return $"Bag - Generator index = {GeneratorIndex}" + + $",\nModulator index = {ModulatorIndex}"; + } + } + + /// Covers sfModList and sfInstModList + public sealed class SF2ModulatorList + { + public const uint Size = 10; + + public SF2Modulator ModulatorSource { get; set; } + public SF2Generator ModulatorDestination { get; set; } + public short ModulatorAmount { get; set; } + public SF2Modulator ModulatorAmountSource { get; set; } + public SF2Transform ModulatorTransform { get; set; } + + internal SF2ModulatorList() { } + internal SF2ModulatorList(EndianBinaryReader reader) + { + ModulatorSource = reader.ReadEnum(); + ModulatorDestination = reader.ReadEnum(); + ModulatorAmount = reader.ReadInt16(); + ModulatorAmountSource = reader.ReadEnum(); + ModulatorTransform = reader.ReadEnum(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(ModulatorSource); + writer.Write(ModulatorDestination); + writer.Write(ModulatorAmount); + writer.Write(ModulatorAmountSource); + writer.Write(ModulatorTransform); + } + + public override string ToString() + { + return $"Modulator List - Modulator source = {ModulatorSource}" + + $",\nModulator destination = {ModulatorDestination}" + + $",\nModulator amount = {ModulatorAmount}" + + $",\nModulator amount source = {ModulatorAmountSource}" + + $",\nModulator transform = {ModulatorTransform}"; + } + } + + public sealed class SF2GeneratorList + { + public const uint Size = 4; + + public SF2Generator Generator { get; set; } + public SF2GeneratorAmount GeneratorAmount { get; set; } + + internal SF2GeneratorList() { } + internal SF2GeneratorList(SF2Generator generator, SF2GeneratorAmount amount) + { + Generator = generator; + GeneratorAmount = amount; + } + internal SF2GeneratorList(EndianBinaryReader reader) + { + Generator = reader.ReadEnum(); + GeneratorAmount = new SF2GeneratorAmount { Amount = reader.ReadInt16() }; + } + + public void Write(EndianBinaryWriter writer) + { + writer.Write(Generator); + writer.Write(GeneratorAmount.Amount); + } + + public override string ToString() + { + return $"Generator List - Generator = {Generator}" + + $",\nGenerator amount = \"{GeneratorAmount}\""; + } + } + + public sealed class SF2Instrument + { + public const uint Size = 22; + + /// Length 20 + public string InstrumentName { get; set; } + public ushort InstrumentBagIndex { get; set; } + + internal SF2Instrument(string name, ushort index) + { + InstrumentName = name; + InstrumentBagIndex = index; + } + internal SF2Instrument(EndianBinaryReader reader) + { + InstrumentName = reader.ReadString(20, true); + InstrumentBagIndex = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(InstrumentName, 20); + writer.Write(InstrumentBagIndex); + } + + public override string ToString() + { + return $"Instrument - Name = \"{InstrumentName}\""; + } + } + + public sealed class SF2SampleHeader + { + public const uint Size = 46; + + /// Length 20 + public string SampleName { get; set; } + public uint Start { get; set; } + public uint End { get; set; } + public uint LoopStart { get; set; } + public uint LoopEnd { get; set; } + public uint SampleRate { get; set; } + public byte OriginalKey { get; set; } + public sbyte PitchCorrection { get; set; } + public ushort SampleLink { get; set; } + public SF2SampleLink SampleType { get; set; } + + internal SF2SampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) + { + SampleName = name; + Start = start; + End = end; + LoopStart = loopStart; + LoopEnd = loopEnd; + SampleRate = sampleRate; + OriginalKey = originalKey; + PitchCorrection = pitchCorrection; + SampleType = SF2SampleLink.MonoSample; + } + internal SF2SampleHeader(EndianBinaryReader reader) + { + SampleName = reader.ReadString(20, true); + Start = reader.ReadUInt32(); + End = reader.ReadUInt32(); + LoopStart = reader.ReadUInt32(); + LoopEnd = reader.ReadUInt32(); + SampleRate = reader.ReadUInt32(); + OriginalKey = reader.ReadByte(); + PitchCorrection = reader.ReadSByte(); + SampleLink = reader.ReadUInt16(); + SampleType = reader.ReadEnum(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(SampleName, 20); + writer.Write(Start); + writer.Write(End); + writer.Write(LoopStart); + writer.Write(LoopEnd); + writer.Write(SampleRate); + writer.Write(OriginalKey); + writer.Write(PitchCorrection); + writer.Write(SampleLink); + writer.Write(SampleType); + } + + public override string ToString() + { + return $"Sample - Name = \"{SampleName}\"" + + $",\nType = {SampleType}"; + } + } + + #region Sub-Chunks + + public sealed class VersionSubChunk : SF2Chunk + { + public SF2VersionTag Version { get; set; } + + internal VersionSubChunk(SF2 inSf2, string subChunkName) : base(inSf2, subChunkName) + { + Size = SF2VersionTag.Size; + inSf2.UpdateSize(); + } + internal VersionSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + Version = new SF2VersionTag(reader); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + Version.Write(writer); + } + + public override string ToString() + { + return $"Version Chunk - Revision = {Version}"; + } + } + + public sealed class HeaderSubChunk : SF2Chunk + { + public int MaxSize { get; } + private int _fieldTargetLength; + private string _field; + /// Length + public string Field + { + get => _field; + set + { + if (value.Length >= MaxSize) // Input too long; cut it down + { + _fieldTargetLength = MaxSize; + } + else if (value.Length % 2 == 0) // Even amount of characters + { + _fieldTargetLength = value.Length + 2; // Add two null-terminators to keep the byte count even + } + else // Odd amount of characters + { + _fieldTargetLength = value.Length + 1; // Add one null-terminator since that would make byte the count even + } + _field = value; + Size = (uint)_fieldTargetLength; + _sf2.UpdateSize(); + } + } + + internal HeaderSubChunk(SF2 inSf2, string subChunkName, int maxSize = 0x100) : base(inSf2, subChunkName) + { + MaxSize = maxSize; + } + internal HeaderSubChunk(SF2 inSf2, EndianBinaryReader reader, int maxSize = 0x100) : base(inSf2, reader) + { + MaxSize = maxSize; + _field = reader.ReadString((int)Size, true); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(_field, _fieldTargetLength); + } + + public override string ToString() + { + return $"Header Chunk - Name = \"{ChunkName}\"" + + $",\nField Max Size = {MaxSize}" + + $",\nField = \"{Field}\""; + } + } + + public sealed class SMPLSubChunk : SF2Chunk + { + private readonly List _samples = new List(); // Block of sample data + + internal SMPLSubChunk(SF2 inSf2) : base(inSf2, "smpl") { } + internal SMPLSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / sizeof(short); i++) + { + _samples.Add(reader.ReadInt16()); + } + } + + // Returns index of the start of the sample + internal uint AddSample(short[] pcm16, bool bLoop, uint loopPos) + { + uint start = (uint)_samples.Count; + + // Write wave + _samples.AddRange(pcm16); + + // If looping is enabled, write 8 samples from the loop point + if (bLoop) + { + // In case (loopPos + i) is greater than the sample length + uint max = (uint)pcm16.Length - loopPos; + for (uint i = 0; i < 8; i++) + { + _samples.Add(pcm16[loopPos + (i % max)]); + } + } + + // Write 46 empty samples + _samples.AddRange(new short[46]); + + Size = (uint)_samples.Count * sizeof(short); + _sf2.UpdateSize(); + return start; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + foreach (short s in _samples) + { + writer.Write(s); + } + } + + public override string ToString() + { + return $"Sample Data Chunk"; + } + } + + public sealed class PHDRSubChunk : SF2Chunk + { + private readonly List _presets = new List(); + public uint Count => (uint)_presets.Count; + + internal PHDRSubChunk(SF2 inSf2) : base(inSf2, "phdr") { } + internal PHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2PresetHeader.Size; i++) + { + _presets.Add(new SF2PresetHeader(reader)); + } + } + + internal void AddPreset(SF2PresetHeader preset) + { + _presets.Add(preset); + Size = Count * SF2PresetHeader.Size; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _presets[i].Write(writer); + } + } + + public override string ToString() + { + return $"Preset Header Chunk - Preset count = {Count}"; + } + } + + public sealed class INSTSubChunk : SF2Chunk + { + private readonly List _instruments = new List(); + public uint Count => (uint)_instruments.Count; + + internal INSTSubChunk(SF2 inSf2) : base(inSf2, "inst") { } + internal INSTSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2Instrument.Size; i++) + { + _instruments.Add(new SF2Instrument(reader)); + } + } + + internal uint AddInstrument(SF2Instrument instrument) + { + _instruments.Add(instrument); + Size = Count * SF2Instrument.Size; + _sf2.UpdateSize(); + return Count - 1; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _instruments[i].Write(writer); + } + } + + public override string ToString() + { + return $"Instrument Chunk - Instrument count = {Count}"; + } + } + + public sealed class BAGSubChunk : SF2Chunk + { + private readonly List _bags = new List(); + public uint Count => (uint)_bags.Count; + + internal BAGSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pbag" : "ibag") { } + internal BAGSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2Bag.Size; i++) + { + _bags.Add(new SF2Bag(reader)); + } + } + + internal void AddBag(SF2Bag bag) + { + _bags.Add(bag); + Size = Count * SF2Bag.Size; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _bags[i].Write(writer); + } + } + + public override string ToString() + { + return $"Bag Chunk - Name = \"{ChunkName}\"" + + $",\nBag count = {Count}"; + } + } + + public sealed class MODSubChunk : SF2Chunk + { + private readonly List _modulators = new List(); + public uint Count => (uint)_modulators.Count; + + internal MODSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pmod" : "imod") { } + internal MODSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2ModulatorList.Size; i++) + { + _modulators.Add(new SF2ModulatorList(reader)); + } + } + + internal void AddModulator(SF2ModulatorList modulator) + { + _modulators.Add(modulator); + Size = Count * SF2ModulatorList.Size; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _modulators[i].Write(writer); + } + } + + public override string ToString() + { + return $"Modulator Chunk - Name = \"{ChunkName}\"" + + $",\nModulator count = {Count}"; + } + } + + public sealed class GENSubChunk : SF2Chunk + { + private readonly List _generators = new List(); + public uint Count => (uint)_generators.Count; + + internal GENSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pgen" : "igen") { } + internal GENSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2GeneratorList.Size; i++) + { + _generators.Add(new SF2GeneratorList(reader)); + } + } + + internal void AddGenerator(SF2GeneratorList generator) + { + _generators.Add(generator); + Size = Count * SF2GeneratorList.Size; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _generators[i].Write(writer); + } + } + + public override string ToString() + { + return $"Generator Chunk - Name = \"{ChunkName}\"" + + $",\nGenerator count = {Count}"; + } + } + + public sealed class SHDRSubChunk : SF2Chunk + { + private readonly List _samples = new List(); + public uint Count => (uint)_samples.Count; + + internal SHDRSubChunk(SF2 inSf2) : base(inSf2, "shdr") { } + internal SHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2SampleHeader.Size; i++) + { + _samples.Add(new SF2SampleHeader(reader)); + } + } + + internal uint AddSample(SF2SampleHeader sample) + { + _samples.Add(sample); + Size = Count * SF2SampleHeader.Size; + _sf2.UpdateSize(); + return Count - 1; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _samples[i].Write(writer); + } + } + + public override string ToString() + { + return $"Sample Header Chunk - Sample header count = {Count}"; + } + } + + #endregion + + #region Main Chunks + + public sealed class InfoListChunk : SF2ListChunk + { + private readonly List _subChunks = new List(); + private const string DefaultEngine = "EMU8000"; + public string Engine + { + get + { + if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "isng") { Field = DefaultEngine }); + return DefaultEngine; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "isng") { Field = value }); + } + } + } + + private const string DefaultBank = "General MIDI"; + public string Bank + { + get + { + if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "INAM") { Field = DefaultBank }); + return DefaultBank; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "INAM") { Field = value }); + } + } + } + public string ROM + { + get + { + if (_subChunks.Find(s => s.ChunkName == "irom") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "irom") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "irom") { Field = value }); + } + } + } + public SF2VersionTag ROMVersion + { + get + { + if (_subChunks.Find(s => s.ChunkName == "iver") is VersionSubChunk chunk) + { + return chunk.Version; + } + else + { + return null; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "iver") is VersionSubChunk chunk) + { + chunk.Version = value; + } + else + { + _subChunks.Add(new VersionSubChunk(_sf2, "iver") { Version = value }); + } + } + } + public string Date + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ICRD") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "ICRD") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ICRD") { Field = value }); + } + } + } + public string Designer + { + get + { + if (_subChunks.Find(s => s.ChunkName == "IENG") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "IENG") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "IENG") { Field = value }); + } + } + } + public string Products + { + get + { + if (_subChunks.Find(s => s.ChunkName == "IPRD") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "IPRD") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "IPRD") { Field = value }); + } + } + } + public string Copyright + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ICOP") is HeaderSubChunk icop) + { + return icop.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "ICOP") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ICOP") { Field = value }); + } + } + } + + private const int CommentMaxSize = 0x10000; + public string Comment + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ICMT") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "ICMT") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ICMT", maxSize: CommentMaxSize) { Field = value }); + } + } + } + public string Tools + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ISFT") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "ISFT") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ISFT") { Field = value }); + } + } + } + + internal InfoListChunk(SF2 inSf2) : base(inSf2, "INFO") + { + // Mandatory sub-chunks + _subChunks.Add(new VersionSubChunk(inSf2, "ifil") { Version = new SF2VersionTag(2, 1) }); + _subChunks.Add(new HeaderSubChunk(inSf2, "isng") { Field = DefaultEngine }); + _subChunks.Add(new HeaderSubChunk(inSf2, "INAM") { Field = DefaultBank }); + inSf2.UpdateSize(); + } + internal InfoListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + long startOffset = reader.BaseStream.Position; + while (reader.BaseStream.Position < startOffset + Size - 4) // The 4 represents the INFO that was already read + { + // Peek 4 chars for the chunk name + string name = reader.ReadString(4, false); + reader.BaseStream.Position -= 4; + switch (name) + { + case "ICMT": _subChunks.Add(new HeaderSubChunk(inSf2, reader, maxSize: CommentMaxSize)); break; + case "ifil": + case "iver": _subChunks.Add(new VersionSubChunk(inSf2, reader)); break; + case "isng": + case "INAM": + case "ICRD": + case "IENG": + case "IPRD": + case "ICOP": + case "ISFT": + case "irom": _subChunks.Add(new HeaderSubChunk(inSf2, reader)); break; + default: throw new NotSupportedException($"Unsupported chunk name at 0x{reader.BaseStream.Position:X}: \"{name}\""); + } + } + } + + internal override uint UpdateSize() + { + Size = 4; + foreach (SF2Chunk sub in _subChunks) + { + Size += sub.Size + 8; + } + + return Size; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + foreach (SF2Chunk sub in _subChunks) + { + sub.Write(writer); + } + } + + public override string ToString() + { + return $"Info List Chunk - Sub-chunk count = {_subChunks.Count}"; + } + } + + public sealed class SdtaListChunk : SF2ListChunk + { + public SMPLSubChunk SMPLSubChunk { get; } + + internal SdtaListChunk(SF2 inSf2) : base(inSf2, "sdta") + { + SMPLSubChunk = new SMPLSubChunk(inSf2); + inSf2.UpdateSize(); + } + internal SdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + SMPLSubChunk = new SMPLSubChunk(inSf2, reader); + } + + internal override uint UpdateSize() + { + return Size = 4 + + SMPLSubChunk.Size + 8; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + SMPLSubChunk.Write(writer); + } + + public override string ToString() + { + return $"Sample Data List Chunk"; + } + } + + public sealed class PdtaListChunk : SF2ListChunk + { + public PHDRSubChunk PHDRSubChunk { get; } + public BAGSubChunk PBAGSubChunk { get; } + public MODSubChunk PMODSubChunk { get; } + public GENSubChunk PGENSubChunk { get; } + public INSTSubChunk INSTSubChunk { get; } + public BAGSubChunk IBAGSubChunk { get; } + public MODSubChunk IMODSubChunk { get; } + public GENSubChunk IGENSubChunk { get; } + public SHDRSubChunk SHDRSubChunk { get; } + + internal PdtaListChunk(SF2 inSf2) : base(inSf2, "pdta") + { + PHDRSubChunk = new PHDRSubChunk(inSf2); + PBAGSubChunk = new BAGSubChunk(inSf2, true); + PMODSubChunk = new MODSubChunk(inSf2, true); + PGENSubChunk = new GENSubChunk(inSf2, true); + INSTSubChunk = new INSTSubChunk(inSf2); + IBAGSubChunk = new BAGSubChunk(inSf2, false); + IMODSubChunk = new MODSubChunk(inSf2, false); + IGENSubChunk = new GENSubChunk(inSf2, false); + SHDRSubChunk = new SHDRSubChunk(inSf2); + inSf2.UpdateSize(); + } + internal PdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + PHDRSubChunk = new PHDRSubChunk(inSf2, reader); + PBAGSubChunk = new BAGSubChunk(inSf2, reader); + PMODSubChunk = new MODSubChunk(inSf2, reader); + PGENSubChunk = new GENSubChunk(inSf2, reader); + INSTSubChunk = new INSTSubChunk(inSf2, reader); + IBAGSubChunk = new BAGSubChunk(inSf2, reader); + IMODSubChunk = new MODSubChunk(inSf2, reader); + IGENSubChunk = new GENSubChunk(inSf2, reader); + SHDRSubChunk = new SHDRSubChunk(inSf2, reader); + } + + internal override uint UpdateSize() + { + return Size = 4 + + PHDRSubChunk.Size + 8 + + PBAGSubChunk.Size + 8 + + PMODSubChunk.Size + 8 + + PGENSubChunk.Size + 8 + + INSTSubChunk.Size + 8 + + IBAGSubChunk.Size + 8 + + IMODSubChunk.Size + 8 + + IGENSubChunk.Size + 8 + + SHDRSubChunk.Size + 8; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + PHDRSubChunk.Write(writer); + PBAGSubChunk.Write(writer); + PMODSubChunk.Write(writer); + PGENSubChunk.Write(writer); + INSTSubChunk.Write(writer); + IBAGSubChunk.Write(writer); + IMODSubChunk.Write(writer); + IGENSubChunk.Write(writer); + SHDRSubChunk.Write(writer); + } + + public override string ToString() + { + return $"Hydra List Chunk"; + } + } + + #endregion +} diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Types.cs b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Types.cs new file mode 100644 index 00000000..b297d802 --- /dev/null +++ b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Types.cs @@ -0,0 +1,142 @@ +using Kermalis.EndianBinaryIO; +using System.Runtime.InteropServices; + +namespace Kermalis.SoundFont2 +{ + /// SF2 v2.1 spec page 16 + public sealed class SF2VersionTag + { + public const uint Size = 4; + + public ushort Major { get; } + public ushort Minor { get; } + + public SF2VersionTag(ushort major, ushort minor) + { + Major = major; + Minor = minor; + } + internal SF2VersionTag(EndianBinaryReader reader) + { + Major = reader.ReadUInt16(); + Minor = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Major); + writer.Write(Minor); + } + + public override string ToString() + { + return $"v{Major}.{Minor}"; + } + } + + /// SF2 spec v2.1 page 19 - Two bytes that can handle either two 8-bit values or a single 16-bit value + [StructLayout(LayoutKind.Explicit)] + public struct SF2GeneratorAmount + { + [FieldOffset(0)] public byte LowByte; + [FieldOffset(1)] public byte HighByte; + [FieldOffset(0)] public short Amount; + [FieldOffset(0)] public ushort UAmount; + + public override string ToString() + { + return $"BLo = {LowByte}, BHi = {HighByte}, Sh = {Amount}, U = {UAmount}"; + } + } + + /// SF2 v2.1 spec page 20 + public enum SF2SampleLink : ushort + { + MonoSample = 1, + RightSample = 2, + LeftSample = 4, + LinkedSample = 8, + RomMonoSample = 0x8001, + RomRightSample = 0x8002, + RomLeftSample = 0x8004, + RomLinkedSample = 0x8008 + } + + /// SF2 v2.1 spec page 38 + public enum SF2Generator : ushort + { + StartAddrsOffset = 0, + EndAddrsOffset = 1, + StartloopAddrsOffset = 2, + EndloopAddrsOffset = 3, + StartAddrsCoarseOffset = 4, + ModLfoToPitch = 5, + VibLfoToPitch = 6, + ModEnvToPitch = 7, + InitialFilterFc = 8, + InitialFilterQ = 9, + ModLfoToFilterFc = 10, + ModEnvToFilterFc = 11, + EndAddrsCoarseOffset = 12, + ModLfoToVolume = 13, + ChorusEffectsSend = 15, + ReverbEffectsSend = 16, + Pan = 17, + DelayModLFO = 21, + FreqModLFO = 22, + DelayVibLFO = 23, + FreqVibLFO = 24, + DelayModEnv = 25, + AttackModEnv = 26, + HoldModEnv = 27, + DecayModEnv = 28, + SustainModEnv = 29, + ReleaseModEnv = 30, + KeynumToModEnvHold = 31, + KeynumToModEnvDecay = 32, + DelayVolEnv = 33, + AttackVolEnv = 34, + HoldVolEnv = 35, + DecayVolEnv = 36, + SustainVolEnv = 37, + ReleaseVolEnv = 38, + KeynumToVolEnvHold = 39, + KeynumToVolEnvDecay = 40, + Instrument = 41, + KeyRange = 43, + VelRange = 44, + StartloopAddrsCoarseOffset = 45, + Keynum = 46, + Velocity = 47, + InitialAttenuation = 48, + EndloopAddrsCoarseOffset = 50, + CoarseTune = 51, + FineTune = 52, + SampleID = 53, + SampleModes = 54, + ScaleTuning = 56, + ExclusiveClass = 57, + OverridingRootKey = 58, + EndOper = 60 + } + + /// SF2 v2.1 spec page 50 + public enum SF2Modulator : ushort + { + None = 0, + NoteOnVelocity = 1, + NoteOnKey = 2, + PolyPressure = 10, + ChnPressure = 13, + PitchWheel = 14, + PitchWheelSensivity = 16 + } + + /// SF2 v2.1 spec page 52 + public enum SF2Transform : ushort + { + Linear = 0, + Concave = 1, + Convex = 2 + } +} diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SoundFont2.csproj b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SoundFont2.csproj new file mode 100644 index 00000000..3ed42002 --- /dev/null +++ b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SoundFont2.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.0 + Release + Kermalis + + SoundFont2 + SoundFont2 + SoundFont2 + Kermalis.SoundFont2 + 1.0.0.0 + + + + ..\Build + Auto + none + false + + + + + + + diff --git a/Legacy Repo (.NET Framework)/VGMusicStudio b/Legacy Repo (.NET Framework)/VGMusicStudio new file mode 160000 index 00000000..3a628c5c --- /dev/null +++ b/Legacy Repo (.NET Framework)/VGMusicStudio @@ -0,0 +1 @@ +Subproject commit 3a628c5c4c62f0e159a3e1161705dbb482618cca diff --git a/MigrationBackup/655cc60b/VG Music Studio/NuGetUpgradeLog.html b/MigrationBackup/655cc60b/VG Music Studio/NuGetUpgradeLog.html new file mode 100644 index 00000000..7ca2a181 --- /dev/null +++ b/MigrationBackup/655cc60b/VG Music Studio/NuGetUpgradeLog.html @@ -0,0 +1,223 @@ + + + + + NuGetMigrationLog +

+ NuGet Migration Report - VG Music Studio

Overview

Migration to PackageReference was completed successfully. Please build and run your solution to verify that all packages are available.
+ If you run into any problems, have feedback, questions, or concerns, please + file an issue on the NuGet GitHub repository.
+ Changed files and this report have been backed up here: + C:\Users\Silve\Source\Repos\VGMusicStudio\MigrationBackup\655cc60b\VG Music Studio

Packages processed

Top-level dependencies:

Package IdVersion
EndianBinaryIO + v1.1.2
Microsoft.Win32.Registry + v5.0.0
Microsoft.Win32.Registry.AccessControl + v5.0.0
Microsoft.Win32.SystemEvents + v5.0.0
Microsoft.Windows.Compatibility + v5.0.2
Microsoft.WindowsAPICodePack-Core + v1.1.0.2
Microsoft.WindowsAPICodePack-Shell + v1.1.0
NAudio + v2.0.1
NAudio.Asio + v2.0.0
NAudio.Core + v2.0.0
NAudio.Midi + v2.0.1
NAudio.Wasapi + v2.0.0
NAudio.WinForms + v2.0.1
NAudio.WinMM + v2.0.1
ObjectListView.Official + v2.9.1
System.CodeDom + v5.0.0
System.ComponentModel.Composition + v5.0.0
System.Configuration.ConfigurationManager + v5.0.0
System.Data.DataSetExtensions + v4.5.0
System.Data.Odbc + v5.0.0
System.Data.OleDb + v5.0.0
System.Data.SqlClient + v4.8.1
System.Diagnostics.EventLog + v5.0.1
System.Diagnostics.PerformanceCounter + v5.0.1
System.DirectoryServices + v5.0.0
System.DirectoryServices.AccountManagement + v5.0.0
System.DirectoryServices.Protocols + v5.0.0
System.Drawing.Common + v5.0.0
System.IO + v4.3.0
System.IO.FileSystem.AccessControl + v5.0.0
System.IO.FileSystem.Primitives + v4.3.0
System.IO.Packaging + v5.0.0
System.IO.Pipes.AccessControl + v5.0.0
System.IO.Ports + v5.0.0
System.Management + v5.0.0
System.Reflection.Emit + v4.7.0
System.Reflection.Emit.ILGeneration + v4.7.0
System.Reflection.Emit.Lightweight + v4.7.0
System.Runtime + v4.3.0
System.Runtime.Caching + v5.0.0
System.Runtime.CompilerServices.Unsafe + v5.0.0
System.Security.AccessControl + v5.0.0
System.Security.Cryptography.Algorithms + v4.3.1
System.Security.Cryptography.Cng + v5.0.0
System.Security.Cryptography.Encoding + v4.3.0
System.Security.Cryptography.Pkcs + v5.0.1
System.Security.Cryptography.Primitives + v4.3.0
System.Security.Cryptography.ProtectedData + v5.0.0
System.Security.Cryptography.Xml + v5.0.0
System.Security.Permissions + v5.0.0
System.Security.Principal.Windows + v5.0.0
System.ServiceModel.Duplex + v4.8.0
System.ServiceModel.Http + v4.8.0
System.ServiceModel.NetTcp + v4.8.0
System.ServiceModel.Primitives + v4.8.0
System.ServiceModel.Security + v4.8.0
System.ServiceModel.Syndication + v5.0.0
System.ServiceProcess.ServiceController + v5.0.0
System.Speech + v5.0.0
System.Text.Encoding.CodePages + v5.0.0
System.Threading.AccessControl + v5.0.0
YamlDotNet + v11.2.1

Transitive dependencies:

Package IdVersion
+ No transitive dependencies found. +

Package compatibility issues

Description
+ No issues were found. +
\ No newline at end of file diff --git a/MigrationBackup/655cc60b/VG Music Studio/VG Music Studio.csproj b/MigrationBackup/655cc60b/VG Music Studio/VG Music Studio.csproj new file mode 100644 index 00000000..1b4c548a --- /dev/null +++ b/MigrationBackup/655cc60b/VG Music Studio/VG Music Studio.csproj @@ -0,0 +1,411 @@ + + + + + Debug + AnyCPU + {97C8ACF8-66A3-4321-91D6-3E94EACA577F} + WinExe + Kermalis.VGMusicStudio + VG Music Studio + v4.8 + 512 + true + + false + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + true + + + AnyCPU + true + full + false + ..\Build\ + DEBUG;TRACE + prompt + 4 + Off + false + + + AnyCPU + pdbonly + true + ..\Build\ + TRACE + prompt + 4 + false + On + + + Properties\Icon.ico + + + Kermalis.VGMusicStudio.Program + + + + Dependencies\DLS2.dll + + + ..\packages\EndianBinaryIO.1.1.2\lib\netstandard2.0\EndianBinaryIO.dll + + + ..\packages\Microsoft.Win32.Registry.5.0.0\lib\net461\Microsoft.Win32.Registry.dll + + + ..\packages\Microsoft.Win32.Registry.AccessControl.5.0.0\lib\net461\Microsoft.Win32.Registry.AccessControl.dll + + + ..\packages\Microsoft.Win32.SystemEvents.5.0.0\lib\net461\Microsoft.Win32.SystemEvents.dll + + + ..\packages\Microsoft.WindowsAPICodePack-Core.1.1.0.2\lib\Microsoft.WindowsAPICodePack.dll + + + ..\packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.Shell.dll + + + ..\packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.ShellExtensions.dll + + + ..\packages\NAudio.2.0.1\lib\netstandard2.0\NAudio.dll + + + ..\packages\NAudio.Asio.2.0.0\lib\netstandard2.0\NAudio.Asio.dll + + + ..\packages\NAudio.Core.2.0.0\lib\netstandard2.0\NAudio.Core.dll + + + ..\packages\NAudio.Midi.2.0.1\lib\netstandard2.0\NAudio.Midi.dll + + + ..\packages\NAudio.Wasapi.2.0.0\lib\netstandard2.0\NAudio.Wasapi.dll + + + ..\packages\NAudio.WinForms.2.0.1\lib\net472\NAudio.WinForms.dll + + + ..\packages\NAudio.WinMM.2.0.1\lib\netstandard2.0\NAudio.WinMM.dll + + + ..\packages\ObjectListView.Official.2.9.1\lib\net20\ObjectListView.dll + + + + + False + Dependencies\Sanford.Multimedia.Midi.dll + + + False + Dependencies\SoundFont2.dll + + + + ..\packages\System.CodeDom.5.0.0\lib\net461\System.CodeDom.dll + + + + + ..\packages\System.Configuration.ConfigurationManager.5.0.0\lib\net461\System.Configuration.ConfigurationManager.dll + + + + ..\packages\System.Data.Odbc.5.0.0\lib\net461\System.Data.Odbc.dll + + + ..\packages\System.Data.OleDb.5.0.0\lib\net461\System.Data.OleDb.dll + + + + ..\packages\System.Data.SqlClient.4.8.1\lib\net461\System.Data.SqlClient.dll + + + ..\packages\System.Diagnostics.EventLog.5.0.1\lib\net461\System.Diagnostics.EventLog.dll + + + ..\packages\System.Diagnostics.PerformanceCounter.5.0.1\lib\net461\System.Diagnostics.PerformanceCounter.dll + + + + + + ..\packages\System.Drawing.Common.5.0.0\lib\net461\System.Drawing.Common.dll + + + + ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll + True + True + + + ..\packages\System.IO.FileSystem.AccessControl.5.0.0\lib\net461\System.IO.FileSystem.AccessControl.dll + + + ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + True + True + + + ..\packages\System.IO.Packaging.5.0.0\lib\net46\System.IO.Packaging.dll + + + ..\packages\System.IO.Pipes.AccessControl.5.0.0\lib\net461\System.IO.Pipes.AccessControl.dll + + + ..\packages\System.IO.Ports.5.0.0\lib\net461\System.IO.Ports.dll + + + + + ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll + True + True + + + + ..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll + + + + + ..\packages\System.Security.AccessControl.5.0.0\lib\net461\System.Security.AccessControl.dll + + + ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll + True + True + + + ..\packages\System.Security.Cryptography.Cng.5.0.0\lib\net47\System.Security.Cryptography.Cng.dll + + + ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + True + True + + + ..\packages\System.Security.Cryptography.Pkcs.5.0.1\lib\net461\System.Security.Cryptography.Pkcs.dll + + + ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + True + True + + + ..\packages\System.Security.Cryptography.ProtectedData.5.0.0\lib\net461\System.Security.Cryptography.ProtectedData.dll + + + ..\packages\System.Security.Cryptography.Xml.5.0.0\lib\net461\System.Security.Cryptography.Xml.dll + + + ..\packages\System.Security.Permissions.5.0.0\lib\net461\System.Security.Permissions.dll + + + ..\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll + + + + ..\packages\System.ServiceModel.Duplex.4.8.0\lib\net461\System.ServiceModel.Duplex.dll + + + ..\packages\System.ServiceModel.Http.4.8.0\lib\net461\System.ServiceModel.Http.dll + + + ..\packages\System.ServiceModel.NetTcp.4.8.0\lib\net461\System.ServiceModel.NetTcp.dll + + + ..\packages\System.ServiceModel.Primitives.4.8.0\lib\net461\System.ServiceModel.Primitives.dll + + + ..\packages\System.ServiceModel.Security.4.8.0\lib\net461\System.ServiceModel.Security.dll + + + ..\packages\System.ServiceModel.Syndication.5.0.0\lib\net461\System.ServiceModel.Syndication.dll + + + + ..\packages\System.ServiceProcess.ServiceController.5.0.0\lib\net461\System.ServiceProcess.ServiceController.dll + + + + ..\packages\System.Text.Encoding.CodePages.5.0.0\lib\net461\System.Text.Encoding.CodePages.dll + + + ..\packages\System.Threading.AccessControl.5.0.0\lib\net461\System.Threading.AccessControl.dll + + + + + + + + + + + + + ..\packages\YamlDotNet.11.2.1\lib\net45\YamlDotNet.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Strings.resx + + + + + Component + + + + + + Component + + + + + + + + + + + + Always + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + True + + + Always + + + Always + + + Always + + + Always + + + + + ResXFileCodeGenerator + Strings.Designer.cs + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + + + False + Microsoft .NET Framework 4.7.1 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + + + \ No newline at end of file diff --git a/MigrationBackup/655cc60b/VG Music Studio/packages.config b/MigrationBackup/655cc60b/VG Music Studio/packages.config new file mode 100644 index 00000000..9786087a --- /dev/null +++ b/MigrationBackup/655cc60b/VG Music Studio/packages.config @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ObjectListView/CellEditing/CellEditKeyEngine.cs b/ObjectListView/CellEditing/CellEditKeyEngine.cs new file mode 100644 index 00000000..a0d67b60 --- /dev/null +++ b/ObjectListView/CellEditing/CellEditKeyEngine.cs @@ -0,0 +1,520 @@ +/* + * CellEditKeyEngine - A engine that allows the behaviour of arbitrary keys to be configured + * + * Author: Phillip Piper + * Date: 3-March-2011 10:53 pm + * + * Change log: + * v2.8 + * 2014-05-30 JPP - When a row is disabled, skip over it when looking for another cell to edit + * v2.5 + * 2012-04-14 JPP - Fixed bug where, on a OLV with only a single editable column, tabbing + * to change rows would edit the cell above rather than the cell below + * the cell being edited. + * 2.5 + * 2011-03-03 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using BrightIdeasSoftware; + +namespace BrightIdeasSoftware { + /// + /// Indicates the behavior of a key when a cell "on the edge" is being edited. + /// and the normal behavior of that key would exceed the edge. For example, + /// for a key that normally moves one column to the left, the "edge" would be + /// the left most column, since the normal action of the key cannot be taken + /// (since there are no more columns to the left). + /// + public enum CellEditAtEdgeBehaviour { + /// + /// The key press will be ignored + /// + Ignore, + + /// + /// The key press will result in the cell editing wrapping to the + /// cell on the opposite edge. + /// + Wrap, + + /// + /// The key press will wrap, but the column will be changed to the + /// appropriate adjacent column. This only makes sense for keys where + /// the normal action is ChangeRow. + /// + ChangeColumn, + + /// + /// The key press will wrap, but the row will be changed to the + /// appropriate adjacent row. This only makes sense for keys where + /// the normal action is ChangeColumn. + /// + ChangeRow, + + /// + /// The key will result in the current edit operation being ended. + /// + EndEdit + }; + + /// + /// Indicates the normal behaviour of a key when used during a cell edit + /// operation. + /// + public enum CellEditCharacterBehaviour { + /// + /// The key press will be ignored + /// + Ignore, + + /// + /// The key press will end the current edit and begin an edit + /// operation on the next editable cell to the left. + /// + ChangeColumnLeft, + + /// + /// The key press will end the current edit and begin an edit + /// operation on the next editable cell to the right. + /// + ChangeColumnRight, + + /// + /// The key press will end the current edit and begin an edit + /// operation on the row above. + /// + ChangeRowUp, + + /// + /// The key press will end the current edit and begin an edit + /// operation on the row below + /// + ChangeRowDown, + + /// + /// The key press will cancel the current edit + /// + CancelEdit, + + /// + /// The key press will finish the current edit operation + /// + EndEdit, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb1, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb2, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb3, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb4, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb5, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb6, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb7, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb8, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb9, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb10, + }; + + /// + /// Instances of this class handle key presses during a cell edit operation. + /// + public class CellEditKeyEngine { + + #region Public interface + + /// + /// Sets the behaviour of a given key + /// + /// + /// + /// + public virtual void SetKeyBehaviour(Keys key, CellEditCharacterBehaviour normalBehaviour, CellEditAtEdgeBehaviour atEdgeBehaviour) { + this.CellEditKeyMap[key] = normalBehaviour; + this.CellEditKeyAtEdgeBehaviourMap[key] = atEdgeBehaviour; + } + + /// + /// Handle a key press + /// + /// + /// + /// True if the key was completely handled. + public virtual bool HandleKey(ObjectListView olv, Keys keyData) { + if (olv == null) throw new ArgumentNullException("olv"); + + CellEditCharacterBehaviour behaviour; + if (!CellEditKeyMap.TryGetValue(keyData, out behaviour)) + return false; + + this.ListView = olv; + + switch (behaviour) { + case CellEditCharacterBehaviour.Ignore: + break; + case CellEditCharacterBehaviour.CancelEdit: + this.HandleCancelEdit(); + break; + case CellEditCharacterBehaviour.EndEdit: + this.HandleEndEdit(); + break; + case CellEditCharacterBehaviour.ChangeColumnLeft: + case CellEditCharacterBehaviour.ChangeColumnRight: + this.HandleColumnChange(keyData, behaviour); + break; + case CellEditCharacterBehaviour.ChangeRowDown: + case CellEditCharacterBehaviour.ChangeRowUp: + this.HandleRowChange(keyData, behaviour); + break; + default: + return this.HandleCustomVerb(keyData, behaviour); + }; + + return true; + } + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the ObjectListView on which the current key is being handled. + /// This cannot be null. + /// + protected ObjectListView ListView { + get { return listView; } + set { listView = value; } + } + private ObjectListView listView; + + /// + /// Gets the row of the cell that is currently being edited + /// + protected OLVListItem ItemBeingEdited { + get { + return (this.ListView == null || this.ListView.CellEditEventArgs == null) ? null : this.ListView.CellEditEventArgs.ListViewItem; + } + } + + /// + /// Gets the index of the column of the cell that is being edited + /// + protected int SubItemIndexBeingEdited { + get { + return (this.ListView == null || this.ListView.CellEditEventArgs == null) ? -1 : this.ListView.CellEditEventArgs.SubItemIndex; + } + } + + /// + /// Gets or sets the map that remembers the normal behaviour of keys + /// + protected IDictionary CellEditKeyMap { + get { + if (cellEditKeyMap == null) + this.InitializeCellEditKeyMaps(); + return cellEditKeyMap; + } + set { + cellEditKeyMap = value; + } + } + private IDictionary cellEditKeyMap; + + /// + /// Gets or sets the map that remembers the desired behaviour of keys + /// on edge cases. + /// + protected IDictionary CellEditKeyAtEdgeBehaviourMap { + get { + if (cellEditKeyAtEdgeBehaviourMap == null) + this.InitializeCellEditKeyMaps(); + return cellEditKeyAtEdgeBehaviourMap; + } + set { + cellEditKeyAtEdgeBehaviourMap = value; + } + } + private IDictionary cellEditKeyAtEdgeBehaviourMap; + + #endregion + + #region Initialization + + /// + /// Setup the default key mapping + /// + protected virtual void InitializeCellEditKeyMaps() { + this.cellEditKeyMap = new Dictionary(); + this.cellEditKeyMap[Keys.Escape] = CellEditCharacterBehaviour.CancelEdit; + this.cellEditKeyMap[Keys.Return] = CellEditCharacterBehaviour.EndEdit; + this.cellEditKeyMap[Keys.Enter] = CellEditCharacterBehaviour.EndEdit; + this.cellEditKeyMap[Keys.Tab] = CellEditCharacterBehaviour.ChangeColumnRight; + this.cellEditKeyMap[Keys.Tab | Keys.Shift] = CellEditCharacterBehaviour.ChangeColumnLeft; + this.cellEditKeyMap[Keys.Left | Keys.Alt] = CellEditCharacterBehaviour.ChangeColumnLeft; + this.cellEditKeyMap[Keys.Right | Keys.Alt] = CellEditCharacterBehaviour.ChangeColumnRight; + this.cellEditKeyMap[Keys.Up | Keys.Alt] = CellEditCharacterBehaviour.ChangeRowUp; + this.cellEditKeyMap[Keys.Down | Keys.Alt] = CellEditCharacterBehaviour.ChangeRowDown; + + this.cellEditKeyAtEdgeBehaviourMap = new Dictionary(); + this.cellEditKeyAtEdgeBehaviourMap[Keys.Tab] = CellEditAtEdgeBehaviour.Wrap; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Tab | Keys.Shift] = CellEditAtEdgeBehaviour.Wrap; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Left | Keys.Alt] = CellEditAtEdgeBehaviour.Wrap; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Right | Keys.Alt] = CellEditAtEdgeBehaviour.Wrap; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Up | Keys.Alt] = CellEditAtEdgeBehaviour.ChangeColumn; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Down | Keys.Alt] = CellEditAtEdgeBehaviour.ChangeColumn; + } + + #endregion + + #region Command handling + + /// + /// Handle the end edit command + /// + protected virtual void HandleEndEdit() { + this.ListView.PossibleFinishCellEditing(); + } + + /// + /// Handle the cancel edit command + /// + protected virtual void HandleCancelEdit() { + this.ListView.CancelCellEdit(); + } + + /// + /// Placeholder that subclasses can override to handle any custom verbs + /// + /// + /// + /// + protected virtual bool HandleCustomVerb(Keys keyData, CellEditCharacterBehaviour behaviour) { + return false; + } + + /// + /// Handle a change row command + /// + /// + /// + protected virtual void HandleRowChange(Keys keyData, CellEditCharacterBehaviour behaviour) { + // If we couldn't finish editing the current cell, don't try to move it + if (!this.ListView.PossibleFinishCellEditing()) + return; + + OLVListItem olvi = this.ItemBeingEdited; + int subItemIndex = this.SubItemIndexBeingEdited; + bool isGoingUp = behaviour == CellEditCharacterBehaviour.ChangeRowUp; + + // Try to find a row above (or below) the currently edited cell + // If we find one, start editing it and we're done. + OLVListItem adjacentOlvi = this.GetAdjacentItemOrNull(olvi, isGoingUp); + if (adjacentOlvi != null) { + this.StartCellEditIfDifferent(adjacentOlvi, subItemIndex); + return; + } + + // There is no adjacent row in the direction we want, so we must be on an edge. + CellEditAtEdgeBehaviour atEdgeBehaviour; + if (!this.CellEditKeyAtEdgeBehaviourMap.TryGetValue(keyData, out atEdgeBehaviour)) + atEdgeBehaviour = CellEditAtEdgeBehaviour.Wrap; + switch (atEdgeBehaviour) { + case CellEditAtEdgeBehaviour.Ignore: + break; + case CellEditAtEdgeBehaviour.EndEdit: + this.ListView.PossibleFinishCellEditing(); + break; + case CellEditAtEdgeBehaviour.Wrap: + adjacentOlvi = this.GetAdjacentItemOrNull(null, isGoingUp); + this.StartCellEditIfDifferent(adjacentOlvi, subItemIndex); + break; + case CellEditAtEdgeBehaviour.ChangeColumn: + // Figure out the next editable column + List editableColumnsInDisplayOrder = this.EditableColumnsInDisplayOrder; + int displayIndex = Math.Max(0, editableColumnsInDisplayOrder.IndexOf(this.ListView.GetColumn(subItemIndex))); + if (isGoingUp) + displayIndex = (editableColumnsInDisplayOrder.Count + displayIndex - 1) % editableColumnsInDisplayOrder.Count; + else + displayIndex = (displayIndex + 1) % editableColumnsInDisplayOrder.Count; + subItemIndex = editableColumnsInDisplayOrder[displayIndex].Index; + + // Wrap to the next row and start the cell edit + adjacentOlvi = this.GetAdjacentItemOrNull(null, isGoingUp); + this.StartCellEditIfDifferent(adjacentOlvi, subItemIndex); + break; + } + } + + /// + /// Handle a change column command + /// + /// + /// + protected virtual void HandleColumnChange(Keys keyData, CellEditCharacterBehaviour behaviour) + { + // If we couldn't finish editing the current cell, don't try to move it + if (!this.ListView.PossibleFinishCellEditing()) + return; + + // Changing columns only works in details mode + if (this.ListView.View != View.Details) + return; + + List editableColumns = this.EditableColumnsInDisplayOrder; + OLVListItem olvi = this.ItemBeingEdited; + int displayIndex = Math.Max(0, + editableColumns.IndexOf(this.ListView.GetColumn(this.SubItemIndexBeingEdited))); + bool isGoingLeft = behaviour == CellEditCharacterBehaviour.ChangeColumnLeft; + + // Are we trying to continue past one of the edges? + if ((isGoingLeft && displayIndex == 0) || + (!isGoingLeft && displayIndex == editableColumns.Count - 1)) + { + // Yes, so figure out our at edge behaviour + CellEditAtEdgeBehaviour atEdgeBehaviour; + if (!this.CellEditKeyAtEdgeBehaviourMap.TryGetValue(keyData, out atEdgeBehaviour)) + atEdgeBehaviour = CellEditAtEdgeBehaviour.Wrap; + switch (atEdgeBehaviour) + { + case CellEditAtEdgeBehaviour.Ignore: + return; + case CellEditAtEdgeBehaviour.EndEdit: + this.HandleEndEdit(); + return; + case CellEditAtEdgeBehaviour.ChangeRow: + case CellEditAtEdgeBehaviour.Wrap: + if (atEdgeBehaviour == CellEditAtEdgeBehaviour.ChangeRow) + olvi = GetAdjacentItem(olvi, isGoingLeft && displayIndex == 0); + if (isGoingLeft) + displayIndex = editableColumns.Count - 1; + else + displayIndex = 0; + break; + } + } + else + { + if (isGoingLeft) + displayIndex -= 1; + else + displayIndex += 1; + } + + int subItemIndex = editableColumns[displayIndex].Index; + this.StartCellEditIfDifferent(olvi, subItemIndex); + } + + #endregion + + #region Utilities + + /// + /// Start editing the indicated cell if that cell is not already being edited + /// + /// The row to edit + /// The cell within that row to edit + protected void StartCellEditIfDifferent(OLVListItem olvi, int subItemIndex) { + if (this.ItemBeingEdited == olvi && this.SubItemIndexBeingEdited == subItemIndex) + return; + + this.ListView.EnsureVisible(olvi.Index); + this.ListView.StartCellEdit(olvi, subItemIndex); + } + + /// + /// Gets the adjacent item to the given item in the given direction. + /// If that item is disabled, continue in that direction until an enabled item is found. + /// + /// The row whose neighbour is sought + /// The direction of the adjacentness + /// An OLVListView adjacent to the given item, or null if there are no more enabled items in that direction. + protected OLVListItem GetAdjacentItemOrNull(OLVListItem olvi, bool up) { + OLVListItem item = up ? this.ListView.GetPreviousItem(olvi) : this.ListView.GetNextItem(olvi); + while (item != null && !item.Enabled) + item = up ? this.ListView.GetPreviousItem(item) : this.ListView.GetNextItem(item); + return item; + } + + /// + /// Gets the adjacent item to the given item in the given direction, wrapping if needed. + /// + /// The row whose neighbour is sought + /// The direction of the adjacentness + /// An OLVListView adjacent to the given item, or null if there are no more items in that direction. + protected OLVListItem GetAdjacentItem(OLVListItem olvi, bool up) { + return this.GetAdjacentItemOrNull(olvi, up) ?? this.GetAdjacentItemOrNull(null, up); + } + + /// + /// Gets a collection of columns that are editable in the order they are shown to the user + /// + protected List EditableColumnsInDisplayOrder { + get { + List editableColumnsInDisplayOrder = new List(); + foreach (OLVColumn x in this.ListView.ColumnsInDisplayOrder) + if (x.IsEditable) + editableColumnsInDisplayOrder.Add(x); + return editableColumnsInDisplayOrder; + } + } + + #endregion + } +} diff --git a/ObjectListView/CellEditing/CellEditors.cs b/ObjectListView/CellEditing/CellEditors.cs new file mode 100644 index 00000000..43140211 --- /dev/null +++ b/ObjectListView/CellEditing/CellEditors.cs @@ -0,0 +1,325 @@ +/* + * CellEditors - Several slightly modified controls that are used as cell editors within ObjectListView. + * + * Author: Phillip Piper + * Date: 20/10/2008 5:15 PM + * + * Change log: + * 2018-05-05 JPP - Added ControlUtilities.AutoResizeDropDown() + * v2.6 + * 2012-08-02 JPP - Make most editors public so they can be reused/subclassed + * v2.3 + * 2009-08-13 JPP - Standardized code formatting + * v2.2.1 + * 2008-01-18 JPP - Added special handling for enums + * 2008-01-16 JPP - Added EditorRegistry + * v2.0.1 + * 2008-10-20 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// An interface that allows cell editors to specifically handle getting and setting + /// values from ObjectListView + /// + public interface IOlvEditor { + object Value { get; set; } + } + + public static class ControlUtilities { + + /// + /// Configure the given ComboBox so that the dropped down menu is auto-sized to + /// be wide enough to show the widest item. + /// + /// + public static void AutoResizeDropDown(ComboBox dropDown) { + if (dropDown == null) + throw new ArgumentNullException("dropDown"); + + dropDown.DropDown += delegate(object sender, EventArgs args) { + + // Calculate the maximum width of the drop down items + int newWidth = 0; + foreach (object item in dropDown.Items) { + newWidth = Math.Max(newWidth, TextRenderer.MeasureText(item.ToString(), dropDown.Font).Width); + } + + int vertScrollBarWidth = dropDown.Items.Count > dropDown.MaxDropDownItems ? SystemInformation.VerticalScrollBarWidth : 0; + dropDown.DropDownWidth = newWidth + vertScrollBarWidth; + }; + } + } + + /// + /// These items allow combo boxes to remember a value and its description. + /// + public class ComboBoxItem + { + /// + /// + /// + /// + /// + public ComboBoxItem(Object key, String description) { + this.key = key; + this.description = description; + } + private readonly String description; + + /// + /// + /// + public Object Key { + get { return key; } + } + private readonly Object key; + + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object. + /// + /// 2 + public override string ToString() { + return this.description; + } + } + + //----------------------------------------------------------------------- + // Cell editors + // These classes are simple cell editors that make it easier to get and set + // the value that the control is showing. + // In many cases, you can intercept the CellEditStarting event to + // change the characteristics of the editor. For example, changing + // the acceptable range for a numeric editor or changing the strings + // that represent true and false values for a boolean editor. + + /// + /// This editor shows and auto completes values from the given listview column. + /// + [ToolboxItem(false)] + public class AutoCompleteCellEditor : ComboBox + { + /// + /// Create an AutoCompleteCellEditor + /// + /// + /// + public AutoCompleteCellEditor(ObjectListView lv, OLVColumn column) { + this.DropDownStyle = ComboBoxStyle.DropDown; + + Dictionary alreadySeen = new Dictionary(); + for (int i = 0; i < Math.Min(lv.GetItemCount(), 1000); i++) { + String str = column.GetStringValue(lv.GetModelObject(i)); + if (!alreadySeen.ContainsKey(str)) { + this.Items.Add(str); + alreadySeen[str] = true; + } + } + + this.Sorted = true; + this.AutoCompleteSource = AutoCompleteSource.ListItems; + this.AutoCompleteMode = AutoCompleteMode.Append; + + ControlUtilities.AutoResizeDropDown(this); + } + } + + /// + /// This combo box is specialized to allow editing of an enum. + /// + [ToolboxItem(false)] + public class EnumCellEditor : ComboBox + { + /// + /// + /// + /// + public EnumCellEditor(Type type) { + this.DropDownStyle = ComboBoxStyle.DropDownList; + this.ValueMember = "Key"; + + ArrayList values = new ArrayList(); + foreach (object value in Enum.GetValues(type)) + values.Add(new ComboBoxItem(value, Enum.GetName(type, value))); + + this.DataSource = values; + + ControlUtilities.AutoResizeDropDown(this); + } + } + + /// + /// This editor simply shows and edits integer values. + /// + [ToolboxItem(false)] + public class IntUpDown : NumericUpDown + { + /// + /// + /// + public IntUpDown() { + this.DecimalPlaces = 0; + this.Minimum = -9999999; + this.Maximum = 9999999; + } + + /// + /// Gets or sets the value shown by this editor + /// + public new int Value { + get { return Decimal.ToInt32(base.Value); } + set { base.Value = new Decimal(value); } + } + } + + /// + /// This editor simply shows and edits unsigned integer values. + /// + /// This class can't be made public because unsigned int is not a + /// CLS-compliant type. If you want to use, just copy the code to this class + /// into your project and use it from there. + [ToolboxItem(false)] + internal class UintUpDown : NumericUpDown + { + public UintUpDown() { + this.DecimalPlaces = 0; + this.Minimum = 0; + this.Maximum = 9999999; + } + + public new uint Value { + get { return Decimal.ToUInt32(base.Value); } + set { base.Value = new Decimal(value); } + } + } + + /// + /// This editor simply shows and edits boolean values. + /// + [ToolboxItem(false)] + public class BooleanCellEditor : ComboBox + { + /// + /// + /// + public BooleanCellEditor() { + this.DropDownStyle = ComboBoxStyle.DropDownList; + this.ValueMember = "Key"; + + ArrayList values = new ArrayList(); + values.Add(new ComboBoxItem(false, "False")); + values.Add(new ComboBoxItem(true, "True")); + + this.DataSource = values; + } + } + + /// + /// This editor simply shows and edits boolean values using a checkbox + /// + [ToolboxItem(false)] + public class BooleanCellEditor2 : CheckBox + { + /// + /// Gets or sets the value shown by this editor + /// + public bool? Value { + get { + switch (this.CheckState) { + case CheckState.Checked: return true; + case CheckState.Indeterminate: return null; + case CheckState.Unchecked: + default: return false; + } + } + set { + if (value.HasValue) + this.CheckState = value.Value ? CheckState.Checked : CheckState.Unchecked; + else + this.CheckState = CheckState.Indeterminate; + } + } + + /// + /// Gets or sets how the checkbox will be aligned + /// + public new HorizontalAlignment TextAlign { + get { + switch (this.CheckAlign) { + case ContentAlignment.MiddleRight: return HorizontalAlignment.Right; + case ContentAlignment.MiddleCenter: return HorizontalAlignment.Center; + case ContentAlignment.MiddleLeft: + default: return HorizontalAlignment.Left; + } + } + set { + switch (value) { + case HorizontalAlignment.Left: + this.CheckAlign = ContentAlignment.MiddleLeft; + break; + case HorizontalAlignment.Center: + this.CheckAlign = ContentAlignment.MiddleCenter; + break; + case HorizontalAlignment.Right: + this.CheckAlign = ContentAlignment.MiddleRight; + break; + } + } + } + } + + /// + /// This editor simply shows and edits floating point values. + /// + /// You can intercept the CellEditStarting event if you want + /// to change the characteristics of the editor. For example, by increasing + /// the number of decimal places. + [ToolboxItem(false)] + public class FloatCellEditor : NumericUpDown + { + /// + /// + /// + public FloatCellEditor() { + this.DecimalPlaces = 2; + this.Minimum = -9999999; + this.Maximum = 9999999; + } + + /// + /// Gets or sets the value shown by this editor + /// + public new double Value { + get { return Convert.ToDouble(base.Value); } + set { base.Value = Convert.ToDecimal(value); } + } + } +} diff --git a/ObjectListView/CellEditing/EditorRegistry.cs b/ObjectListView/CellEditing/EditorRegistry.cs new file mode 100644 index 00000000..f92854f5 --- /dev/null +++ b/ObjectListView/CellEditing/EditorRegistry.cs @@ -0,0 +1,213 @@ +/* + * EditorRegistry - A registry mapping types to cell editors. + * + * Author: Phillip Piper + * Date: 6-March-2011 7:53 am + * + * Change log: + * 2011-03-31 JPP - Use OLVColumn.DataType if the value to be edited is null + * 2011-03-06 JPP - Separated from CellEditors.cs + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Reflection; + +namespace BrightIdeasSoftware { + + /// + /// A delegate that creates an editor for the given value + /// + /// The model from which that value came + /// The column for which the editor is being created + /// A representative value of the type to be edited. This value may not be the exact + /// value for the column/model combination. It could be simply representative of + /// the appropriate type of value. + /// A control which can edit the given value + public delegate Control EditorCreatorDelegate(Object model, OLVColumn column, Object value); + + /// + /// An editor registry gives a way to decide what cell editor should be used to edit + /// the value of a cell. Programmers can register non-standard types and the control that + /// should be used to edit instances of that type. + /// + /// + /// All ObjectListViews share the same editor registry. + /// + public class EditorRegistry { + #region Initializing + + /// + /// Create an EditorRegistry + /// + public EditorRegistry() { + this.InitializeStandardTypes(); + } + + private void InitializeStandardTypes() { + this.Register(typeof(Boolean), typeof(BooleanCellEditor)); + this.Register(typeof(Int16), typeof(IntUpDown)); + this.Register(typeof(Int32), typeof(IntUpDown)); + this.Register(typeof(Int64), typeof(IntUpDown)); + this.Register(typeof(UInt16), typeof(UintUpDown)); + this.Register(typeof(UInt32), typeof(UintUpDown)); + this.Register(typeof(UInt64), typeof(UintUpDown)); + this.Register(typeof(Single), typeof(FloatCellEditor)); + this.Register(typeof(Double), typeof(FloatCellEditor)); + this.Register(typeof(DateTime), delegate(Object model, OLVColumn column, Object value) { + DateTimePicker c = new DateTimePicker(); + c.Format = DateTimePickerFormat.Short; + return c; + }); + this.Register(typeof(Boolean), delegate(Object model, OLVColumn column, Object value) { + CheckBox c = new BooleanCellEditor2(); + c.ThreeState = column.TriStateCheckBoxes; + return c; + }); + } + + #endregion + + #region Registering + + /// + /// Register that values of 'type' should be edited by instances of 'controlType'. + /// + /// The type of value to be edited + /// The type of the Control that will edit values of 'type' + /// + /// ObjectListView.EditorRegistry.Register(typeof(Color), typeof(MySpecialColorEditor)); + /// + public void Register(Type type, Type controlType) { + this.Register(type, delegate(Object model, OLVColumn column, Object value) { + return controlType.InvokeMember("", BindingFlags.CreateInstance, null, null, null) as Control; + }); + } + + /// + /// Register the given delegate so that it is called to create editors + /// for values of the given type + /// + /// The type of value to be edited + /// The delegate that will create a control that can edit values of 'type' + /// + /// ObjectListView.EditorRegistry.Register(typeof(Color), CreateColorEditor); + /// ... + /// public Control CreateColorEditor(Object model, OLVColumn column, Object value) + /// { + /// return new MySpecialColorEditor(); + /// } + /// + public void Register(Type type, EditorCreatorDelegate creator) { + this.creatorMap[type] = creator; + } + + /// + /// Register a delegate that will be called to create an editor for values + /// that have not been handled. + /// + /// The delegate that will create a editor for all other types + public void RegisterDefault(EditorCreatorDelegate creator) { + this.defaultCreator = creator; + } + + /// + /// Register a delegate that will be given a chance to create a control + /// before any other option is considered. + /// + /// The delegate that will create a control + public void RegisterFirstChance(EditorCreatorDelegate creator) { + this.firstChanceCreator = creator; + } + + /// + /// Remove the registered handler for the given type + /// + /// Does nothing if the given type doesn't exist + /// The type whose registration is to be removed + public void Unregister(Type type) { + if (this.creatorMap.ContainsKey(type)) + this.creatorMap.Remove(type); + } + + #endregion + + #region Accessing + + /// + /// Create and return an editor that is appropriate for the given value. + /// Return null if no appropriate editor can be found. + /// + /// The model involved + /// The column to be edited + /// The value to be edited. This value may not be the exact + /// value for the column/model combination. It could be simply representative of + /// the appropriate type of value. + /// A Control that can edit the given type of values + public Control GetEditor(Object model, OLVColumn column, Object value) { + Control editor; + + // Give the first chance delegate a chance to decide + if (this.firstChanceCreator != null) { + editor = this.firstChanceCreator(model, column, value); + if (editor != null) + return editor; + } + + // Try to find a creator based on the type of the value (or the column) + Type type = value == null ? column.DataType : value.GetType(); + if (type != null && this.creatorMap.ContainsKey(type)) { + editor = this.creatorMap[type](model, column, value); + if (editor != null) + return editor; + } + + // Enums without other processing get a special editor + if (value != null && value.GetType().IsEnum) + return this.CreateEnumEditor(value.GetType()); + + // Give any default creator a final chance + if (this.defaultCreator != null) + return this.defaultCreator(model, column, value); + + return null; + } + + /// + /// Create and return an editor that will edit values of the given type + /// + /// A enum type + protected Control CreateEnumEditor(Type type) { + return new EnumCellEditor(type); + } + + #endregion + + #region Private variables + + private EditorCreatorDelegate firstChanceCreator; + private EditorCreatorDelegate defaultCreator; + private Dictionary creatorMap = new Dictionary(); + + #endregion + } +} diff --git a/ObjectListView/CustomDictionary.xml b/ObjectListView/CustomDictionary.xml new file mode 100644 index 00000000..f2cf5b9f --- /dev/null +++ b/ObjectListView/CustomDictionary.xml @@ -0,0 +1,46 @@ + + + + + br + Canceled + Center + Color + Colors + f + fmt + g + gdi + hti + i + lightbox + lv + lvi + lvsi + m + multi + Munger + n + olv + olvi + p + parms + r + Renderer + s + SubItem + Unapply + Unpause + x + y + + + ComPlus + + + + + OLV + + + diff --git a/ObjectListView/DataListView.cs b/ObjectListView/DataListView.cs new file mode 100644 index 00000000..2961d040 --- /dev/null +++ b/ObjectListView/DataListView.cs @@ -0,0 +1,240 @@ +/* + * DataListView - A data-bindable listview + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * 2015-02-02 JPP - Made Unfreezing more efficient by removing a redundant BuildList() call + * v2.6 + * 2011-02-27 JPP - Moved most of the logic to DataSourceAdapter (where it + * can be used by FastDataListView too) + * v2.3 + * 2009-01-18 JPP - Boolean columns are now handled as checkboxes + * - Auto-generated columns would fail if the data source was + * reseated, even to the same data source + * v2.0.1 + * 2009-01-07 JPP - Made all public and protected methods virtual + * 2008-10-03 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2015 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Drawing.Design; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + + /// + /// A DataListView is a ListView that can be bound to a datasource (which would normally be a DataTable or DataView). + /// + /// + /// This listview keeps itself in sync with its source datatable by listening for change events. + /// The DataListView will automatically create columns to show all of the data source's columns/properties, if there is not already + /// a column showing that property. This allows you to define one or two columns in the designer and then have the others generated automatically. + /// If you don't want any column to be auto generated, set to false. + /// These generated columns will be only the simplest view of the world, and would look more interesting with a few delegates installed. + /// This listview will also automatically generate missing aspect getters to fetch the values from the data view. + /// Changing data sources is possible, but error prone. Before changing data sources, the programmer is responsible for modifying/resetting + /// the column collection to be valid for the new data source. + /// Internally, a CurrencyManager controls keeping the data source in-sync with other users of the data source (as per normal .NET + /// behavior). This means that the model objects in the DataListView are DataRowView objects. If you write your own AspectGetters/Setters, + /// they will be given DataRowView objects. + /// + public class DataListView : ObjectListView + { + #region Life and death + + /// + /// Make a DataListView + /// + public DataListView() + { + this.Adapter = new DataSourceAdapter(this); + } + + /// + /// + /// + /// + protected override void Dispose(bool disposing) { + this.Adapter.Dispose(); + base.Dispose(disposing); + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets whether or not columns will be automatically generated to show the + /// columns when the DataSource is set. + /// + /// This must be set before the DataSource is set. It has no effect afterwards. + [Category("Data"), + Description("Should the control automatically generate columns from the DataSource"), + DefaultValue(true)] + public bool AutoGenerateColumns { + get { return this.Adapter.AutoGenerateColumns; } + set { this.Adapter.AutoGenerateColumns = value; } + } + + /// + /// Get or set the DataSource that will be displayed in this list view. + /// + /// The DataSource should implement either , , + /// or . Some common examples are the following types of objects: + /// + /// + /// + /// + /// + /// + /// + /// When binding to a list container (i.e. one that implements the + /// interface, such as ) + /// you must also set the property in order + /// to identify which particular list you would like to display. You + /// may also set the property even when + /// DataSource refers to a list, since can + /// also be used to navigate relations between lists. + /// When a DataSource is set, the control will create OLVColumns to show any + /// data source columns that are not already shown. + /// If the DataSource is changed, you will have to remove any previously + /// created columns, since they will be configured for the previous DataSource. + /// . + /// + [Category("Data"), + TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")] + public virtual Object DataSource + { + get { return this.Adapter.DataSource; } + set { this.Adapter.DataSource = value; } + } + + /// + /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data. + /// + /// If the data source is not a DataSet or DataViewManager, this property has no effect + [Category("Data"), + Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design", typeof(UITypeEditor)), + DefaultValue("")] + public virtual string DataMember + { + get { return this.Adapter.DataMember; } + set { this.Adapter.DataMember = value; } + } + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the DataSourceAdaptor that does the bulk of the work needed + /// for data binding. + /// + /// + /// Adaptors cannot be shared between controls. Each DataListView needs its own adapter. + /// + protected DataSourceAdapter Adapter { + get { + Debug.Assert(adapter != null, "Data adapter should not be null"); + return adapter; + } + set { adapter = value; } + } + private DataSourceAdapter adapter; + + #endregion + + #region Object manipulations + + /// + /// Add the given collection of model objects to this control. + /// + /// A collection of model objects + /// This is a no-op for data lists, since the data + /// is controlled by the DataSource. Manipulate the data source + /// rather than this view of the data source. + public override void AddObjects(ICollection modelObjects) + { + } + + /// + /// Insert the given collection of objects before the given position + /// + /// Where to insert the objects + /// The objects to be inserted + /// This is a no-op for data lists, since the data + /// is controlled by the DataSource. Manipulate the data source + /// rather than this view of the data source. + public override void InsertObjects(int index, ICollection modelObjects) { + } + + /// + /// Remove the given collection of model objects from this control. + /// + /// This is a no-op for data lists, since the data + /// is controlled by the DataSource. Manipulate the data source + /// rather than this view of the data source. + public override void RemoveObjects(ICollection modelObjects) + { + } + + #endregion + + #region Event Handlers + + /// + /// Change the Unfreeze behaviour + /// + protected override void DoUnfreeze() { + + // Copied from base method, but we don't need to BuildList() since we know that our + // data adaptor is going to do that immediately after this method exits. + this.EndUpdate(); + this.ResizeFreeSpaceFillingColumns(); + // this.BuildList(); + } + + /// + /// Handles parent binding context changes + /// + /// Unused EventArgs. + protected override void OnParentBindingContextChanged(EventArgs e) + { + base.OnParentBindingContextChanged(e); + + // BindingContext is an ambient property - by default it simply picks + // up the parent control's context (unless something has explicitly + // given us our own). So we must respond to changes in our parent's + // binding context in the same way we would changes to our own + // binding context. + + // THINK: Do we need to forward this to the adapter? + } + + #endregion + } +} diff --git a/ObjectListView/DataTreeListView.cs b/ObjectListView/DataTreeListView.cs new file mode 100644 index 00000000..65179a91 --- /dev/null +++ b/ObjectListView/DataTreeListView.cs @@ -0,0 +1,240 @@ +/* + * DataTreeListView - A data bindable TreeListView + * + * Author: Phillip Piper + * Date: 05/05/2012 3:26 PM + * + * Change log: + + * 2012-05-05 JPP Initial version + * + * TO DO: + + * + * Copyright (C) 2012 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Drawing.Design; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A DataTreeListView is a TreeListView that calculates its hierarchy based on + /// information in the data source. + /// + /// + /// Like a , a DataTreeListView sources all its information + /// from a combination of and . + /// can be a DataTable, DataSet, + /// or anything that implements . + /// + /// + /// To function properly, the DataTreeListView requires: + /// + /// the table to have a column which holds a unique for the row. The name of this column must be set in . + /// the table to have a column which holds id of the hierarchical parent of the row. The name of this column must be set in . + /// a value which identifies which rows are the roots of the tree (). + /// + /// The hierarchy structure is determined finding all the rows where the parent key is equal to . These rows + /// become the root objects of the hierarchy. + /// + /// Like a TreeListView, the hierarchy must not contain cycles. Bad things will happen if the data is cyclic. + /// + public partial class DataTreeListView : TreeListView + { + #region Public Properties + + /// + /// Gets or sets whether or not columns will be automatically generated to show the + /// columns when the DataSource is set. + /// + /// This must be set before the DataSource is set. It has no effect afterwards. + [Category("Data"), + Description("Should the control automatically generate columns from the DataSource"), + DefaultValue(true)] + public bool AutoGenerateColumns + { + get { return this.Adapter.AutoGenerateColumns; } + set { this.Adapter.AutoGenerateColumns = value; } + } + + /// + /// Get or set the DataSource that will be displayed in this list view. + /// + /// The DataSource should implement either , , + /// or . Some common examples are the following types of objects: + /// + /// + /// + /// + /// + /// + /// + /// When binding to a list container (i.e. one that implements the + /// interface, such as ) + /// you must also set the property in order + /// to identify which particular list you would like to display. You + /// may also set the property even when + /// DataSource refers to a list, since can + /// also be used to navigate relations between lists. + /// + [Category("Data"), + TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")] + public virtual Object DataSource { + get { return this.Adapter.DataSource; } + set { this.Adapter.DataSource = value; } + } + + /// + /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data. + /// + /// If the data source is not a DataSet or DataViewManager, this property has no effect + [Category("Data"), + Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design", typeof(UITypeEditor)), + DefaultValue("")] + public virtual string DataMember { + get { return this.Adapter.DataMember; } + set { this.Adapter.DataMember = value; } + } + + /// + /// Gets or sets the name of the property/column that uniquely identifies each row. + /// + /// + /// + /// The value contained by this column must be unique across all rows + /// in the data source. Odd and unpredictable things will happen if two + /// rows have the same id. + /// + /// Null cannot be a valid key value. + /// + [Category("Data"), + Description("The name of the property/column that holds the key of a row"), + DefaultValue(null)] + public virtual string KeyAspectName { + get { return this.Adapter.KeyAspectName; } + set { this.Adapter.KeyAspectName = value; } + } + + /// + /// Gets or sets the name of the property/column that contains the key of + /// the parent of a row. + /// + /// + /// + /// The test condition for deciding if one row is the parent of another is functionally + /// equivalent to this: + /// + /// Object.Equals(candidateParentRow[this.KeyAspectName], row[this.ParentKeyAspectName]) + /// + /// + /// Unlike key value, parent keys can be null but a null parent key can only be used + /// to identify root objects. + /// + [Category("Data"), + Description("The name of the property/column that holds the key of the parent of a row"), + DefaultValue(null)] + public virtual string ParentKeyAspectName { + get { return this.Adapter.ParentKeyAspectName; } + set { this.Adapter.ParentKeyAspectName = value; } + } + + /// + /// Gets or sets the value that identifies a row as a root object. + /// When the ParentKey of a row equals the RootKeyValue, that row will + /// be treated as root of the TreeListView. + /// + /// + /// + /// The test condition for deciding a root object is functionally + /// equivalent to this: + /// + /// Object.Equals(candidateRow[this.ParentKeyAspectName], this.RootKeyValue) + /// + /// + /// The RootKeyValue can be null. Actually, it can be any value that can + /// be compared for equality against a basic type. + /// If this is set to the wrong value (i.e. to a value that no row + /// has in the parent id column), the list will be empty. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual object RootKeyValue { + get { return this.Adapter.RootKeyValue; } + set { this.Adapter.RootKeyValue = value; } + } + + /// + /// Gets or sets the value that identifies a row as a root object. + /// . The RootKeyValue can be of any type, + /// but the IDE cannot sensibly represent a value of any type, + /// so this is a typed wrapper around that property. + /// + /// + /// If you want the root value to be something other than a string, + /// you will have set it yourself. + /// + [Category("Data"), + Description("The parent id value that identifies a row as a root object"), + DefaultValue(null)] + public virtual string RootKeyValueString { + get { return Convert.ToString(this.Adapter.RootKeyValue); } + set { this.Adapter.RootKeyValue = value; } + } + + /// + /// Gets or sets whether or not the key columns (id and parent id) should + /// be shown to the user. + /// + /// This must be set before the DataSource is set. It has no effect + /// afterwards. + [Category("Data"), + Description("Should the keys columns (id and parent id) be shown to the user?"), + DefaultValue(true)] + public virtual bool ShowKeyColumns { + get { return this.Adapter.ShowKeyColumns; } + set { this.Adapter.ShowKeyColumns = value; } + } + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the DataSourceAdaptor that does the bulk of the work needed + /// for data binding. + /// + protected TreeDataSourceAdapter Adapter { + get { + if (this.adapter == null) + this.adapter = new TreeDataSourceAdapter(this); + return adapter; + } + set { adapter = value; } + } + private TreeDataSourceAdapter adapter; + + #endregion + } +} diff --git a/ObjectListView/DragDrop/DragSource.cs b/ObjectListView/DragDrop/DragSource.cs new file mode 100644 index 00000000..1abf13b5 --- /dev/null +++ b/ObjectListView/DragDrop/DragSource.cs @@ -0,0 +1,219 @@ +/* + * DragSource.cs - Add drag source functionality to an ObjectListView + * + * Author: Phillip Piper + * Date: 2009-03-17 5:15 PM + * + * Change log: + * 2011-03-29 JPP - Separate OLVDataObject.cs + * v2.3 + * 2009-07-06 JPP - Make sure Link is acceptable as an drop effect by default + * (since MS didn't make it part of the 'All' value) + * v2.2 + * 2009-04-15 JPP - Separated DragSource.cs into DropSink.cs + * 2009-03-17 JPP - Initial version + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace BrightIdeasSoftware +{ + /// + /// An IDragSource controls how drag out from the ObjectListView will behave + /// + public interface IDragSource + { + /// + /// A drag operation is beginning. Return the data object that will be used + /// for data transfer. Return null to prevent the drag from starting. The data + /// object will normally include all the selected objects. + /// + /// + /// The returned object is later passed to the GetAllowedEffect() and EndDrag() + /// methods. + /// + /// What ObjectListView is being dragged from. + /// Which mouse button is down? + /// What item was directly dragged by the user? There may be more than just this + /// item selected. + /// The data object that will be used for data transfer. This will often be a subclass + /// of DataObject, but does not need to be. + Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item); + + /// + /// What operations are possible for this drag? This controls the icon shown during the drag + /// + /// The data object returned by StartDrag() + /// A combination of DragDropEffects flags + DragDropEffects GetAllowedEffects(Object dragObject); + + /// + /// The drag operation is complete. Do whatever is necessary to complete the action. + /// + /// The data object returned by StartDrag() + /// The value returned from GetAllowedEffects() + void EndDrag(Object dragObject, DragDropEffects effect); + } + + /// + /// A do-nothing implementation of IDragSource that can be safely subclassed. + /// + public class AbstractDragSource : IDragSource + { + #region IDragSource Members + + /// + /// See IDragSource documentation + /// + /// + /// + /// + /// + public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) { + return null; + } + + /// + /// See IDragSource documentation + /// + /// + /// + public virtual DragDropEffects GetAllowedEffects(Object data) { + return DragDropEffects.None; + } + + /// + /// See IDragSource documentation + /// + /// + /// + public virtual void EndDrag(Object dragObject, DragDropEffects effect) { + } + + #endregion + } + + /// + /// A reasonable implementation of IDragSource that provides normal + /// drag source functionality. It creates a data object that supports + /// inter-application dragging of text and HTML representation of + /// the dragged rows. It can optionally force a refresh of all dragged + /// rows when the drag is complete. + /// + /// Subclasses can override GetDataObject() to add new + /// data formats to the data transfer object. + public class SimpleDragSource : IDragSource + { + #region Constructors + + /// + /// Construct a SimpleDragSource + /// + public SimpleDragSource() { + } + + /// + /// Construct a SimpleDragSource that refreshes the dragged rows when + /// the drag is complete + /// + /// + public SimpleDragSource(bool refreshAfterDrop) { + this.RefreshAfterDrop = refreshAfterDrop; + } + + #endregion + + #region Public properties + + /// + /// Gets or sets whether the dragged rows should be refreshed when the + /// drag operation is complete. + /// + public bool RefreshAfterDrop { + get { return refreshAfterDrop; } + set { refreshAfterDrop = value; } + } + private bool refreshAfterDrop; + + #endregion + + #region IDragSource Members + + /// + /// Create a DataObject when the user does a left mouse drag operation. + /// See IDragSource for further information. + /// + /// + /// + /// + /// + public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) { + // We only drag on left mouse + if (button != MouseButtons.Left) + return null; + + return this.CreateDataObject(olv); + } + + /// + /// Which operations are allowed in the operation? By default, all operations are supported. + /// + /// + /// All operations are supported + public virtual DragDropEffects GetAllowedEffects(Object data) { + return DragDropEffects.All | DragDropEffects.Link; // why didn't MS include 'Link' in 'All'?? + } + + /// + /// The drag operation is finished. Refreshes the dragged rows if so configured. + /// + /// + /// + public virtual void EndDrag(Object dragObject, DragDropEffects effect) { + OLVDataObject data = dragObject as OLVDataObject; + if (data == null) + return; + + if (this.RefreshAfterDrop) + data.ListView.RefreshObjects(data.ModelObjects); + } + + /// + /// Create a data object that will be used to as the data object + /// for the drag operation. + /// + /// + /// Subclasses can override this method add new formats to the data object. + /// + /// The ObjectListView that is the source of the drag + /// A data object for the drag + protected virtual object CreateDataObject(ObjectListView olv) { + return new OLVDataObject(olv); + } + + #endregion + } +} diff --git a/ObjectListView/DragDrop/DropSink.cs b/ObjectListView/DragDrop/DropSink.cs new file mode 100644 index 00000000..c82bd673 --- /dev/null +++ b/ObjectListView/DragDrop/DropSink.cs @@ -0,0 +1,1562 @@ +/* + * DropSink.cs - Add drop sink ability to an ObjectListView + * + * Author: Phillip Piper + * Date: 2009-03-17 5:15 PM + * + * Change log: + * 2018-04-26 JPP - Implemented LeftOfItem and RightOfItem target locations + * - Added support for rearranging on non-Detail views. + * v2.9 + * 2015-07-08 JPP - Added SimpleDropSink.EnableFeedback to allow all the pretty and helpful + * user feedback during drags to be turned off + * v2.7 + * 2011-04-20 JPP - Rewrote how ModelDropEventArgs.RefreshObjects() works on TreeListViews + * v2.4.1 + * 2010-08-24 JPP - Moved AcceptExternal property up to SimpleDragSource. + * v2.3 + * 2009-09-01 JPP - Correctly handle case where RefreshObjects() is called for + * objects that were children but are now roots. + * 2009-08-27 JPP - Added ModelDropEventArgs.RefreshObjects() to simplify updating after + * a drag-drop operation + * 2009-08-19 JPP - Changed to use OlvHitTest() + * v2.2.1 + * 2007-07-06 JPP - Added StandardDropActionFromKeys property to OlvDropEventArgs + * v2.2 + * 2009-05-17 JPP - Added a Handled flag to OlvDropEventArgs + * - Tweaked the appearance of the drop-on-background feedback + * 2009-04-15 JPP - Separated DragDrop.cs into DropSink.cs + * 2009-03-17 JPP - Initial version + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// Objects that implement this interface can acts as the receiver for drop + /// operation for an ObjectListView. + /// + public interface IDropSink + { + /// + /// Gets or sets the ObjectListView that is the drop sink + /// + ObjectListView ListView { get; set; } + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + void DrawFeedback(Graphics g, Rectangle bounds); + + /// + /// The user has released the drop over this control + /// + /// + /// Implementors should set args.Effect to the appropriate DragDropEffects. This value is returned + /// to the originator of the drag. + /// + /// + void Drop(DragEventArgs args); + + /// + /// A drag has entered this control. + /// + /// Implementors should set args.Effect to the appropriate DragDropEffects. + /// + void Enter(DragEventArgs args); + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// + void GiveFeedback(GiveFeedbackEventArgs args); + + /// + /// The drag has left the bounds of this control + /// + void Leave(); + + /// + /// The drag is moving over this control. + /// + /// This is where any drop target should be calculated. + /// Implementors should set args.Effect to the appropriate DragDropEffects. + /// + /// + void Over(DragEventArgs args); + + /// + /// Should the drag be allowed to continue? + /// + /// + void QueryContinue(QueryContinueDragEventArgs args); + } + + /// + /// This is a do-nothing implementation of IDropSink that is a useful + /// base class for more sophisticated implementations. + /// + public class AbstractDropSink : IDropSink + { + #region IDropSink Members + + /// + /// Gets or sets the ObjectListView that is the drop sink + /// + public virtual ObjectListView ListView { + get { return listView; } + set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + public virtual void DrawFeedback(Graphics g, Rectangle bounds) { + } + + /// + /// The user has released the drop over this control + /// + /// + /// Implementors should set args.Effect to the appropriate DragDropEffects. This value is returned + /// to the originator of the drag. + /// + /// + public virtual void Drop(DragEventArgs args) { + this.Cleanup(); + } + + /// + /// A drag has entered this control. + /// + /// Implementors should set args.Effect to the appropriate DragDropEffects. + /// + public virtual void Enter(DragEventArgs args) { + } + + /// + /// The drag has left the bounds of this control + /// + public virtual void Leave() { + this.Cleanup(); + } + + /// + /// The drag is moving over this control. + /// + /// This is where any drop target should be calculated. + /// Implementors should set args.Effect to the appropriate DragDropEffects. + /// + /// + public virtual void Over(DragEventArgs args) { + } + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// You only need to override this if you want non-standard cursors. + /// The standard cursors are supplied automatically. + /// + public virtual void GiveFeedback(GiveFeedbackEventArgs args) { + args.UseDefaultCursors = true; + } + + /// + /// Should the drag be allowed to continue? + /// + /// + /// You only need to override this if you want the user to be able + /// to end the drop in some non-standard way, e.g. dragging to a + /// certain point even without releasing the mouse, or going outside + /// the bounds of the application. + /// + /// + public virtual void QueryContinue(QueryContinueDragEventArgs args) { + } + + + #endregion + + #region Commands + + /// + /// This is called when the mouse leaves the drop region and after the + /// drop has completed. + /// + protected virtual void Cleanup() { + } + + #endregion + } + + /// + /// The enum indicates which target has been found for a drop operation + /// + [Flags] + public enum DropTargetLocation + { + /// + /// No applicable target has been found + /// + None = 0, + + /// + /// The list itself is the target of the drop + /// + Background = 0x01, + + /// + /// An item is the target + /// + Item = 0x02, + + /// + /// Between two items (or above the top item or below the bottom item) + /// can be the target. This is not actually ever a target, only a value indicate + /// that it is valid to drop between items + /// + BetweenItems = 0x04, + + /// + /// Above an item is the target + /// + AboveItem = 0x08, + + /// + /// Below an item is the target + /// + BelowItem = 0x10, + + /// + /// A subitem is the target of the drop + /// + SubItem = 0x20, + + /// + /// On the right of an item is the target + /// + RightOfItem = 0x40, + + /// + /// On the left of an item is the target + /// + LeftOfItem = 0x80 + } + + /// + /// This class represents a simple implementation of a drop sink. + /// + /// + /// Actually, it should be called CleverDropSink -- it's far from simple and can do quite a lot in its own right. + /// + public class SimpleDropSink : AbstractDropSink + { + #region Life and death + + /// + /// Make a new drop sink + /// + public SimpleDropSink() { + this.timer = new Timer(); + this.timer.Interval = 250; + this.timer.Tick += new EventHandler(this.timer_Tick); + + this.CanDropOnItem = true; + //this.CanDropOnSubItem = true; + //this.CanDropOnBackground = true; + //this.CanDropBetween = true; + + this.FeedbackColor = Color.FromArgb(180, Color.MediumBlue); + this.billboard = new BillboardOverlay(); + } + + #endregion + + #region Public properties + + /// + /// Get or set the locations where a drop is allowed to occur (OR-ed together) + /// + public DropTargetLocation AcceptableLocations { + get { return this.acceptableLocations; } + set { this.acceptableLocations = value; } + } + private DropTargetLocation acceptableLocations; + + /// + /// Gets or sets whether this sink allows model objects to be dragged from other lists. Defaults to true. + /// + public bool AcceptExternal { + get { return this.acceptExternal; } + set { this.acceptExternal = value; } + } + private bool acceptExternal = true; + + /// + /// Gets or sets whether the ObjectListView should scroll when the user drags + /// something near to the top or bottom rows. Defaults to true. + /// + /// AutoScroll does not scroll horizontally. + public bool AutoScroll { + get { return this.autoScroll; } + set { this.autoScroll = value; } + } + private bool autoScroll = true; + + /// + /// Gets the billboard overlay that will be used to display feedback + /// messages during a drag operation. + /// + /// Set this to null to stop the feedback. + public BillboardOverlay Billboard { + get { return this.billboard; } + set { this.billboard = value; } + } + private BillboardOverlay billboard; + + /// + /// Get or set whether a drop can occur between items of the list + /// + public bool CanDropBetween { + get { return (this.AcceptableLocations & DropTargetLocation.BetweenItems) == DropTargetLocation.BetweenItems; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.BetweenItems; + else + this.AcceptableLocations &= ~DropTargetLocation.BetweenItems; + } + } + + /// + /// Get or set whether a drop can occur on the listview itself + /// + public bool CanDropOnBackground { + get { return (this.AcceptableLocations & DropTargetLocation.Background) == DropTargetLocation.Background; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.Background; + else + this.AcceptableLocations &= ~DropTargetLocation.Background; + } + } + + /// + /// Get or set whether a drop can occur on items in the list + /// + public bool CanDropOnItem { + get { return (this.AcceptableLocations & DropTargetLocation.Item) == DropTargetLocation.Item; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.Item; + else + this.AcceptableLocations &= ~DropTargetLocation.Item; + } + } + + /// + /// Get or set whether a drop can occur on a subitem in the list + /// + public bool CanDropOnSubItem { + get { return (this.AcceptableLocations & DropTargetLocation.SubItem) == DropTargetLocation.SubItem; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.SubItem; + else + this.AcceptableLocations &= ~DropTargetLocation.SubItem; + } + } + + /// + /// Gets or sets whether the drop sink should draw feedback onto the given list + /// during the drag operation. Defaults to true. + /// + /// If this is false, you will have to give the user feedback in some + /// other fashion, like cursor changes + public bool EnableFeedback { + get { return enableFeedback; } + set { enableFeedback = value; } + } + private bool enableFeedback = true; + + /// + /// Get or set the index of the item that is the target of the drop + /// + public int DropTargetIndex { + get { return dropTargetIndex; } + set { + if (this.dropTargetIndex != value) { + this.dropTargetIndex = value; + this.ListView.Invalidate(); + } + } + } + private int dropTargetIndex = -1; + + /// + /// Get the item that is the target of the drop + /// + public OLVListItem DropTargetItem { + get { + return this.ListView.GetItem(this.DropTargetIndex); + } + } + + /// + /// Get or set the location of the target of the drop + /// + public DropTargetLocation DropTargetLocation { + get { return dropTargetLocation; } + set { + if (this.dropTargetLocation != value) { + this.dropTargetLocation = value; + this.ListView.Invalidate(); + } + } + } + private DropTargetLocation dropTargetLocation; + + /// + /// Get or set the index of the subitem that is the target of the drop + /// + public int DropTargetSubItemIndex { + get { return dropTargetSubItemIndex; } + set { + if (this.dropTargetSubItemIndex != value) { + this.dropTargetSubItemIndex = value; + this.ListView.Invalidate(); + } + } + } + private int dropTargetSubItemIndex = -1; + + /// + /// Get or set the color that will be used to provide drop feedback + /// + public Color FeedbackColor { + get { return this.feedbackColor; } + set { this.feedbackColor = value; } + } + private Color feedbackColor; + + /// + /// Get whether the alt key was down during this drop event + /// + public bool IsAltDown { + get { return (this.KeyState & 32) == 32; } + } + + /// + /// Get whether any modifier key was down during this drop event + /// + public bool IsAnyModifierDown { + get { return (this.KeyState & (4 + 8 + 32)) != 0; } + } + + /// + /// Get whether the control key was down during this drop event + /// + public bool IsControlDown { + get { return (this.KeyState & 8) == 8; } + } + + /// + /// Get whether the left mouse button was down during this drop event + /// + public bool IsLeftMouseButtonDown { + get { return (this.KeyState & 1) == 1; } + } + + /// + /// Get whether the right mouse button was down during this drop event + /// + public bool IsMiddleMouseButtonDown { + get { return (this.KeyState & 16) == 16; } + } + + /// + /// Get whether the right mouse button was down during this drop event + /// + public bool IsRightMouseButtonDown { + get { return (this.KeyState & 2) == 2; } + } + + /// + /// Get whether the shift key was down during this drop event + /// + public bool IsShiftDown { + get { return (this.KeyState & 4) == 4; } + } + + /// + /// Get or set the state of the keys during this drop event + /// + public int KeyState { + get { return this.keyState; } + set { this.keyState = value; } + } + private int keyState; + + /// + /// Gets or sets whether the drop sink will automatically use cursors + /// based on the drop effect. By default, this is true. If this is + /// set to false, you must set the Cursor yourself. + /// + public bool UseDefaultCursors { + get { return useDefaultCursors; } + set { useDefaultCursors = value; } + } + private bool useDefaultCursors = true; + + #endregion + + #region Events + + /// + /// Triggered when the sink needs to know if a drop can occur. + /// + /// + /// Handlers should set Effect to indicate what is possible. + /// Handlers can change any of the DropTarget* settings to change + /// the target of the drop. + /// + public event EventHandler CanDrop; + + /// + /// Triggered when the drop is made. + /// + public event EventHandler Dropped; + + /// + /// Triggered when the sink needs to know if a drop can occur + /// AND the source is an ObjectListView + /// + /// + /// Handlers should set Effect to indicate what is possible. + /// Handlers can change any of the DropTarget* settings to change + /// the target of the drop. + /// + public event EventHandler ModelCanDrop; + + /// + /// Triggered when the drop is made. + /// AND the source is an ObjectListView + /// + public event EventHandler ModelDropped; + + #endregion + + #region DropSink Interface + + /// + /// Cleanup the drop sink when the mouse has left the control or + /// the drag has finished. + /// + protected override void Cleanup() { + this.DropTargetLocation = DropTargetLocation.None; + this.ListView.FullRowSelect = this.originalFullRowSelect; + this.Billboard.Text = null; + } + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + public override void DrawFeedback(Graphics g, Rectangle bounds) { + g.SmoothingMode = ObjectListView.SmoothingMode; + + if (this.EnableFeedback) { + switch (this.DropTargetLocation) { + case DropTargetLocation.Background: + this.DrawFeedbackBackgroundTarget(g, bounds); + break; + case DropTargetLocation.Item: + this.DrawFeedbackItemTarget(g, bounds); + break; + case DropTargetLocation.AboveItem: + this.DrawFeedbackAboveItemTarget(g, bounds); + break; + case DropTargetLocation.BelowItem: + this.DrawFeedbackBelowItemTarget(g, bounds); + break; + case DropTargetLocation.LeftOfItem: + this.DrawFeedbackLeftOfItemTarget(g, bounds); + break; + case DropTargetLocation.RightOfItem: + this.DrawFeedbackRightOfItemTarget(g, bounds); + break; + } + } + + if (this.Billboard != null) { + this.Billboard.Draw(this.ListView, g, bounds); + } + } + + /// + /// The user has released the drop over this control + /// + /// + public override void Drop(DragEventArgs args) { + this.dropEventArgs.DragEventArgs = args; + this.TriggerDroppedEvent(args); + this.timer.Stop(); + this.Cleanup(); + } + + /// + /// A drag has entered this control. + /// + /// Implementors should set args.Effect to the appropriate DragDropEffects. + /// + public override void Enter(DragEventArgs args) { + //System.Diagnostics.Debug.WriteLine("Enter"); + + /* + * When FullRowSelect is true, we have two problems: + * 1) GetItemRect(ItemOnly) returns the whole row rather than just the icon/text, which messes + * up our calculation of the drop rectangle. + * 2) during the drag, the Timer events will not fire! This is the major problem, since without + * those events we can't autoscroll. + * + * The first problem we can solve through coding, but the second is more difficult. + * We avoid both problems by turning off FullRowSelect during the drop operation. + */ + this.originalFullRowSelect = this.ListView.FullRowSelect; + this.ListView.FullRowSelect = false; + + // Setup our drop event args block + this.dropEventArgs = new ModelDropEventArgs(); + this.dropEventArgs.DropSink = this; + this.dropEventArgs.ListView = this.ListView; + this.dropEventArgs.DragEventArgs = args; + this.dropEventArgs.DataObject = args.Data; + OLVDataObject olvData = args.Data as OLVDataObject; + if (olvData != null) { + this.dropEventArgs.SourceListView = olvData.ListView; + this.dropEventArgs.SourceModels = olvData.ModelObjects; + } + + this.Over(args); + } + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// + public override void GiveFeedback(GiveFeedbackEventArgs args) { + args.UseDefaultCursors = this.UseDefaultCursors; + } + + /// + /// The drag is moving over this control. + /// + /// + public override void Over(DragEventArgs args) { + //System.Diagnostics.Debug.WriteLine("Over"); + this.dropEventArgs.DragEventArgs = args; + this.KeyState = args.KeyState; + Point pt = this.ListView.PointToClient(new Point(args.X, args.Y)); + args.Effect = this.CalculateDropAction(args, pt); + this.CheckScrolling(pt); + } + + #endregion + + #region Events + + /// + /// Trigger the Dropped events + /// + /// + protected virtual void TriggerDroppedEvent(DragEventArgs args) { + this.dropEventArgs.Handled = false; + + // If the source is an ObjectListView, trigger the ModelDropped event + if (this.dropEventArgs.SourceListView != null) + this.OnModelDropped(this.dropEventArgs); + + if (!this.dropEventArgs.Handled) + this.OnDropped(this.dropEventArgs); + } + + /// + /// Trigger CanDrop + /// + /// + protected virtual void OnCanDrop(OlvDropEventArgs args) { + if (this.CanDrop != null) + this.CanDrop(this, args); + } + + /// + /// Trigger Dropped + /// + /// + protected virtual void OnDropped(OlvDropEventArgs args) { + if (this.Dropped != null) + this.Dropped(this, args); + } + + /// + /// Trigger ModelCanDrop + /// + /// + protected virtual void OnModelCanDrop(ModelDropEventArgs args) { + + // Don't allow drops from other list, if that's what's configured + if (!this.AcceptExternal && args.SourceListView != null && args.SourceListView != this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + args.InfoMessage = "This list doesn't accept drops from other lists"; + return; + } + + if (this.ModelCanDrop != null) + this.ModelCanDrop(this, args); + } + + /// + /// Trigger ModelDropped + /// + /// + protected virtual void OnModelDropped(ModelDropEventArgs args) { + if (this.ModelDropped != null) + this.ModelDropped(this, args); + } + + #endregion + + #region Implementation + + private void timer_Tick(object sender, EventArgs e) { + this.HandleTimerTick(); + } + + /// + /// Handle the timer tick event, which is sent when the listview should + /// scroll + /// + protected virtual void HandleTimerTick() { + + // If the mouse has been released, stop scrolling. + // This is only necessary if the mouse is released outside of the control. + // If the mouse is released inside the control, we would receive a Drop event. + if ((this.IsLeftMouseButtonDown && (Control.MouseButtons & MouseButtons.Left) != MouseButtons.Left) || + (this.IsMiddleMouseButtonDown && (Control.MouseButtons & MouseButtons.Middle) != MouseButtons.Middle) || + (this.IsRightMouseButtonDown && (Control.MouseButtons & MouseButtons.Right) != MouseButtons.Right)) { + this.timer.Stop(); + this.Cleanup(); + return; + } + + // Auto scrolling will continue while the mouse is close to the ListView + const int GRACE_PERIMETER = 30; + + Point pt = this.ListView.PointToClient(Cursor.Position); + Rectangle r2 = this.ListView.ClientRectangle; + r2.Inflate(GRACE_PERIMETER, GRACE_PERIMETER); + if (r2.Contains(pt)) { + this.ListView.LowLevelScroll(0, this.scrollAmount); + } + } + + /// + /// When the mouse is at the given point, what should the target of the drop be? + /// + /// This method should update the DropTarget* members of the given arg block + /// + /// The mouse point, in client co-ordinates + protected virtual void CalculateDropTarget(OlvDropEventArgs args, Point pt) { + const int SMALL_VALUE = 3; + DropTargetLocation location = DropTargetLocation.None; + int targetIndex = -1; + int targetSubIndex = 0; + + if (this.CanDropOnBackground) + location = DropTargetLocation.Background; + + // Which item is the mouse over? + // If it is not over any item, it's over the background. + OlvListViewHitTestInfo info = this.ListView.OlvHitTest(pt.X, pt.Y); + if (info.Item != null && this.CanDropOnItem) { + location = DropTargetLocation.Item; + targetIndex = info.Item.Index; + if (info.SubItem != null && this.CanDropOnSubItem) + targetSubIndex = info.Item.SubItems.IndexOf(info.SubItem); + } + + // Check to see if the mouse is "between" rows. + // ("between" is somewhat loosely defined). + // If the view is Details or List, then "between" is considered vertically. + // If the view is SmallIcon, LargeIcon or Tile, then "between" is considered horizontally. + if (this.CanDropBetween && this.ListView.GetItemCount() > 0) { + + switch (this.ListView.View) { + case View.LargeIcon: + case View.Tile: + case View.SmallIcon: + // If the mouse is over an item, check to see if it is near the left or right edge. + if (info.Item != null) { + int delta = this.CanDropOnItem ? SMALL_VALUE : info.Item.Bounds.Width / 2; + if (pt.X <= info.Item.Bounds.Left + delta) { + targetIndex = info.Item.Index; + location = DropTargetLocation.LeftOfItem; + } else if (pt.X >= info.Item.Bounds.Right - delta) { + targetIndex = info.Item.Index; + location = DropTargetLocation.RightOfItem; + } + } else { + // Is there an item a little to the *right* of the mouse? + // If so, we say the drop point is *left* that item + int probeWidth = SMALL_VALUE * 2; + info = this.ListView.OlvHitTest(pt.X + probeWidth, pt.Y); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.LeftOfItem; + } else { + // Is there an item a little to the left of the mouse? + info = this.ListView.OlvHitTest(pt.X - probeWidth, pt.Y); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.RightOfItem; + } + } + } + break; + case View.Details: + case View.List: + // If the mouse is over an item, check to see if it is near the top or bottom + if (info.Item != null) { + int delta = this.CanDropOnItem ? SMALL_VALUE : this.ListView.RowHeightEffective / 2; + + if (pt.Y <= info.Item.Bounds.Top + delta) { + targetIndex = info.Item.Index; + location = DropTargetLocation.AboveItem; + } else if (pt.Y >= info.Item.Bounds.Bottom - delta) { + targetIndex = info.Item.Index; + location = DropTargetLocation.BelowItem; + } + } else { + // Is there an item a little below the mouse? + // If so, we say the drop point is above that row + info = this.ListView.OlvHitTest(pt.X, pt.Y + SMALL_VALUE); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.AboveItem; + } else { + // Is there an item a little above the mouse? + info = this.ListView.OlvHitTest(pt.X, pt.Y - SMALL_VALUE); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.BelowItem; + } + } + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + args.DropTargetLocation = location; + args.DropTargetIndex = targetIndex; + args.DropTargetSubItemIndex = targetSubIndex; + } + + /// + /// What sort of action is possible when the mouse is at the given point? + /// + /// + /// + /// + /// + /// + public virtual DragDropEffects CalculateDropAction(DragEventArgs args, Point pt) { + + this.CalculateDropTarget(this.dropEventArgs, pt); + + this.dropEventArgs.MouseLocation = pt; + this.dropEventArgs.InfoMessage = null; + this.dropEventArgs.Handled = false; + + if (this.dropEventArgs.SourceListView != null) { + this.dropEventArgs.TargetModel = this.ListView.GetModelObject(this.dropEventArgs.DropTargetIndex); + this.OnModelCanDrop(this.dropEventArgs); + } + + if (!this.dropEventArgs.Handled) + this.OnCanDrop(this.dropEventArgs); + + this.UpdateAfterCanDropEvent(this.dropEventArgs); + + return this.dropEventArgs.Effect; + } + + /// + /// Based solely on the state of the modifier keys, what drop operation should + /// be used? + /// + /// The drop operation that matches the state of the keys + public DragDropEffects CalculateStandardDropActionFromKeys() { + if (this.IsControlDown) { + if (this.IsShiftDown) + return DragDropEffects.Link; + else + return DragDropEffects.Copy; + } else { + return DragDropEffects.Move; + } + } + + /// + /// Should the listview be made to scroll when the mouse is at the given point? + /// + /// + protected virtual void CheckScrolling(Point pt) { + if (!this.AutoScroll) + return; + + Rectangle r = this.ListView.ContentRectangle; + int rowHeight = this.ListView.RowHeightEffective; + int close = rowHeight; + + // In Tile view, using the whole row height is too much + if (this.ListView.View == View.Tile) + close /= 2; + + if (pt.Y <= (r.Top + close)) { + // Scroll faster if the mouse is closer to the top + this.timer.Interval = ((pt.Y <= (r.Top + close / 2)) ? 100 : 350); + this.timer.Start(); + this.scrollAmount = -rowHeight; + } else { + if (pt.Y >= (r.Bottom - close)) { + this.timer.Interval = ((pt.Y >= (r.Bottom - close / 2)) ? 100 : 350); + this.timer.Start(); + this.scrollAmount = rowHeight; + } else { + this.timer.Stop(); + } + } + } + + /// + /// Update the state of our sink to reflect the information that + /// may have been written into the drop event args. + /// + /// + protected virtual void UpdateAfterCanDropEvent(OlvDropEventArgs args) { + this.DropTargetIndex = args.DropTargetIndex; + this.DropTargetLocation = args.DropTargetLocation; + this.DropTargetSubItemIndex = args.DropTargetSubItemIndex; + + if (this.Billboard != null) { + Point pt = args.MouseLocation; + pt.Offset(5, 5); + if (this.Billboard.Text != this.dropEventArgs.InfoMessage || this.Billboard.Location != pt) { + this.Billboard.Text = this.dropEventArgs.InfoMessage; + this.Billboard.Location = pt; + this.ListView.Invalidate(); + } + } + } + + #endregion + + #region Rendering + + /// + /// Draw the feedback that shows that the background is the target + /// + /// + /// + protected virtual void DrawFeedbackBackgroundTarget(Graphics g, Rectangle bounds) { + float penWidth = 12.0f; + Rectangle r = bounds; + r.Inflate((int)-penWidth / 2, (int)-penWidth / 2); + using (Pen p = new Pen(Color.FromArgb(128, this.FeedbackColor), penWidth)) { + using (GraphicsPath path = this.GetRoundedRect(r, 30.0f)) { + g.DrawPath(p, path); + } + } + } + + /// + /// Draw the feedback that shows that an item (or a subitem) is the target + /// + /// + /// + /// + /// DropTargetItem and DropTargetSubItemIndex tells what is the target + /// + protected virtual void DrawFeedbackItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + r.Inflate(1, 1); + float diameter = r.Height / 3; + using (GraphicsPath path = this.GetRoundedRect(r, diameter)) { + using (SolidBrush b = new SolidBrush(Color.FromArgb(48, this.FeedbackColor))) { + g.FillPath(b, path); + } + using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { + g.DrawPath(p, path); + } + } + } + + /// + /// Draw the feedback that shows the drop will occur before target + /// + /// + /// + protected virtual void DrawFeedbackAboveItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Left, r.Top, r.Right, r.Top); + } + + /// + /// Draw the feedback that shows the drop will occur after target + /// + /// + /// + protected virtual void DrawFeedbackBelowItemTarget(Graphics g, Rectangle bounds) + { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Left, r.Bottom, r.Right, r.Bottom); + } + + /// + /// Draw the feedback that shows the drop will occur to the left of target + /// + /// + /// + protected virtual void DrawFeedbackLeftOfItemTarget(Graphics g, Rectangle bounds) + { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Left, r.Top, r.Left, r.Bottom); + } + + /// + /// Draw the feedback that shows the drop will occur to the right of target + /// + /// + /// + protected virtual void DrawFeedbackRightOfItemTarget(Graphics g, Rectangle bounds) + { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Right, r.Top, r.Right, r.Bottom); + } + + /// + /// Return a GraphicPath that is round corner rectangle. + /// + /// + /// + /// + protected GraphicsPath GetRoundedRect(Rectangle rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + + return path; + } + + /// + /// Calculate the target rectangle when the given item (and possible subitem) + /// is the target of the drop. + /// + /// + /// + /// + protected virtual Rectangle CalculateDropTargetRectangle(OLVListItem item, int subItem) { + if (subItem > 0) + return item.SubItems[subItem].Bounds; + + Rectangle r = this.ListView.CalculateCellTextBounds(item, subItem); + + // Allow for indent + if (item.IndentCount > 0) { + int indentWidth = this.ListView.SmallImageSize.Width * item.IndentCount; + r.X += indentWidth; + r.Width -= indentWidth; + } + + return r; + } + + /// + /// Draw a "between items" line at the given co-ordinates + /// + /// + /// + /// + /// + /// + protected virtual void DrawBetweenLine(Graphics g, int x1, int y1, int x2, int y2) { + using (Brush b = new SolidBrush(this.FeedbackColor)) { + if (y1 == y2) { + // Put right and left arrow on a horizontal line + DrawClosedFigure(g, b, RightPointingArrow(x1, y1)); + DrawClosedFigure(g, b, LeftPointingArrow(x2, y2)); + } else { + // Put up and down arrows on a vertical line + DrawClosedFigure(g, b, DownPointingArrow(x1, y1)); + DrawClosedFigure(g, b, UpPointingArrow(x2, y2)); + } + } + + using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { + g.DrawLine(p, x1, y1, x2, y2); + } + } + + private const int ARROW_SIZE = 6; + + private static void DrawClosedFigure(Graphics g, Brush b, Point[] pts) { + using (GraphicsPath gp = new GraphicsPath()) { + gp.StartFigure(); + gp.AddLines(pts); + gp.CloseFigure(); + g.FillPath(b, gp); + } + } + + private static Point[] RightPointingArrow(int x, int y) { + return new Point[] { + new Point(x, y - ARROW_SIZE), + new Point(x, y + ARROW_SIZE), + new Point(x + ARROW_SIZE, y) + }; + } + + private static Point[] LeftPointingArrow(int x, int y) { + return new Point[] { + new Point(x, y - ARROW_SIZE), + new Point(x, y + ARROW_SIZE), + new Point(x - ARROW_SIZE, y) + }; + } + + private static Point[] DownPointingArrow(int x, int y) { + return new Point[] { + new Point(x - ARROW_SIZE, y), + new Point(x + ARROW_SIZE, y), + new Point(x, y + ARROW_SIZE) + }; + } + + private static Point[] UpPointingArrow(int x, int y) { + return new Point[] { + new Point(x - ARROW_SIZE, y), + new Point(x + ARROW_SIZE, y), + new Point(x, y - ARROW_SIZE) + }; + } + + #endregion + + private Timer timer; + private int scrollAmount; + private bool originalFullRowSelect; + private ModelDropEventArgs dropEventArgs; + } + + /// + /// This drop sink allows items within the same list to be rearranged, + /// as well as allowing items to be dropped from other lists. + /// + /// + /// + /// This class can only be used on plain ObjectListViews and FastObjectListViews. + /// The other flavours have no way to implement the insert operation that is required. + /// + /// + /// This class does not work with grouping. + /// + /// + /// This class works when the OLV is sorted, but it is up to the programmer + /// to decide what rearranging such lists "means". Example: if the control is sorting + /// students by academic grade, and the user drags a "Fail" grade student up amongst the "A+" + /// students, it is the responsibility of the programmer to makes the appropriate changes + /// to the model and redraw/rebuild the control so that the users action makes sense. + /// + /// + /// Users of this class should listen for the CanDrop event to decide + /// if models from another OLV can be moved to OLV under this sink. + /// + /// + public class RearrangingDropSink : SimpleDropSink + { + /// + /// Create a RearrangingDropSink + /// + public RearrangingDropSink() { + this.CanDropBetween = true; + this.CanDropOnBackground = true; + this.CanDropOnItem = false; + } + + /// + /// Create a RearrangingDropSink + /// + /// + public RearrangingDropSink(bool acceptDropsFromOtherLists) + : this() { + this.AcceptExternal = acceptDropsFromOtherLists; + } + + /// + /// Trigger OnModelCanDrop + /// + /// + protected override void OnModelCanDrop(ModelDropEventArgs args) { + base.OnModelCanDrop(args); + + if (args.Handled) + return; + + args.Effect = DragDropEffects.Move; + + // Don't allow drops from other list, if that's what's configured + if (!this.AcceptExternal && args.SourceListView != this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + args.InfoMessage = "This list doesn't accept drops from other lists"; + } + + // If we are rearranging the same list, don't allow drops on the background + if (args.DropTargetLocation == DropTargetLocation.Background && args.SourceListView == this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + } + } + + /// + /// Trigger OnModelDropped + /// + /// + protected override void OnModelDropped(ModelDropEventArgs args) { + base.OnModelDropped(args); + + if (!args.Handled) + this.RearrangeModels(args); + } + + /// + /// Do the work of processing the dropped items + /// + /// + public virtual void RearrangeModels(ModelDropEventArgs args) { + switch (args.DropTargetLocation) { + case DropTargetLocation.AboveItem: + case DropTargetLocation.LeftOfItem: + this.ListView.MoveObjects(args.DropTargetIndex, args.SourceModels); + break; + case DropTargetLocation.BelowItem: + case DropTargetLocation.RightOfItem: + this.ListView.MoveObjects(args.DropTargetIndex + 1, args.SourceModels); + break; + case DropTargetLocation.Background: + this.ListView.AddObjects(args.SourceModels); + break; + default: + return; + } + + if (args.SourceListView != this.ListView) { + args.SourceListView.RemoveObjects(args.SourceModels); + } + + // Some views have to be "encouraged" to show the changes + switch (this.ListView.View) { + case View.LargeIcon: + case View.SmallIcon: + case View.Tile: + this.ListView.BuildList(); + break; + } + } + } + + /// + /// When a drop sink needs to know if something can be dropped, or + /// to notify that a drop has occurred, it uses an instance of this class. + /// + public class OlvDropEventArgs : EventArgs + { + /// + /// Create a OlvDropEventArgs + /// + public OlvDropEventArgs() { + } + + #region Data Properties + + /// + /// Get the original drag-drop event args + /// + public DragEventArgs DragEventArgs + { + get { return this.dragEventArgs; } + internal set { this.dragEventArgs = value; } + } + private DragEventArgs dragEventArgs; + + /// + /// Get the data object that is being dragged + /// + public object DataObject + { + get { return this.dataObject; } + internal set { this.dataObject = value; } + } + private object dataObject; + + /// + /// Get the drop sink that originated this event + /// + public SimpleDropSink DropSink { + get { return this.dropSink; } + internal set { this.dropSink = value; } + } + private SimpleDropSink dropSink; + + /// + /// Get or set the index of the item that is the target of the drop + /// + public int DropTargetIndex { + get { return dropTargetIndex; } + set { this.dropTargetIndex = value; } + } + private int dropTargetIndex = -1; + + /// + /// Get or set the location of the target of the drop + /// + public DropTargetLocation DropTargetLocation { + get { return dropTargetLocation; } + set { this.dropTargetLocation = value; } + } + private DropTargetLocation dropTargetLocation; + + /// + /// Get or set the index of the subitem that is the target of the drop + /// + public int DropTargetSubItemIndex { + get { return dropTargetSubItemIndex; } + set { this.dropTargetSubItemIndex = value; } + } + private int dropTargetSubItemIndex = -1; + + /// + /// Get the item that is the target of the drop + /// + public OLVListItem DropTargetItem { + get { + return this.ListView.GetItem(this.DropTargetIndex); + } + set { + if (value == null) + this.DropTargetIndex = -1; + else + this.DropTargetIndex = value.Index; + } + } + + /// + /// Get or set the drag effect that should be used for this operation + /// + public DragDropEffects Effect { + get { return this.effect; } + set { this.effect = value; } + } + private DragDropEffects effect; + + /// + /// Get or set if this event was handled. No further processing will be done for a handled event. + /// + public bool Handled { + get { return this.handled; } + set { this.handled = value; } + } + private bool handled; + + /// + /// Get or set the feedback message for this operation + /// + /// + /// If this is not null, it will be displayed as a feedback message + /// during the drag. + /// + public string InfoMessage { + get { return this.infoMessage; } + set { this.infoMessage = value; } + } + private string infoMessage; + + /// + /// Get the ObjectListView that is being dropped on + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Get the location of the mouse (in target ListView co-ords) + /// + public Point MouseLocation { + get { return this.mouseLocation; } + internal set { this.mouseLocation = value; } + } + private Point mouseLocation; + + /// + /// Get the drop action indicated solely by the state of the modifier keys + /// + public DragDropEffects StandardDropActionFromKeys { + get { + return this.DropSink.CalculateStandardDropActionFromKeys(); + } + } + + #endregion + } + + /// + /// These events are triggered when the drag source is an ObjectListView. + /// + public class ModelDropEventArgs : OlvDropEventArgs + { + /// + /// Create a ModelDropEventArgs + /// + public ModelDropEventArgs() + { + } + + /// + /// Gets the model objects that are being dragged. + /// + public IList SourceModels { + get { return this.dragModels; } + internal set { + this.dragModels = value; + TreeListView tlv = this.SourceListView as TreeListView; + if (tlv != null) { + foreach (object model in this.SourceModels) { + object parent = tlv.GetParent(model); + if (!toBeRefreshed.Contains(parent)) + toBeRefreshed.Add(parent); + } + } + } + } + private IList dragModels; + private ArrayList toBeRefreshed = new ArrayList(); + + /// + /// Gets the ObjectListView that is the source of the dragged objects. + /// + public ObjectListView SourceListView { + get { return this.sourceListView; } + internal set { this.sourceListView = value; } + } + private ObjectListView sourceListView; + + /// + /// Get the model object that is being dropped upon. + /// + /// This is only value for TargetLocation == Item + public object TargetModel { + get { return this.targetModel; } + internal set { this.targetModel = value; } + } + private object targetModel; + + /// + /// Refresh all the objects involved in the operation + /// + public void RefreshObjects() { + + toBeRefreshed.AddRange(this.SourceModels); + TreeListView tlv = this.SourceListView as TreeListView; + if (tlv == null) + this.SourceListView.RefreshObjects(toBeRefreshed); + else + tlv.RebuildAll(true); + + TreeListView tlv2 = this.ListView as TreeListView; + if (tlv2 == null) + this.ListView.RefreshObject(this.TargetModel); + else + tlv2.RebuildAll(true); + } + } +} diff --git a/ObjectListView/DragDrop/OLVDataObject.cs b/ObjectListView/DragDrop/OLVDataObject.cs new file mode 100644 index 00000000..116861ba --- /dev/null +++ b/ObjectListView/DragDrop/OLVDataObject.cs @@ -0,0 +1,185 @@ +/* + * OLVDataObject.cs - An OLE DataObject that knows how to convert rows of an OLV to text and HTML + * + * Author: Phillip Piper + * Date: 2011-03-29 3:34PM + * + * Change log: + * v2.8 + * 2014-05-02 JPP - When the listview is completely empty, don't try to set CSV text in the clipboard. + * v2.6 + * 2012-08-08 JPP - Changed to use OLVExporter. + * - Added CSV to formats exported to Clipboard + * v2.4 + * 2011-03-29 JPP - Initial version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Windows.Forms; + +namespace BrightIdeasSoftware { + + /// + /// A data transfer object that knows how to transform a list of model + /// objects into a text and HTML representation. + /// + public class OLVDataObject : DataObject { + #region Life and death + + /// + /// Create a data object from the selected objects in the given ObjectListView + /// + /// The source of the data object + public OLVDataObject(ObjectListView olv) + : this(olv, olv.SelectedObjects) { + } + + /// + /// Create a data object which operates on the given model objects + /// in the given ObjectListView + /// + /// The source of the data object + /// The model objects to be put into the data object + public OLVDataObject(ObjectListView olv, IList modelObjects) { + this.objectListView = olv; + this.modelObjects = modelObjects; + this.includeHiddenColumns = olv.IncludeHiddenColumnsInDataTransfer; + this.includeColumnHeaders = olv.IncludeColumnHeadersInCopy; + this.CreateTextFormats(); + } + + #endregion + + #region Properties + + /// + /// Gets or sets whether hidden columns will also be included in the text + /// and HTML representation. If this is false, only visible columns will + /// be included. + /// + public bool IncludeHiddenColumns { + get { return includeHiddenColumns; } + } + private readonly bool includeHiddenColumns; + + /// + /// Gets or sets whether column headers will also be included in the text + /// and HTML representation. + /// + public bool IncludeColumnHeaders { + get { return includeColumnHeaders; } + } + private readonly bool includeColumnHeaders; + + /// + /// Gets the ObjectListView that is being used as the source of the data + /// + public ObjectListView ListView { + get { return objectListView; } + } + private readonly ObjectListView objectListView; + + /// + /// Gets the model objects that are to be placed in the data object + /// + public IList ModelObjects { + get { return modelObjects; } + } + private readonly IList modelObjects; + + #endregion + + /// + /// Put a text and HTML representation of our model objects + /// into the data object. + /// + public void CreateTextFormats() { + + OLVExporter exporter = this.CreateExporter(); + + // Put both the text and html versions onto the clipboard. + // For some reason, SetText() with UnicodeText doesn't set the basic CF_TEXT format, + // but using SetData() does. + //this.SetText(sbText.ToString(), TextDataFormat.UnicodeText); + this.SetData(exporter.ExportTo(OLVExporter.ExportFormat.TabSeparated)); + string exportTo = exporter.ExportTo(OLVExporter.ExportFormat.CSV); + if (!String.IsNullOrEmpty(exportTo)) + this.SetText(exportTo, TextDataFormat.CommaSeparatedValue); + this.SetText(ConvertToHtmlFragment(exporter.ExportTo(OLVExporter.ExportFormat.HTML)), TextDataFormat.Html); + } + + /// + /// Create an exporter for the data contained in this object + /// + /// + protected OLVExporter CreateExporter() { + OLVExporter exporter = new OLVExporter(this.ListView); + exporter.IncludeColumnHeaders = this.IncludeColumnHeaders; + exporter.IncludeHiddenColumns = this.IncludeHiddenColumns; + exporter.ModelObjects = this.ModelObjects; + return exporter; + } + + /// + /// Make a HTML representation of our model objects + /// + [Obsolete("Use OLVExporter directly instead", false)] + public string CreateHtml() { + OLVExporter exporter = this.CreateExporter(); + return exporter.ExportTo(OLVExporter.ExportFormat.HTML); + } + + /// + /// Convert the fragment of HTML into the Clipboards HTML format. + /// + /// The HTML format is found here http://msdn2.microsoft.com/en-us/library/aa767917.aspx + /// + /// The HTML to put onto the clipboard. It must be valid HTML! + /// A string that can be put onto the clipboard and will be recognised as HTML + private string ConvertToHtmlFragment(string fragment) { + // Minimal implementation of HTML clipboard format + const string SOURCE = "http://www.codeproject.com/Articles/16009/A-Much-Easier-to-Use-ListView"; + + const String MARKER_BLOCK = + "Version:1.0\r\n" + + "StartHTML:{0,8}\r\n" + + "EndHTML:{1,8}\r\n" + + "StartFragment:{2,8}\r\n" + + "EndFragment:{3,8}\r\n" + + "StartSelection:{2,8}\r\n" + + "EndSelection:{3,8}\r\n" + + "SourceURL:{4}\r\n" + + "{5}"; + + int prefixLength = String.Format(MARKER_BLOCK, 0, 0, 0, 0, SOURCE, "").Length; + + const String DEFAULT_HTML_BODY = + "" + + "{0}"; + + string html = String.Format(DEFAULT_HTML_BODY, fragment); + int startFragment = prefixLength + html.IndexOf(fragment, StringComparison.Ordinal); + int endFragment = startFragment + fragment.Length; + + return String.Format(MARKER_BLOCK, prefixLength, prefixLength + html.Length, startFragment, endFragment, SOURCE, html); + } + } +} diff --git a/ObjectListView/FastDataListView.cs b/ObjectListView/FastDataListView.cs new file mode 100644 index 00000000..8b30d2b1 --- /dev/null +++ b/ObjectListView/FastDataListView.cs @@ -0,0 +1,169 @@ +/* + * FastDataListView - A data bindable listview that has the speed of a virtual list + * + * Author: Phillip Piper + * Date: 22/09/2010 8:11 AM + * + * Change log: + * 2015-02-02 JPP - Made Unfreezing more efficient by removing a redundant BuildList() call + * v2.6 + * 2010-09-22 JPP - Initial version + * + * Copyright (C) 2006-2015 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.ComponentModel; +using System.Windows.Forms; +using System.Drawing.Design; + +namespace BrightIdeasSoftware +{ + /// + /// A FastDataListView virtualizes the display of data from a DataSource. It operates on + /// DataSets and DataTables in the same way as a DataListView, but does so much more efficiently. + /// + /// + /// + /// A FastDataListView still has to load all its data from the DataSource. If you have SQL statement + /// that returns 1 million rows, all 1 million rows will still need to read from the database. + /// However, once the rows are loaded, the FastDataListView will only build rows as they are displayed. + /// + /// + public class FastDataListView : FastObjectListView + { + /// + /// + /// + /// + protected override void Dispose(bool disposing) + { + if (this.adapter != null) { + this.adapter.Dispose(); + this.adapter = null; + } + + base.Dispose(disposing); + } + + #region Public Properties + + /// + /// Gets or sets whether or not columns will be automatically generated to show the + /// columns when the DataSource is set. + /// + /// This must be set before the DataSource is set. It has no effect afterwards. + [Category("Data"), + Description("Should the control automatically generate columns from the DataSource"), + DefaultValue(true)] + public bool AutoGenerateColumns + { + get { return this.Adapter.AutoGenerateColumns; } + set { this.Adapter.AutoGenerateColumns = value; } + } + + /// + /// Get or set the VirtualListDataSource that will be displayed in this list view. + /// + /// The VirtualListDataSource should implement either , , + /// or . Some common examples are the following types of objects: + /// + /// + /// + /// + /// + /// + /// + /// When binding to a list container (i.e. one that implements the + /// interface, such as ) + /// you must also set the property in order + /// to identify which particular list you would like to display. You + /// may also set the property even when + /// VirtualListDataSource refers to a list, since can + /// also be used to navigate relations between lists. + /// + [Category("Data"), + TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")] + public virtual Object DataSource { + get { return this.Adapter.DataSource; } + set { this.Adapter.DataSource = value; } + } + + /// + /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data. + /// + /// If the data source is not a DataSet or DataViewManager, this property has no effect + [Category("Data"), + Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design", typeof(UITypeEditor)), + DefaultValue("")] + public virtual string DataMember { + get { return this.Adapter.DataMember; } + set { this.Adapter.DataMember = value; } + } + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the DataSourceAdaptor that does the bulk of the work needed + /// for data binding. + /// + protected DataSourceAdapter Adapter { + get { + if (adapter == null) + adapter = this.CreateDataSourceAdapter(); + return adapter; + } + set { adapter = value; } + } + private DataSourceAdapter adapter; + + #endregion + + #region Implementation + + /// + /// Create the DataSourceAdapter that this control will use. + /// + /// A DataSourceAdapter configured for this list + /// Subclasses should override this to create their + /// own specialized adapters + protected virtual DataSourceAdapter CreateDataSourceAdapter() { + return new DataSourceAdapter(this); + } + + /// + /// Change the Unfreeze behaviour + /// + protected override void DoUnfreeze() + { + + // Copied from base method, but we don't need to BuildList() since we know that our + // data adaptor is going to do that immediately after this method exits. + this.EndUpdate(); + this.ResizeFreeSpaceFillingColumns(); + // this.BuildList(); + } + + #endregion + } +} diff --git a/ObjectListView/FastObjectListView.cs b/ObjectListView/FastObjectListView.cs new file mode 100644 index 00000000..0c5fe302 --- /dev/null +++ b/ObjectListView/FastObjectListView.cs @@ -0,0 +1,422 @@ +/* + * FastObjectListView - A listview that behaves like an ObjectListView but has the speed of a virtual list + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * 2014-10-15 JPP - Fire Filter event when applying filters + * v2.8 + * 2012-06-11 JPP - Added more efficient version of FilteredObjects + * v2.5.1 + * 2011-04-25 JPP - Fixed problem with removing objects from filtered or sorted list + * v2.4 + * 2010-04-05 JPP - Added filtering + * v2.3 + * 2009-08-27 JPP - Added GroupingStrategy + * - Added optimized Objects property + * v2.2.1 + * 2009-01-07 JPP - Made all public and protected methods virtual + * 2008-09-27 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A FastObjectListView trades function for speed. + /// + /// + /// On my mid-range laptop, this view builds a list of 10,000 objects in 0.1 seconds, + /// as opposed to a normal ObjectListView which takes 10-15 seconds. Lists of up to 50,000 items should be + /// able to be handled with sub-second response times even on low end machines. + /// + /// A FastObjectListView is implemented as a virtual list with many of the virtual modes limits (e.g. no sorting) + /// fixed through coding. There are some functions that simply cannot be provided. Specifically, a FastObjectListView cannot: + /// + /// use Tile view + /// show groups on XP + /// + /// + /// + public class FastObjectListView : VirtualObjectListView + { + /// + /// Make a FastObjectListView + /// + public FastObjectListView() { + this.VirtualListDataSource = new FastObjectListDataSource(this); + this.GroupingStrategy = new FastListGroupingStrategy(); + } + + /// + /// Gets the collection of objects that survive any filtering that may be in place. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable FilteredObjects { + get { + // This is much faster than the base method + return ((FastObjectListDataSource)this.VirtualListDataSource).FilteredObjectList; + } + } + + /// + /// Get/set the collection of objects that this list will show + /// + /// + /// + /// The contents of the control will be updated immediately after setting this property. + /// + /// This method preserves selection, if possible. Use SetObjects() if + /// you do not want to preserve the selection. Preserving selection is the slowest part of this + /// code and performance is O(n) where n is the number of selected rows. + /// This method is not thread safe. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable Objects { + get { + // This is much faster than the base method + return ((FastObjectListDataSource)this.VirtualListDataSource).ObjectList; + } + set { base.Objects = value; } + } + + /// + /// Move the given collection of objects to the given index. + /// + /// This operation only makes sense on non-grouped ObjectListViews. + /// + /// + public override void MoveObjects(int index, ICollection modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate() { this.MoveObjects(index, modelObjects); }); + return; + } + + // If any object that is going to be moved is before the point where the insertion + // will occur, then we have to reduce the location of our insertion point + int displacedObjectCount = 0; + foreach (object modelObject in modelObjects) { + int i = this.IndexOf(modelObject); + if (i >= 0 && i <= index) + displacedObjectCount++; + } + index -= displacedObjectCount; + + this.BeginUpdate(); + try { + this.RemoveObjects(modelObjects); + this.InsertObjects(index, modelObjects); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Remove any sorting and revert to the given order of the model objects + /// + /// To be really honest, Unsort() doesn't work on FastObjectListViews since + /// the original ordering of model objects is lost when Sort() is called. So this method + /// effectively just turns off sorting. + public override void Unsort() { + this.ShowGroups = false; + this.PrimarySortColumn = null; + this.PrimarySortOrder = SortOrder.None; + this.SetObjects(this.Objects); + } + } + + /// + /// Provide a data source for a FastObjectListView + /// + /// + /// This class isn't intended to be used directly, but it is left as a public + /// class just in case someone wants to subclass it. + /// + public class FastObjectListDataSource : AbstractVirtualListDataSource + { + /// + /// Create a FastObjectListDataSource + /// + /// + public FastObjectListDataSource(FastObjectListView listView) + : base(listView) { + } + + #region IVirtualListDataSource Members + + /// + /// Get n'th object + /// + /// + /// + public override object GetNthObject(int n) { + if (n >= 0 && n < this.filteredObjectList.Count) + return this.filteredObjectList[n]; + + return null; + } + + /// + /// How many items are in the data source + /// + /// + public override int GetObjectCount() { + return this.filteredObjectList.Count; + } + + /// + /// Get the index of the given model + /// + /// + /// + public override int GetObjectIndex(object model) { + int index; + + if (model != null && this.objectsToIndexMap.TryGetValue(model, out index)) + return index; + + return -1; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public override int SearchText(string text, int first, int last, OLVColumn column) { + if (first <= last) { + for (int i = first; i <= last; i++) { + string data = column.GetStringValue(this.listView.GetNthItemInDisplayOrder(i).RowObject); + if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } else { + for (int i = first; i >= last; i--) { + string data = column.GetStringValue(this.listView.GetNthItemInDisplayOrder(i).RowObject); + if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } + + return -1; + } + + /// + /// + /// + /// + /// + public override void Sort(OLVColumn column, SortOrder sortOrder) { + if (sortOrder != SortOrder.None) { + ModelObjectComparer comparer = new ModelObjectComparer(column, sortOrder, this.listView.SecondarySortColumn, this.listView.SecondarySortOrder); + this.fullObjectList.Sort(comparer); + this.filteredObjectList.Sort(comparer); + } + this.RebuildIndexMap(); + } + + /// + /// + /// + /// + public override void AddObjects(ICollection modelObjects) { + foreach (object modelObject in modelObjects) { + if (modelObject != null) + this.fullObjectList.Add(modelObject); + } + this.FilterObjects(); + this.RebuildIndexMap(); + } + + /// + /// + /// + /// + /// + public override void InsertObjects(int index, ICollection modelObjects) { + this.fullObjectList.InsertRange(index, modelObjects); + this.FilterObjects(); + this.RebuildIndexMap(); + } + + /// + /// Remove the given collection of models from this source. + /// + /// + public override void RemoveObjects(ICollection modelObjects) { + + // We have to unselect any object that is about to be deleted + List indicesToRemove = new List(); + foreach (object modelObject in modelObjects) { + int i = this.GetObjectIndex(modelObject); + if (i >= 0) + indicesToRemove.Add(i); + } + + // Sort the indices from highest to lowest so that we + // remove latter ones before earlier ones. In this way, the + // indices of the rows doesn't change after the deletes. + indicesToRemove.Sort(); + indicesToRemove.Reverse(); + + foreach (int i in indicesToRemove) + this.listView.SelectedIndices.Remove(i); + + // Remove the objects from the unfiltered list + foreach (object modelObject in modelObjects) + this.fullObjectList.Remove(modelObject); + + this.FilterObjects(); + this.RebuildIndexMap(); + } + + /// + /// + /// + /// + public override void SetObjects(IEnumerable collection) { + ArrayList newObjects = ObjectListView.EnumerableToArray(collection, true); + + this.fullObjectList = newObjects; + this.FilterObjects(); + this.RebuildIndexMap(); + } + + /// + /// Update/replace the nth object with the given object + /// + /// + /// + public override void UpdateObject(int index, object modelObject) { + if (index < 0 || index >= this.filteredObjectList.Count) + return; + + int i = this.fullObjectList.IndexOf(this.filteredObjectList[index]); + if (i < 0) + return; + + if (ReferenceEquals(this.fullObjectList[i], modelObject)) + return; + + this.fullObjectList[i] = modelObject; + this.filteredObjectList[index] = modelObject; + this.objectsToIndexMap[modelObject] = index; + } + + private ArrayList fullObjectList = new ArrayList(); + private ArrayList filteredObjectList = new ArrayList(); + private IModelFilter modelFilter; + private IListFilter listFilter; + + #endregion + + #region IFilterableDataSource Members + + /// + /// Apply the given filters to this data source. One or both may be null. + /// + /// + /// + public override void ApplyFilters(IModelFilter iModelFilter, IListFilter iListFilter) { + this.modelFilter = iModelFilter; + this.listFilter = iListFilter; + this.SetObjects(this.fullObjectList); + } + + #endregion + + #region Implementation + + /// + /// Gets the full list of objects being used for this fast list. + /// This list is unfiltered. + /// + public ArrayList ObjectList { + get { return fullObjectList; } + } + + /// + /// Gets the list of objects from ObjectList which survive any installed filters. + /// + public ArrayList FilteredObjectList { + get { return filteredObjectList; } + } + + /// + /// Rebuild the map that remembers which model object is displayed at which line + /// + protected void RebuildIndexMap() { + this.objectsToIndexMap.Clear(); + for (int i = 0; i < this.filteredObjectList.Count; i++) + this.objectsToIndexMap[this.filteredObjectList[i]] = i; + } + readonly Dictionary objectsToIndexMap = new Dictionary(); + + /// + /// Build our filtered list from our full list. + /// + protected void FilterObjects() { + + // If this list isn't filtered, we don't need to do anything else + if (!this.listView.UseFiltering) { + this.filteredObjectList = new ArrayList(this.fullObjectList); + return; + } + + // Tell the world to filter the objects. If they do so, don't do anything else + // ReSharper disable PossibleMultipleEnumeration + FilterEventArgs args = new FilterEventArgs(this.fullObjectList); + this.listView.OnFilter(args); + if (args.FilteredObjects != null) { + this.filteredObjectList = ObjectListView.EnumerableToArray(args.FilteredObjects, false); + return; + } + + IEnumerable objects = (this.listFilter == null) ? + this.fullObjectList : this.listFilter.Filter(this.fullObjectList); + + // Apply the object filter if there is one + if (this.modelFilter == null) { + this.filteredObjectList = ObjectListView.EnumerableToArray(objects, false); + } else { + this.filteredObjectList = new ArrayList(); + foreach (object model in objects) { + if (this.modelFilter.Filter(model)) + this.filteredObjectList.Add(model); + } + } + } + + #endregion + } + +} diff --git a/ObjectListView/Filtering/Cluster.cs b/ObjectListView/Filtering/Cluster.cs new file mode 100644 index 00000000..f90a84d9 --- /dev/null +++ b/ObjectListView/Filtering/Cluster.cs @@ -0,0 +1,125 @@ +/* + * Cluster - Implements a simple cluster + * + * Author: Phillip Piper + * Date: 3-March-2011 10:53 pm + * + * Change log: + * 2011-03-03 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware { + + /// + /// Concrete implementation of the ICluster interface. + /// + public class Cluster : ICluster { + + #region Life and death + + /// + /// Create a cluster + /// + /// The key for the cluster + public Cluster(object key) { + this.Count = 1; + this.ClusterKey = key; + } + + #endregion + + #region Public overrides + + /// + /// Return a string representation of this cluster + /// + /// + public override string ToString() { + return this.DisplayLabel ?? "[empty]"; + } + + #endregion + + #region Implementation of ICluster + + /// + /// Gets or sets how many items belong to this cluster + /// + public int Count { + get { return count; } + set { count = value; } + } + private int count; + + /// + /// Gets or sets the label that will be shown to the user to represent + /// this cluster + /// + public string DisplayLabel { + get { return displayLabel; } + set { displayLabel = value; } + } + private string displayLabel; + + /// + /// Gets or sets the actual data object that all members of this cluster + /// have commonly returned. + /// + public object ClusterKey { + get { return clusterKey; } + set { clusterKey = value; } + } + private object clusterKey; + + #endregion + + #region Implementation of IComparable + + /// + /// Return an indication of the ordering between this object and the given one + /// + /// + /// + public int CompareTo(object other) { + if (other == null || other == System.DBNull.Value) + return 1; + + ICluster otherCluster = other as ICluster; + if (otherCluster == null) + return 1; + + string keyAsString = this.ClusterKey as string; + if (keyAsString != null) + return String.Compare(keyAsString, otherCluster.ClusterKey as string, StringComparison.CurrentCultureIgnoreCase); + + IComparable keyAsComparable = this.ClusterKey as IComparable; + if (keyAsComparable != null) + return keyAsComparable.CompareTo(otherCluster.ClusterKey); + + return -1; + } + + #endregion + } +} diff --git a/ObjectListView/Filtering/ClusteringStrategy.cs b/ObjectListView/Filtering/ClusteringStrategy.cs new file mode 100644 index 00000000..b2380f0a --- /dev/null +++ b/ObjectListView/Filtering/ClusteringStrategy.cs @@ -0,0 +1,189 @@ +/* + * ClusteringStrategy - Implements a simple clustering strategy + * + * Author: Phillip Piper + * Date: 3-March-2011 10:53 pm + * + * Change log: + * 2011-03-03 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Text; + +namespace BrightIdeasSoftware { + + /// + /// This class provides a useful base implementation of a clustering + /// strategy where the clusters are grouped around the value of a given column. + /// + public class ClusteringStrategy : IClusteringStrategy { + + #region Static properties + + /// + /// This field is the text that will be shown to the user when a cluster + /// key is null. It is exposed so it can be localized. + /// + static public string NULL_LABEL = "[null]"; + + /// + /// This field is the text that will be shown to the user when a cluster + /// key is empty (i.e. a string of zero length). It is exposed so it can be localized. + /// + static public string EMPTY_LABEL = "[empty]"; + + /// + /// Gets or sets the format that will be used by default for clusters that only + /// contain 1 item. The format string must accept two placeholders: + /// - {0} is the cluster key converted to a string + /// - {1} is the number of items in the cluster (always 1 in this case) + /// + static public string DefaultDisplayLabelFormatSingular { + get { return defaultDisplayLabelFormatSingular; } + set { defaultDisplayLabelFormatSingular = value; } + } + static private string defaultDisplayLabelFormatSingular = "{0} ({1} item)"; + + /// + /// Gets or sets the format that will be used by default for clusters that + /// contain 0 or two or more items. The format string must accept two placeholders: + /// - {0} is the cluster key converted to a string + /// - {1} is the number of items in the cluster + /// + static public string DefaultDisplayLabelFormatPlural { + get { return defaultDisplayLabelFormatPural; } + set { defaultDisplayLabelFormatPural = value; } + } + static private string defaultDisplayLabelFormatPural = "{0} ({1} items)"; + + #endregion + + #region Life and death + + /// + /// Create a clustering strategy + /// + public ClusteringStrategy() { + this.DisplayLabelFormatSingular = DefaultDisplayLabelFormatSingular; + this.DisplayLabelFormatPlural = DefaultDisplayLabelFormatPlural; + } + + #endregion + + #region Public properties + + /// + /// Gets or sets the column upon which this strategy is operating + /// + public OLVColumn Column { + get { return column; } + set { column = value; } + } + private OLVColumn column; + + /// + /// Gets or sets the format that will be used when the cluster + /// contains only 1 item. The format string must accept two placeholders: + /// - {0} is the cluster key converted to a string + /// - {1} is the number of items in the cluster (always 1 in this case) + /// + /// If this is not set, the value from + /// ClusteringStrategy.DefaultDisplayLabelFormatSingular will be used + public string DisplayLabelFormatSingular { + get { return displayLabelFormatSingular; } + set { displayLabelFormatSingular = value; } + } + private string displayLabelFormatSingular; + + /// + /// Gets or sets the format that will be used when the cluster + /// contains 0 or two or more items. The format string must accept two placeholders: + /// - {0} is the cluster key converted to a string + /// - {1} is the number of items in the cluster + /// + /// If this is not set, the value from + /// ClusteringStrategy.DefaultDisplayLabelFormatPlural will be used + public string DisplayLabelFormatPlural { + get { return displayLabelFormatPural; } + set { displayLabelFormatPural = value; } + } + private string displayLabelFormatPural; + + #endregion + + #region ICluster implementation + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// + /// + virtual public object GetClusterKey(object model) { + return this.Column.GetValue(model); + } + + /// + /// Create a cluster to hold the given cluster key + /// + /// + /// + virtual public ICluster CreateCluster(object clusterKey) { + return new Cluster(clusterKey); + } + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + virtual public string GetClusterDisplayLabel(ICluster cluster) { + string s = this.Column.ValueToString(cluster.ClusterKey) ?? NULL_LABEL; + if (String.IsNullOrEmpty(s)) + s = EMPTY_LABEL; + return this.ApplyDisplayFormat(cluster, s); + } + + /// + /// Create a filter that will include only model objects that + /// match one or more of the given values. + /// + /// + /// + virtual public IModelFilter CreateFilter(IList valuesChosenForFiltering) { + return new OneOfFilter(this.GetClusterKey, valuesChosenForFiltering); + } + + /// + /// Create a label that combines the string representation of the cluster + /// key with a format string that holds an "X [N items in cluster]" type layout. + /// + /// + /// + /// + virtual protected string ApplyDisplayFormat(ICluster cluster, string s) { + string format = (cluster.Count == 1) ? this.DisplayLabelFormatSingular : this.DisplayLabelFormatPlural; + return String.IsNullOrEmpty(format) ? s : String.Format(format, s, cluster.Count); + } + + #endregion + } +} diff --git a/ObjectListView/Filtering/ClustersFromGroupsStrategy.cs b/ObjectListView/Filtering/ClustersFromGroupsStrategy.cs new file mode 100644 index 00000000..ca95ecf0 --- /dev/null +++ b/ObjectListView/Filtering/ClustersFromGroupsStrategy.cs @@ -0,0 +1,70 @@ +/* + * ClusteringStrategy - Implements a simple clustering strategy + * + * Author: Phillip Piper + * Date: 1-April-2011 8:12am + * + * Change log: + * 2011-04-01 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware { + + /// + /// This class calculates clusters from the groups that the column uses. + /// + /// + /// + /// This is the default strategy for all non-date, filterable columns. + /// + /// + /// This class does not strictly mimic the groups created by the given column. + /// In particular, if the programmer changes the default grouping technique + /// by listening for grouping events, this class will not mimic that behaviour. + /// + /// + public class ClustersFromGroupsStrategy : ClusteringStrategy { + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// + /// + public override object GetClusterKey(object model) { + return this.Column.GetGroupKey(model); + } + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + public override string GetClusterDisplayLabel(ICluster cluster) { + string s = this.Column.ConvertGroupKeyToTitle(cluster.ClusterKey); + if (String.IsNullOrEmpty(s)) + s = EMPTY_LABEL; + return this.ApplyDisplayFormat(cluster, s); + } + } +} diff --git a/ObjectListView/Filtering/DateTimeClusteringStrategy.cs b/ObjectListView/Filtering/DateTimeClusteringStrategy.cs new file mode 100644 index 00000000..e0a864b5 --- /dev/null +++ b/ObjectListView/Filtering/DateTimeClusteringStrategy.cs @@ -0,0 +1,187 @@ +/* + * DateTimeClusteringStrategy - A strategy to cluster objects by a date time + * + * Author: Phillip Piper + * Date: 30-March-2011 9:40am + * + * Change log: + * 2011-03-30 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Globalization; + +namespace BrightIdeasSoftware { + + /// + /// This enum is used to indicate various portions of a datetime + /// + [Flags] + public enum DateTimePortion { + /// + /// Year + /// + Year = 0x01, + + /// + /// Month + /// + Month = 0x02, + + /// + /// Day of the month + /// + Day = 0x04, + + /// + /// Hour + /// + Hour = 0x08, + + /// + /// Minute + /// + Minute = 0x10, + + /// + /// Second + /// + Second = 0x20 + } + + /// + /// This class implements a strategy where the model objects are clustered + /// according to some portion of the datetime value in the configured column. + /// + /// To create a strategy that grouped people who were born in + /// the same month, you would create a strategy that extracted just + /// the month, and formatted it to show just the month's name. Like this: + /// + /// + /// someColumn.ClusteringStrategy = new DateTimeClusteringStrategy(DateTimePortion.Month, "MMMM"); + /// + public class DateTimeClusteringStrategy : ClusteringStrategy { + #region Life and death + + /// + /// Create a strategy that clusters by month/year + /// + public DateTimeClusteringStrategy() + : this(DateTimePortion.Year | DateTimePortion.Month, "MMMM yyyy") { + } + + /// + /// Create a strategy that clusters around the given parts + /// + /// + /// + public DateTimeClusteringStrategy(DateTimePortion portions, string format) { + this.Portions = portions; + this.Format = format; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the format string that will be used to create a user-presentable + /// version of the cluster key. + /// + /// The format should use the date/time format strings, as documented + /// in the Windows SDK. Both standard formats and custom format will work. + /// "D" - long date pattern + /// "MMMM, yyyy" - "January, 1999" + public string Format { + get { return format; } + set { format = value; } + } + private string format; + + /// + /// Gets or sets the parts of the DateTime that will be extracted when + /// determining the clustering key for an object. + /// + public DateTimePortion Portions { + get { return portions; } + set { portions = value; } + } + private DateTimePortion portions = DateTimePortion.Year | DateTimePortion.Month; + + #endregion + + #region IClusterStrategy implementation + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// + /// + public override object GetClusterKey(object model) { + // Get the data attribute we want from the given model + // Make sure the returned value is a DateTime + DateTime? dateTime = this.Column.GetValue(model) as DateTime?; + if (!dateTime.HasValue) + return null; + + // Extract the parts of the datetime that we are interested in. + // Even if we aren't interested in a particular portion, we still have to give it a reasonable default + // otherwise we won't be able to build a DateTime object for it + int year = ((this.Portions & DateTimePortion.Year) == DateTimePortion.Year) ? dateTime.Value.Year : 1; + int month = ((this.Portions & DateTimePortion.Month) == DateTimePortion.Month) ? dateTime.Value.Month : 1; + int day = ((this.Portions & DateTimePortion.Day) == DateTimePortion.Day) ? dateTime.Value.Day : 1; + int hour = ((this.Portions & DateTimePortion.Hour) == DateTimePortion.Hour) ? dateTime.Value.Hour : 0; + int minute = ((this.Portions & DateTimePortion.Minute) == DateTimePortion.Minute) ? dateTime.Value.Minute : 0; + int second = ((this.Portions & DateTimePortion.Second) == DateTimePortion.Second) ? dateTime.Value.Second : 0; + + return new DateTime(year, month, day, hour, minute, second); + } + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + public override string GetClusterDisplayLabel(ICluster cluster) { + DateTime? dateTime = cluster.ClusterKey as DateTime?; + + return this.ApplyDisplayFormat(cluster, dateTime.HasValue ? this.DateToString(dateTime.Value) : NULL_LABEL); + } + + /// + /// Convert the given date into a user presentable string + /// + /// + /// + protected virtual string DateToString(DateTime dateTime) { + if (String.IsNullOrEmpty(this.Format)) + return dateTime.ToString(CultureInfo.CurrentUICulture); + + try { + return dateTime.ToString(this.Format); + } + catch (FormatException) { + return String.Format("Bad format string '{0}' for value '{1}'", this.Format, dateTime); + } + } + + #endregion + } +} diff --git a/ObjectListView/Filtering/FilterMenuBuilder.cs b/ObjectListView/Filtering/FilterMenuBuilder.cs new file mode 100644 index 00000000..e91614ac --- /dev/null +++ b/ObjectListView/Filtering/FilterMenuBuilder.cs @@ -0,0 +1,369 @@ +/* + * FilterMenuBuilder - Responsible for creating a Filter menu + * + * Author: Phillip Piper + * Date: 4-March-2011 11:59 pm + * + * Change log: + * 2012-05-20 JPP - Allow the same model object to be in multiple clusters + * Useful for xor'ed flag fields, and multi-value strings + * (e.g. hobbies that are stored as comma separated values). + * v2.5.1 + * 2012-04-14 JPP - Fixed rare bug with clustering an empty list (SF #3445118) + * v2.5 + * 2011-04-12 JPP - Added some images to menu + * 2011-03-04 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Collections; +using System.Drawing; + +namespace BrightIdeasSoftware { + + /// + /// Instances of this class know how to build a Filter menu. + /// It is responsible for clustering the values in the target column, + /// build a menu that shows those clusters, and then constructing + /// a filter that will enact the users choices. + /// + /// + /// Almost all of the methods in this class are declared as "virtual protected" + /// so that subclasses can provide alternative behaviours. + /// + public class FilterMenuBuilder { + + #region Static properties + + /// + /// Gets or sets the string that labels the Apply button. + /// Exposed so it can be localized. + /// + static public string APPLY_LABEL = "Apply"; + + /// + /// Gets or sets the string that labels the Clear All menu item. + /// Exposed so it can be localized. + /// + static public string CLEAR_ALL_FILTERS_LABEL = "Clear All Filters"; + + /// + /// Gets or sets the string that labels the Filtering menu as a whole.. + /// Exposed so it can be localized. + /// + static public string FILTERING_LABEL = "Filtering"; + + /// + /// Gets or sets the string that represents Select All values. + /// If this is set to null or empty, no Select All option will be included. + /// Exposed so it can be localized. + /// + static public string SELECT_ALL_LABEL = "Select All"; + + /// + /// Gets or sets the image that will be placed next to the Clear Filtering menu item + /// + static public Bitmap ClearFilteringImage = BrightIdeasSoftware.Properties.Resources.ClearFiltering; + + /// + /// Gets or sets the image that will be placed next to all "Apply" menu items on the filtering menu + /// + static public Bitmap FilteringImage = BrightIdeasSoftware.Properties.Resources.Filtering; + + #endregion + + #region Public properties + + /// + /// Gets or sets whether null should be considered as a valid data value. + /// If this is true (the default), then a cluster will null as a key will be allow. + /// If this is false, object that return a cluster key of null will ignored. + /// + public bool TreatNullAsDataValue { + get { return treatNullAsDataValue; } + set { treatNullAsDataValue = value; } + } + private bool treatNullAsDataValue = true; + + /// + /// Gets or sets the maximum number of objects that the clustering strategy + /// will consider. This should be large enough to collect all unique clusters, + /// but small enough to finish in a reasonable time. + /// + /// The default value is 10,000. This should be perfectly + /// acceptable for almost all lists. + public int MaxObjectsToConsider { + get { return maxObjectsToConsider; } + set { maxObjectsToConsider = value; } + } + private int maxObjectsToConsider = 10000; + + #endregion + + /// + /// Create a Filter menu on the given tool tip for the given column in the given ObjectListView. + /// + /// This is the main entry point into this class. + /// + /// + /// + /// The strip that should be shown to the user + virtual public ToolStripDropDown MakeFilterMenu(ToolStripDropDown strip, ObjectListView listView, OLVColumn column) { + if (strip == null) throw new ArgumentNullException("strip"); + if (listView == null) throw new ArgumentNullException("listView"); + if (column == null) throw new ArgumentNullException("column"); + + if (!column.UseFiltering || column.ClusteringStrategy == null || listView.Objects == null) + return strip; + + List clusters = this.Cluster(column.ClusteringStrategy, listView, column); + if (clusters.Count > 0) { + this.SortClusters(column.ClusteringStrategy, clusters); + strip.Items.Add(this.CreateFilteringMenuItem(column, clusters)); + } + + return strip; + } + + /// + /// Create a collection of clusters that should be presented to the user + /// + /// + /// + /// + /// + virtual protected List Cluster(IClusteringStrategy strategy, ObjectListView listView, OLVColumn column) { + // Build a map that correlates cluster key to clusters + NullableDictionary map = new NullableDictionary(); + int count = 0; + foreach (object model in listView.ObjectsForClustering) { + this.ClusterOneModel(strategy, map, model); + + if (count++ > this.MaxObjectsToConsider) + break; + } + + // Now that we know exactly how many items are in each cluster, create a label for it + foreach (ICluster cluster in map.Values) + cluster.DisplayLabel = strategy.GetClusterDisplayLabel(cluster); + + return new List(map.Values); + } + + private void ClusterOneModel(IClusteringStrategy strategy, NullableDictionary map, object model) { + object clusterKey = strategy.GetClusterKey(model); + + // If the returned value is an IEnumerable, that means the given model can belong to more than one cluster + IEnumerable keyEnumerable = clusterKey as IEnumerable; + if (clusterKey is string || keyEnumerable == null) + keyEnumerable = new object[] {clusterKey}; + + // Deal with nulls and DBNulls + ArrayList nullCorrected = new ArrayList(); + foreach (object key in keyEnumerable) { + if (key == null || key == System.DBNull.Value) { + if (this.TreatNullAsDataValue) + nullCorrected.Add(null); + } else nullCorrected.Add(key); + } + + // Group by key + foreach (object key in nullCorrected) { + if (map.ContainsKey(key)) + map[key].Count += 1; + else + map[key] = strategy.CreateCluster(key); + } + } + + /// + /// Order the given list of clusters in the manner in which they should be presented to the user. + /// + /// + /// + virtual protected void SortClusters(IClusteringStrategy strategy, List clusters) { + clusters.Sort(); + } + + /// + /// Do the work of making a menu that shows the clusters to the users + /// + /// + /// + /// + virtual protected ToolStripMenuItem CreateFilteringMenuItem(OLVColumn column, List clusters) { + ToolStripCheckedListBox checkedList = new ToolStripCheckedListBox(); + checkedList.Tag = column; + foreach (ICluster cluster in clusters) + checkedList.AddItem(cluster, column.ValuesChosenForFiltering.Contains(cluster.ClusterKey)); + if (!String.IsNullOrEmpty(SELECT_ALL_LABEL)) { + int checkedCount = checkedList.CheckedItems.Count; + if (checkedCount == 0) + checkedList.AddItem(SELECT_ALL_LABEL, CheckState.Unchecked); + else + checkedList.AddItem(SELECT_ALL_LABEL, checkedCount == clusters.Count ? CheckState.Checked : CheckState.Indeterminate); + } + checkedList.ItemCheck += new ItemCheckEventHandler(HandleItemCheckedWrapped); + + ToolStripMenuItem clearAll = new ToolStripMenuItem(CLEAR_ALL_FILTERS_LABEL, ClearFilteringImage, delegate(object sender, EventArgs args) { + this.ClearAllFilters(column); + }); + ToolStripMenuItem apply = new ToolStripMenuItem(APPLY_LABEL, FilteringImage, delegate(object sender, EventArgs args) { + this.EnactFilter(checkedList, column); + }); + ToolStripMenuItem subMenu = new ToolStripMenuItem(FILTERING_LABEL, null, new ToolStripItem[] { + clearAll, new ToolStripSeparator(), checkedList, apply }); + return subMenu; + } + + /// + /// Wrap a protected section around the real HandleItemChecked method, so that if + /// that method tries to change a "checkedness" of an item, we don't get a recursive + /// stack error. Effectively, this ensure that HandleItemChecked is only called + /// in response to a user action. + /// + /// + /// + private void HandleItemCheckedWrapped(object sender, ItemCheckEventArgs e) { + if (alreadyInHandleItemChecked) + return; + + try { + alreadyInHandleItemChecked = true; + this.HandleItemChecked(sender, e); + } + finally { + alreadyInHandleItemChecked = false; + } + } + bool alreadyInHandleItemChecked = false; + + /// + /// Handle a user-generated ItemCheck event + /// + /// + /// + virtual protected void HandleItemChecked(object sender, ItemCheckEventArgs e) { + + ToolStripCheckedListBox checkedList = sender as ToolStripCheckedListBox; + if (checkedList == null) return; + OLVColumn column = checkedList.Tag as OLVColumn; + if (column == null) return; + ObjectListView listView = column.ListView as ObjectListView; + if (listView == null) return; + + // Deal with the "Select All" item if there is one + int selectAllIndex = checkedList.Items.IndexOf(SELECT_ALL_LABEL); + if (selectAllIndex >= 0) + HandleSelectAllItem(e, checkedList, selectAllIndex); + } + + /// + /// Handle any checking/unchecking of the Select All option, and keep + /// its checkedness in sync with everything else that is checked. + /// + /// + /// + /// + virtual protected void HandleSelectAllItem(ItemCheckEventArgs e, ToolStripCheckedListBox checkedList, int selectAllIndex) { + // Did they check/uncheck the "Select All"? + if (e.Index == selectAllIndex) { + if (e.NewValue == CheckState.Checked) + checkedList.CheckAll(); + if (e.NewValue == CheckState.Unchecked) + checkedList.UncheckAll(); + return; + } + + // OK. The user didn't check/uncheck SelectAll. Now we have to update it's + // checkedness to reflect the state of everything else + // If all clusters are checked, we check the Select All. + // If no clusters are checked, the uncheck the Select All. + // For everything else, Select All is set to indeterminate. + + // How many items are currently checked? + int count = checkedList.CheckedItems.Count; + + // First complication. + // The value of the Select All itself doesn't count + if (checkedList.GetItemCheckState(selectAllIndex) != CheckState.Unchecked) + count -= 1; + + // Another complication. + // CheckedItems does not yet know about the item the user has just + // clicked, so we have to adjust the count of checked items to what + // it is going to be + if (e.NewValue != e.CurrentValue) { + if (e.NewValue == CheckState.Checked) + count += 1; + else + count -= 1; + } + + // Update the state of the Select All item + if (count == 0) + checkedList.SetItemState(selectAllIndex, CheckState.Unchecked); + else if (count == checkedList.Items.Count - 1) + checkedList.SetItemState(selectAllIndex, CheckState.Checked); + else + checkedList.SetItemState(selectAllIndex, CheckState.Indeterminate); + } + + /// + /// Clear all the filters that are applied to the given column + /// + /// The column from which filters are to be removed + virtual protected void ClearAllFilters(OLVColumn column) { + + ObjectListView olv = column.ListView as ObjectListView; + if (olv == null || olv.IsDisposed) + return; + + olv.ResetColumnFiltering(); + } + + /// + /// Apply the selected values from the given list as a filter on the given column + /// + /// A list in which the checked items should be used as filters + /// The column for which a filter should be generated + virtual protected void EnactFilter(ToolStripCheckedListBox checkedList, OLVColumn column) { + + ObjectListView olv = column.ListView as ObjectListView; + if (olv == null || olv.IsDisposed) + return; + + // Collect all the checked values + ArrayList chosenValues = new ArrayList(); + foreach (object x in checkedList.CheckedItems) { + ICluster cluster = x as ICluster; + if (cluster != null) { + chosenValues.Add(cluster.ClusterKey); + } + } + column.ValuesChosenForFiltering = chosenValues; + + olv.UpdateColumnFiltering(); + } + } +} diff --git a/ObjectListView/Filtering/Filters.cs b/ObjectListView/Filtering/Filters.cs new file mode 100644 index 00000000..db69a624 --- /dev/null +++ b/ObjectListView/Filtering/Filters.cs @@ -0,0 +1,489 @@ +/* + * Filters - Filtering on ObjectListViews + * + * Author: Phillip Piper + * Date: 03/03/2010 17:00 + * + * Change log: + * 2011-03-01 JPP Added CompositeAllFilter, CompositeAnyFilter and OneOfFilter + * v2.4.1 + * 2010-06-23 JPP Extended TextMatchFilter to handle regular expressions and string prefix matching. + * v2.4 + * 2010-03-03 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2010-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Reflection; +using System.Drawing; + +namespace BrightIdeasSoftware +{ + /// + /// Interface for model-by-model filtering + /// + public interface IModelFilter + { + /// + /// Should the given model be included when this filter is installed + /// + /// The model object to consider + /// Returns true if the model will be included by the filter + bool Filter(object modelObject); + } + + /// + /// Interface for whole list filtering + /// + public interface IListFilter + { + /// + /// Return a subset of the given list of model objects as the new + /// contents of the ObjectListView + /// + /// The collection of model objects that the list will possibly display + /// The filtered collection that holds the model objects that will be displayed. + IEnumerable Filter(IEnumerable modelObjects); + } + + /// + /// Base class for model-by-model filters + /// + public class AbstractModelFilter : IModelFilter + { + /// + /// Should the given model be included when this filter is installed + /// + /// The model object to consider + /// Returns true if the model will be included by the filter + virtual public bool Filter(object modelObject) { + return true; + } + } + + /// + /// This filter calls a given Predicate to decide if a model object should be included + /// + public class ModelFilter : IModelFilter + { + /// + /// Create a filter based on the given predicate + /// + /// The function that will filter objects + public ModelFilter(Predicate predicate) { + this.Predicate = predicate; + } + + /// + /// Gets or sets the predicate used to filter model objects + /// + protected Predicate Predicate { + get { return predicate; } + set { predicate = value; } + } + private Predicate predicate; + + /// + /// Should the given model object be included? + /// + /// + /// + virtual public bool Filter(object modelObject) { + return this.Predicate == null ? true : this.Predicate(modelObject); + } + } + + /// + /// A CompositeFilter joins several other filters together. + /// If there are no filters, all model objects are included + /// + abstract public class CompositeFilter : IModelFilter { + + /// + /// Create an empty filter + /// + public CompositeFilter() { + } + + /// + /// Create a composite filter from the given list of filters + /// + /// A list of filters + public CompositeFilter(IEnumerable filters) { + foreach (IModelFilter filter in filters) { + if (filter != null) + Filters.Add(filter); + } + } + + /// + /// Gets or sets the filters used by this composite + /// + public IList Filters { + get { return filters; } + set { filters = value; } + } + private IList filters = new List(); + + /// + /// Get the sub filters that are text match filters + /// + public IEnumerable TextFilters { + get { + foreach (IModelFilter filter in this.Filters) { + TextMatchFilter textFilter = filter as TextMatchFilter; + if (textFilter != null) + yield return textFilter; + } + } + } + + /// + /// Decide whether or not the given model should be included by the filter + /// + /// + /// True if the object is included by the filter + virtual public bool Filter(object modelObject) { + if (this.Filters == null || this.Filters.Count == 0) + return true; + + return this.FilterObject(modelObject); + } + + /// + /// Decide whether or not the given model should be included by the filter + /// + /// Filters is guaranteed to be non-empty when this method is called + /// The model object under consideration + /// True if the object is included by the filter + abstract public bool FilterObject(object modelObject); + } + + /// + /// A CompositeAllFilter joins several other filters together. + /// A model object must satisfy all filters to be included. + /// If there are no filters, all model objects are included + /// + public class CompositeAllFilter : CompositeFilter { + + /// + /// Create a filter + /// + /// + public CompositeAllFilter(List filters) + : base(filters) { + } + + /// + /// Decide whether or not the given model should be included by the filter + /// + /// Filters is guaranteed to be non-empty when this method is called + /// The model object under consideration + /// True if the object is included by the filter + override public bool FilterObject(object modelObject) { + foreach (IModelFilter filter in this.Filters) + if (!filter.Filter(modelObject)) + return false; + + return true; + } + } + + /// + /// A CompositeAllFilter joins several other filters together. + /// A model object must only satisfy one of the filters to be included. + /// If there are no filters, all model objects are included + /// + public class CompositeAnyFilter : CompositeFilter { + + /// + /// Create a filter from the given filters + /// + /// + public CompositeAnyFilter(List filters) + : base(filters) { + } + + /// + /// Decide whether or not the given model should be included by the filter + /// + /// Filters is guaranteed to be non-empty when this method is called + /// The model object under consideration + /// True if the object is included by the filter + override public bool FilterObject(object modelObject) { + foreach (IModelFilter filter in this.Filters) + if (filter.Filter(modelObject)) + return true; + + return false; + } + } + + /// + /// Instances of this class extract a value from the model object + /// and compare that value to a list of fixed values. The model + /// object is included if the extracted value is in the list + /// + /// If there is no delegate installed or there are + /// no values to match, no model objects will be matched + public class OneOfFilter : IModelFilter { + + /// + /// Create a filter that will use the given delegate to extract values + /// + /// + public OneOfFilter(AspectGetterDelegate valueGetter) : + this(valueGetter, new ArrayList()) { + } + + /// + /// Create a filter that will extract values using the given delegate + /// and compare them to the values in the given list. + /// + /// + /// + public OneOfFilter(AspectGetterDelegate valueGetter, ICollection possibleValues) { + this.ValueGetter = valueGetter; + this.PossibleValues = new ArrayList(possibleValues); + } + + /// + /// Gets or sets the delegate that will be used to extract values + /// from model objects + /// + virtual public AspectGetterDelegate ValueGetter { + get { return valueGetter; } + set { valueGetter = value; } + } + private AspectGetterDelegate valueGetter; + + /// + /// Gets or sets the list of values that the value extracted from + /// the model object must match in order to be included. + /// + virtual public IList PossibleValues { + get { return possibleValues; } + set { possibleValues = value; } + } + private IList possibleValues; + + /// + /// Should the given model object be included? + /// + /// + /// + public virtual bool Filter(object modelObject) { + if (this.ValueGetter == null || this.PossibleValues == null || this.PossibleValues.Count == 0) + return false; + + object result = this.ValueGetter(modelObject); + IEnumerable enumerable = result as IEnumerable; + if (result is string || enumerable == null) + return this.DoesValueMatch(result); + + foreach (object x in enumerable) { + if (this.DoesValueMatch(x)) + return true; + } + return false; + } + + /// + /// Decides if the given property is a match for the values in the PossibleValues collection + /// + /// + /// + protected virtual bool DoesValueMatch(object result) { + return this.PossibleValues.Contains(result); + } + } + + /// + /// Instances of this class match a property of a model objects against + /// a list of bit flags. The property should be an xor-ed collection + /// of bits flags. + /// + /// Both the property compared and the list of possible values + /// must be convertible to ulongs. + public class FlagBitSetFilter : OneOfFilter { + + /// + /// Create an instance + /// + /// + /// + public FlagBitSetFilter(AspectGetterDelegate valueGetter, ICollection possibleValues) : base(valueGetter, possibleValues) { + this.ConvertPossibleValues(); + } + + /// + /// Gets or sets the collection of values that will be matched. + /// These must be ulongs (or convertible to ulongs). + /// + public override IList PossibleValues { + get { return base.PossibleValues; } + set { + base.PossibleValues = value; + this.ConvertPossibleValues(); + } + } + + private void ConvertPossibleValues() { + this.possibleValuesAsUlongs = new List(); + foreach (object x in this.PossibleValues) + this.possibleValuesAsUlongs.Add(Convert.ToUInt64(x)); + } + + /// + /// Decides if the given property is a match for the values in the PossibleValues collection + /// + /// + /// + protected override bool DoesValueMatch(object result) { + try { + UInt64 value = Convert.ToUInt64(result); + foreach (ulong flag in this.possibleValuesAsUlongs) { + if ((value & flag) == flag) + return true; + } + return false; + } + catch (InvalidCastException) { + return false; + } + catch (FormatException) { + return false; + } + } + + private List possibleValuesAsUlongs = new List(); + } + + /// + /// Base class for whole list filters + /// + public class AbstractListFilter : IListFilter + { + /// + /// Return a subset of the given list of model objects as the new + /// contents of the ObjectListView + /// + /// The collection of model objects that the list will possibly display + /// The filtered collection that holds the model objects that will be displayed. + virtual public IEnumerable Filter(IEnumerable modelObjects) { + return modelObjects; + } + } + + /// + /// Instance of this class implement delegate based whole list filtering + /// + public class ListFilter : AbstractListFilter + { + /// + /// A delegate that filters on a whole list + /// + /// + /// + public delegate IEnumerable ListFilterDelegate(IEnumerable rowObjects); + + /// + /// Create a ListFilter + /// + /// + public ListFilter(ListFilterDelegate function) { + this.Function = function; + } + + /// + /// Gets or sets the delegate that will filter the list + /// + public ListFilterDelegate Function { + get { return function; } + set { function = value; } + } + private ListFilterDelegate function; + + /// + /// Do the actual work of filtering + /// + /// + /// + public override IEnumerable Filter(IEnumerable modelObjects) { + if (this.Function == null) + return modelObjects; + + return this.Function(modelObjects); + } + } + + /// + /// Filter the list so only the last N entries are displayed + /// + public class TailFilter : AbstractListFilter + { + /// + /// Create a no-op tail filter + /// + public TailFilter() { + } + + /// + /// Create a filter that includes on the last N model objects + /// + /// + public TailFilter(int numberOfObjects) { + this.Count = numberOfObjects; + } + + /// + /// Gets or sets the number of model objects that will be + /// returned from the tail of the list + /// + public int Count { + get { return count; } + set { count = value; } + } + private int count; + + /// + /// Return the last N subset of the model objects + /// + /// + /// + public override IEnumerable Filter(IEnumerable modelObjects) { + if (this.Count <= 0) + return modelObjects; + + ArrayList list = ObjectListView.EnumerableToArray(modelObjects, false); + + if (this.Count > list.Count) + return list; + + object[] tail = new object[this.Count]; + list.CopyTo(list.Count - this.Count, tail, 0, this.Count); + return new ArrayList(tail); + } + } +} \ No newline at end of file diff --git a/ObjectListView/Filtering/FlagClusteringStrategy.cs b/ObjectListView/Filtering/FlagClusteringStrategy.cs new file mode 100644 index 00000000..519ce891 --- /dev/null +++ b/ObjectListView/Filtering/FlagClusteringStrategy.cs @@ -0,0 +1,160 @@ +/* + * FlagClusteringStrategy - Implements a clustering strategy for a field which is a single integer + * containing an XOR'ed collection of bit flags + * + * Author: Phillip Piper + * Date: 23-March-2012 8:33 am + * + * Change log: + * 2012-03-23 JPP - First version + * + * Copyright (C) 2012 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; + +namespace BrightIdeasSoftware { + + /// + /// Instances of this class cluster model objects on the basis of a + /// property that holds an xor-ed collection of bit flags. + /// + public class FlagClusteringStrategy : ClusteringStrategy + { + #region Life and death + + /// + /// Create a clustering strategy that operates on the flags of the given enum + /// + /// + public FlagClusteringStrategy(Type enumType) { + if (enumType == null) throw new ArgumentNullException("enumType"); + if (!enumType.IsEnum) throw new ArgumentException("Type must be enum", "enumType"); + if (enumType.GetCustomAttributes(typeof(FlagsAttribute), false) == null) throw new ArgumentException("Type must have [Flags] attribute", "enumType"); + + List flags = new List(); + foreach (object x in Enum.GetValues(enumType)) + flags.Add(Convert.ToInt64(x)); + + List flagLabels = new List(); + foreach (string x in Enum.GetNames(enumType)) + flagLabels.Add(x); + + this.SetValues(flags.ToArray(), flagLabels.ToArray()); + } + + /// + /// Create a clustering strategy around the given collections of flags and their display labels. + /// There must be the same number of elements in both collections. + /// + /// The list of flags. + /// + public FlagClusteringStrategy(long[] values, string[] labels) { + this.SetValues(values, labels); + } + + #endregion + + #region Implementation + + /// + /// Gets the value that will be xor-ed to test for the presence of a particular value. + /// + public long[] Values { + get { return this.values; } + private set { this.values = value; } + } + private long[] values; + + /// + /// Gets the labels that will be used when the corresponding Value is XOR present in the data. + /// + public string[] Labels { + get { return this.labels; } + private set { this.labels = value; } + } + private string[] labels; + + private void SetValues(long[] flags, string[] flagLabels) { + if (flags == null || flags.Length == 0) throw new ArgumentNullException("flags"); + if (flagLabels == null || flagLabels.Length == 0) throw new ArgumentNullException("flagLabels"); + if (flags.Length != flagLabels.Length) throw new ArgumentException("values and labels must have the same number of entries", "flags"); + + this.Values = flags; + this.Labels = flagLabels; + } + + #endregion + + #region Implementation of IClusteringStrategy + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// + /// + public override object GetClusterKey(object model) { + List flags = new List(); + try { + long modelValue = Convert.ToInt64(this.Column.GetValue(model)); + foreach (long x in this.Values) { + if ((x & modelValue) == x) + flags.Add(x); + } + return flags; + } + catch (InvalidCastException ex) { + System.Diagnostics.Debug.Write(ex); + return flags; + } + catch (FormatException ex) { + System.Diagnostics.Debug.Write(ex); + return flags; + } + } + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + public override string GetClusterDisplayLabel(ICluster cluster) { + long clusterKeyAsUlong = Convert.ToInt64(cluster.ClusterKey); + for (int i = 0; i < this.Values.Length; i++ ) { + if (clusterKeyAsUlong == this.Values[i]) + return this.ApplyDisplayFormat(cluster, this.Labels[i]); + } + return this.ApplyDisplayFormat(cluster, clusterKeyAsUlong.ToString(CultureInfo.CurrentUICulture)); + } + + /// + /// Create a filter that will include only model objects that + /// match one or more of the given values. + /// + /// + /// + public override IModelFilter CreateFilter(IList valuesChosenForFiltering) { + return new FlagBitSetFilter(this.GetClusterKey, valuesChosenForFiltering); + } + + #endregion + } +} \ No newline at end of file diff --git a/ObjectListView/Filtering/ICluster.cs b/ObjectListView/Filtering/ICluster.cs new file mode 100644 index 00000000..31965950 --- /dev/null +++ b/ObjectListView/Filtering/ICluster.cs @@ -0,0 +1,56 @@ +/* + * ICluster - A cluster is a group of objects that can be included or excluded as a whole + * + * Author: Phillip Piper + * Date: 4-March-2011 11:59 pm + * + * Change log: + * 2011-03-04 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware { + + /// + /// A cluster is a like collection of objects that can be usefully filtered + /// as whole using the filtering UI provided by the ObjectListView. + /// + public interface ICluster : IComparable { + /// + /// Gets or sets how many items belong to this cluster + /// + int Count { get; set; } + + /// + /// Gets or sets the label that will be shown to the user to represent + /// this cluster + /// + string DisplayLabel { get; set; } + + /// + /// Gets or sets the actual data object that all members of this cluster + /// have commonly returned. + /// + object ClusterKey { get; set; } + } +} diff --git a/ObjectListView/Filtering/IClusteringStrategy.cs b/ObjectListView/Filtering/IClusteringStrategy.cs new file mode 100644 index 00000000..fb6a4e2c --- /dev/null +++ b/ObjectListView/Filtering/IClusteringStrategy.cs @@ -0,0 +1,80 @@ +/* + * IClusterStrategy - Encapsulates the ability to create a list of clusters from an ObjectListView + * + * Author: Phillip Piper + * Date: 4-March-2011 11:59 pm + * + * Change log: + * 2012-05-23 JPP - Added CreateFilter() method to interface to allow the strategy + * to control the actual model filter that is created. + * v2.5 + * 2011-03-04 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware{ + + /// + /// Implementation of this interface control the selecting of cluster keys + /// and how those clusters will be presented to the user + /// + public interface IClusteringStrategy { + + /// + /// Gets or sets the column upon which this strategy will operate + /// + OLVColumn Column { get; set; } + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// If the returned value is an IEnumerable, the given model is considered + /// to belong to multiple clusters + /// + /// + object GetClusterKey(object model); + + /// + /// Create a cluster to hold the given cluster key + /// + /// + /// + ICluster CreateCluster(object clusterKey); + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + string GetClusterDisplayLabel(ICluster cluster); + + /// + /// Create a filter that will include only model objects that + /// match one or more of the given values. + /// + /// + /// + IModelFilter CreateFilter(IList valuesChosenForFiltering); + } +} diff --git a/ObjectListView/Filtering/TextMatchFilter.cs b/ObjectListView/Filtering/TextMatchFilter.cs new file mode 100644 index 00000000..8df37197 --- /dev/null +++ b/ObjectListView/Filtering/TextMatchFilter.cs @@ -0,0 +1,642 @@ +/* + * TextMatchFilter - Text based filtering on ObjectListViews + * + * Author: Phillip Piper + * Date: 31/05/2011 7:45am + * + * Change log: + * 2018-05-01 JPP - Added ITextMatchFilter to allow for alternate implementations + * - Made several classes public so they can be subclassed + * v2.6 + * 2012-10-13 JPP Allow filtering to consider additional columns + * v2.5.1 + * 2011-06-22 JPP Handle searching for empty strings + * v2.5.0 + * 2011-05-31 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2011-2018 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.Drawing; + +namespace BrightIdeasSoftware { + + public interface ITextMatchFilter: IModelFilter { + /// + /// Find all the ways in which this filter matches the given string. + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted + /// + /// A list of character ranges indicating the matched substrings + IEnumerable FindAllMatchedRanges(string cellText); + } + + /// + /// Instances of this class include only those rows of the listview + /// that match one or more given strings. + /// + /// This class can match strings by prefix, regex, or simple containment. + /// There are factory methods for each of these matching strategies. + public class TextMatchFilter : AbstractModelFilter, ITextMatchFilter { + + #region Life and death + + /// + /// Create a text filter that will include rows where any cell matches + /// any of the given regex expressions. + /// + /// + /// + /// + /// Any string that is not a valid regex expression will be ignored. + public static TextMatchFilter Regex(ObjectListView olv, params string[] texts) { + TextMatchFilter filter = new TextMatchFilter(olv); + filter.RegexStrings = texts; + return filter; + } + + /// + /// Create a text filter that includes rows where any cell begins with one of the given strings + /// + /// + /// + /// + public static TextMatchFilter Prefix(ObjectListView olv, params string[] texts) { + TextMatchFilter filter = new TextMatchFilter(olv); + filter.PrefixStrings = texts; + return filter; + } + + /// + /// Create a text filter that includes rows where any cell contains any of the given strings. + /// + /// + /// + /// + public static TextMatchFilter Contains(ObjectListView olv, params string[] texts) { + TextMatchFilter filter = new TextMatchFilter(olv); + filter.ContainsStrings = texts; + return filter; + } + + /// + /// Create a TextFilter + /// + /// + public TextMatchFilter(ObjectListView olv) { + this.ListView = olv; + } + + /// + /// Create a TextFilter that finds the given string + /// + /// + /// + public TextMatchFilter(ObjectListView olv, string text) { + this.ListView = olv; + this.ContainsStrings = new string[] { text }; + } + + /// + /// Create a TextFilter that finds the given string using the given comparison + /// + /// + /// + /// + public TextMatchFilter(ObjectListView olv, string text, StringComparison comparison) { + this.ListView = olv; + this.ContainsStrings = new string[] { text }; + this.StringComparison = comparison; + } + + #endregion + + #region Public properties + + /// + /// Gets or sets which columns will be used for the comparisons? If this is null, all columns will be used + /// + public OLVColumn[] Columns { + get { return columns; } + set { columns = value; } + } + private OLVColumn[] columns; + + /// + /// Gets or sets additional columns which will be used in the comparison. These will be used + /// in addition to either the Columns property or to all columns taken from the control. + /// + public OLVColumn[] AdditionalColumns { + get { return additionalColumns; } + set { additionalColumns = value; } + } + private OLVColumn[] additionalColumns; + + /// + /// Gets or sets the collection of strings that will be used for + /// contains matching. Setting this replaces all previous texts + /// of any kind. + /// + public IEnumerable ContainsStrings { + get { + foreach (TextMatchingStrategy component in this.MatchingStrategies) + yield return component.Text; + } + set { + this.MatchingStrategies = new List(); + if (value != null) { + foreach (string text in value) + this.MatchingStrategies.Add(new TextContainsMatchingStrategy(this, text)); + } + } + } + + /// + /// Gets whether or not this filter has any search criteria + /// + public bool HasComponents { + get { + return this.MatchingStrategies.Count > 0; + } + } + + /// + /// Gets or set the ObjectListView upon which this filter will work + /// + /// + /// You cannot really rebase a filter after it is created, so do not change this value. + /// It is included so that it can be set in an object initialiser. + /// + public ObjectListView ListView { + get { return listView; } + set { listView = value; } + } + private ObjectListView listView; + + /// + /// Gets or sets the collection of strings that will be used for + /// prefix matching. Setting this replaces all previous texts + /// of any kind. + /// + public IEnumerable PrefixStrings { + get { + foreach (TextMatchingStrategy component in this.MatchingStrategies) + yield return component.Text; + } + set { + this.MatchingStrategies = new List(); + if (value != null) { + foreach (string text in value) + this.MatchingStrategies.Add(new TextBeginsMatchingStrategy(this, text)); + } + } + } + + /// + /// Gets or sets the options that will be used when compiling the regular expression. + /// + /// + /// This is only used when doing Regex matching (obviously). + /// If this is not set specifically, the appropriate options are chosen to match the + /// StringComparison setting (culture invariant, case sensitive). + /// + public RegexOptions RegexOptions { + get { + if (!regexOptions.HasValue) { + switch (this.StringComparison) { + case StringComparison.CurrentCulture: + regexOptions = RegexOptions.None; + break; + case StringComparison.CurrentCultureIgnoreCase: + regexOptions = RegexOptions.IgnoreCase; + break; + case StringComparison.Ordinal: + case StringComparison.InvariantCulture: + regexOptions = RegexOptions.CultureInvariant; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.InvariantCultureIgnoreCase: + regexOptions = RegexOptions.CultureInvariant | RegexOptions.IgnoreCase; + break; + default: + regexOptions = RegexOptions.None; + break; + } + } + return regexOptions.Value; + } + set { + regexOptions = value; + } + } + private RegexOptions? regexOptions; + + /// + /// Gets or sets the collection of strings that will be used for + /// regex pattern matching. Setting this replaces all previous texts + /// of any kind. + /// + public IEnumerable RegexStrings { + get { + foreach (TextMatchingStrategy component in this.MatchingStrategies) + yield return component.Text; + } + set { + this.MatchingStrategies = new List(); + if (value != null) { + foreach (string text in value) + this.MatchingStrategies.Add(new TextRegexMatchingStrategy(this, text)); + } + } + } + + /// + /// Gets or sets how the filter will match text + /// + public StringComparison StringComparison { + get { return this.stringComparison; } + set { this.stringComparison = value; } + } + private StringComparison stringComparison = StringComparison.InvariantCultureIgnoreCase; + + #endregion + + #region Implementation + + /// + /// Loop over the columns that are being considering by the filter + /// + /// + protected virtual IEnumerable IterateColumns() { + if (this.Columns == null) { + foreach (OLVColumn column in this.ListView.Columns) + yield return column; + } else { + foreach (OLVColumn column in this.Columns) + yield return column; + } + if (this.AdditionalColumns != null) { + foreach (OLVColumn column in this.AdditionalColumns) + yield return column; + } + } + + #endregion + + #region Public interface + + /// + /// Do the actual work of filtering + /// + /// + /// + public override bool Filter(object modelObject) { + if (this.ListView == null || !this.HasComponents) + return true; + + foreach (OLVColumn column in this.IterateColumns()) { + if (column.IsVisible && column.Searchable) { + string[] cellTexts = column.GetSearchValues(modelObject); + if (cellTexts != null && cellTexts.Length > 0) { + foreach (TextMatchingStrategy filter in this.MatchingStrategies) { + if (String.IsNullOrEmpty(filter.Text)) + return true; + foreach (string cellText in cellTexts) { + if (filter.MatchesText(cellText)) + return true; + } + } + } + } + } + + return false; + } + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted + /// + /// A list of character ranges indicating the matched substrings + public IEnumerable FindAllMatchedRanges(string cellText) { + List ranges = new List(); + + foreach (TextMatchingStrategy filter in this.MatchingStrategies) { + if (!String.IsNullOrEmpty(filter.Text)) + ranges.AddRange(filter.FindAllMatchedRanges(cellText)); + } + + return ranges; + } + + /// + /// Is the given column one of the columns being used by this filter? + /// + /// + /// + public bool IsIncluded(OLVColumn column) { + if (this.Columns == null) { + return column.ListView == this.ListView; + } + + foreach (OLVColumn x in this.Columns) { + if (x == column) + return true; + } + + return false; + } + + #endregion + + #region Implementation members + + protected List MatchingStrategies = new List(); + + #endregion + + #region Components + + /// + /// Base class for the various types of string matching that TextMatchFilter provides + /// + public abstract class TextMatchingStrategy { + + /// + /// Gets how the filter will match text + /// + public StringComparison StringComparison { + get { return this.TextFilter.StringComparison; } + } + + /// + /// Gets the text filter to which this component belongs + /// + public TextMatchFilter TextFilter { + get { return textFilter; } + set { textFilter = value; } + } + private TextMatchFilter textFilter; + + /// + /// Gets or sets the text that will be matched + /// + public string Text { + get { return this.text; } + set { this.text = value; } + } + private string text; + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted. + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// A list of character ranges indicating the matched substrings + public abstract IEnumerable FindAllMatchedRanges(string cellText); + + /// + /// Does the given text match the filter + /// + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// Return true if the given cellText matches our strategy + public abstract bool MatchesText(string cellText); + } + + /// + /// This component provides text contains matching strategy. + /// + public class TextContainsMatchingStrategy : TextMatchingStrategy { + + /// + /// Create a text contains strategy + /// + /// + /// + public TextContainsMatchingStrategy(TextMatchFilter filter, string text) { + this.TextFilter = filter; + this.Text = text; + } + + /// + /// Does the given text match the filter + /// + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// Return true if the given cellText matches our strategy + public override bool MatchesText(string cellText) { + return cellText.IndexOf(this.Text, this.StringComparison) != -1; + } + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted. + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// A list of character ranges indicating the matched substrings + public override IEnumerable FindAllMatchedRanges(string cellText) { + List ranges = new List(); + + int matchIndex = cellText.IndexOf(this.Text, this.StringComparison); + while (matchIndex != -1) { + ranges.Add(new CharacterRange(matchIndex, this.Text.Length)); + matchIndex = cellText.IndexOf(this.Text, matchIndex + this.Text.Length, this.StringComparison); + } + + return ranges; + } + } + + /// + /// This component provides text begins with matching strategy. + /// + public class TextBeginsMatchingStrategy : TextMatchingStrategy { + + /// + /// Create a text begins strategy + /// + /// + /// + public TextBeginsMatchingStrategy(TextMatchFilter filter, string text) { + this.TextFilter = filter; + this.Text = text; + } + + /// + /// Does the given text match the filter + /// + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// Return true if the given cellText matches our strategy + public override bool MatchesText(string cellText) { + return cellText.StartsWith(this.Text, this.StringComparison); + } + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted. + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// A list of character ranges indicating the matched substrings + public override IEnumerable FindAllMatchedRanges(string cellText) { + List ranges = new List(); + + if (cellText.StartsWith(this.Text, this.StringComparison)) + ranges.Add(new CharacterRange(0, this.Text.Length)); + + return ranges; + } + + } + + /// + /// This component provides regex matching strategy. + /// + public class TextRegexMatchingStrategy : TextMatchingStrategy { + + /// + /// Creates a regex strategy + /// + /// + /// + public TextRegexMatchingStrategy(TextMatchFilter filter, string text) { + this.TextFilter = filter; + this.Text = text; + } + + /// + /// Gets or sets the options that will be used when compiling the regular expression. + /// + public RegexOptions RegexOptions { + get { + return this.TextFilter.RegexOptions; + } + } + + /// + /// Gets or sets a compiled regular expression, based on our current Text and RegexOptions. + /// + /// + /// If Text fails to compile as a regular expression, this will return a Regex object + /// that will match all strings. + /// + protected Regex Regex { + get { + if (this.regex == null) { + try { + this.regex = new Regex(this.Text, this.RegexOptions); + } + catch (ArgumentException) { + this.regex = TextRegexMatchingStrategy.InvalidRegexMarker; + } + } + return this.regex; + } + set { + this.regex = value; + } + } + private Regex regex; + + /// + /// Gets whether or not our current regular expression is a valid regex + /// + protected bool IsRegexInvalid { + get { + return this.Regex == TextRegexMatchingStrategy.InvalidRegexMarker; + } + } + private static Regex InvalidRegexMarker = new Regex(".*"); + + /// + /// Does the given text match the filter + /// + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// Return true if the given cellText matches our strategy + public override bool MatchesText(string cellText) { + if (this.IsRegexInvalid) + return true; + return this.Regex.Match(cellText).Success; + } + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted. + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// A list of character ranges indicating the matched substrings + public override IEnumerable FindAllMatchedRanges(string cellText) { + List ranges = new List(); + + if (!this.IsRegexInvalid) { + foreach (Match match in this.Regex.Matches(cellText)) { + if (match.Length > 0) + ranges.Add(new CharacterRange(match.Index, match.Length)); + } + } + + return ranges; + } + } + + #endregion + } +} diff --git a/ObjectListView/FullClassDiagram.cd b/ObjectListView/FullClassDiagram.cd new file mode 100644 index 00000000..3126de20 --- /dev/null +++ b/ObjectListView/FullClassDiagram.cd @@ -0,0 +1,1261 @@ + + + + + + AQCABIAAAIAhAACgAAAAAIAMAAAECAAggAIAIIAAAEA= + CellEditing\CellEditKeyEngine.cs + + + + + + AAAAAAAAAAAAAAQEIAAEAAAAAAAAAAAAAAAAAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAgAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAQIAAABAQAAQAAgAAAAAAABAIAAAAQAAAAAAAA= + CellEditing\EditorRegistry.cs + + + + + + ABgAAAAAAAAAAgIhAAACAAAEAAAAAAAAAAAAAAAAAAA= + DataListView.cs + + + + + + BAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAQAA= + DragDrop\DragSource.cs + + + + + + + BAAAAAAAAAAAAAAAAQAAAAAQAAAQEAAAAAAAAAAAQAA= + DragDrop\DragSource.cs + + + + + + + AAAAAAAAAAAQQAAAAAIAEAAAAAEFAAAAgAAAAMAAAAA= + DragDrop\DropSink.cs + + + + + + + ZS0gAiQEBAoQDA8YQBMAMCAFgwQHBALcAEBEiEAAAQA= + DragDrop\DropSink.cs + + + + + + BYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + DragDrop\DropSink.cs + + + + + + IACgGiAAIBoAAAAAAAAAAAAAAALAAAAKgAQECIAEgAA= + DragDrop\DropSink.cs + + + + + + BAEAAAAAAAAAAAAAAAEQAAAAAAAAAAIAAAAAgAQAAEA= + DragDrop\DropSink.cs + + + + + + AQAAEAEABAAAAAAAEAAAAAAAAAIAAgACgAAAAAAAQBA= + DragDrop\OLVDataObject.cs + + + + + + AAAAAAAAAAAAAgIAAAACAAAEQAAAAAAAAAAAAAAAAAA= + FastDataListView.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAA= + FastObjectListView.cs + + + + + + ABAAAAgAQAQAABAgAAASQ4AQAAIEAAAAAAAAAEIAgAA= + FastObjectListView.cs + + + + + + AAAAAAAQAAAAEAQEBAAAAAQAgAAAAIAAAAAAAAAAAAA= + Filtering\Cluster.cs + + + + + + + AAAAAAAEAAAAAAAAAhBABAgAQAAIKAQBAQAAAAAAoIA= + Filtering\ClusteringStrategy.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQBAAAAAAAAAAA= + Filtering\ClustersFromGroupsStrategy.cs + + + + + + AAAAAAIAAAACAAAAAAAACAAAAQgAAAQBAAAAAAAAAAA= + Filtering\DateTimeClusteringStrategy.cs + + + + + + SAEAADAQgEEAAQAIAAgQIAAAAFQAAAAAAAAAoAAAAAE= + Filtering\FilterMenuBuilder.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + + AAAAAAAAAEAAAABAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + + AAAAAAAAAAgAAAAAAIAAAACAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + + AAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + + AAAAAAAAAAAAAAAAAgAAAAIAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAAAAAAAAAABAAAAAQAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAgAAACAAAABAAABAAgAQACABABAAAAyEIAAIgSgAA= + Filtering\TextMatchFilter.cs + + + + + + EgAAQTAAZAEgWGAASFwIIEYECGBGAQKAQEEGAQJAAEE= + Implementation\Attributes.cs + + + + + + AAIAAAAAAAQAAAAAAAAAAAAAQAAAAAAAAABQAAAAAAA= + Implementation\Comparers.cs + + + + + + + AAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA= + Implementation\Comparers.cs + + + + + + + AAIAAAAAAAQAAAAAAAAAAAAAQAAAAAAAAABQAAAAAAA= + Implementation\Comparers.cs + + + + + + + AZggAAAwAGAAAgACQAYCBCAEICACACUEhAEAQKBAgQg= + Implementation\DataSourceAdapter.cs + + + + + + + 5/7+//3///fX/+//+///f/3//f/37N////+//7+///8= + Implementation\Enums.cs + + + + + + + ASgQyXBAABICBAAAAIAAACCEMAKBQAOAABDAgAUpAQA= + Implementation\Events.cs + + + + + + ABEAEAAFQAAAABAABABQEAQAQAAAECAAAAAgAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAABAAAIAAAAAAAAAEAAAQAAAAABAAAAAAAABAAIAA= + Implementation\Events.cs + + + + + + AAQABAAAAAQQAAAAAAEAAAQAAAAABAAAAAAgABQAIAE= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAEAAAAAAAAAAAAEAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAEAAAAAAAAAAAAAAAEAAAQAAAAAAAAAAAAAAAQAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAgAAAAIAAAAAAAAAAAAgAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAQAAAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAEA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAQAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAMABAAAAQgAZAFAAGUARAAAAAgAgAAIAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + CAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAQAAAAAAAEGAAAAAAAAAAAgAACAIAAAACAAHAAA= + Implementation\Events.cs + + + + + + AAAgAAAAMAAAAAAAgARABAAEUAQIAAAAiAgAAIAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAABAAAAAQAAAAAAIiAgACIAIAAA= + Implementation\Events.cs + + + + + + AEAAAAAAAAAAAAAAgAQAAAAEAAAAAQAAgAkEAIAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAEAAAAAAAAABABAAAUAQAAAAAAAgAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAIgAAAGJACRCAAEEABEAAAAJACBAAAAAAgIAAAAAAA= + Implementation\Events.cs + + + + + + QAAAEAAAAAAAABAABABQEAQAQAAAEAAAAAAAAEAAAAA= + Implementation\Events.cs + + + + + + QAAAAEAAAAAAAAAAgAAAAIAAAAAAAAAAAIAAAACAAAA= + Implementation\Events.cs + + + + + + QAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + QAAAgEAACggAgAAIAAAAAEAAAIAAAAAAAAAAAAAAAII= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAgAAAAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAgAAAAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAgAAAAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAgAAAAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAIMAAAACBEAEAoQAAAKAACAAUhAAgRIQAIAE= + Implementation\GroupingParameters.cs + + + + + + bEYChOwmAAQgiCQEQBgEAMwAEAAMAEQMgEFAFODAGhM= + Implementation\Groups.cs + + + + + + AAAAgAAAJAAAAAQEABCAAAAAhAAAAACAAAAAAAIAAAE= + Implementation\Munger.cs + + + + + + AAAIACAAJAAAgAAEACAAAAAABAAAIAAAAAEAAAAAEAA= + Implementation\Munger.cs + + + + + + AAEAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAABAAA= + Implementation\Munger.cs + + + + + + cChxFKmMjlNGI/LfZWKToPLMK45gioYxDANnzL7yfN4= + Implementation\NativeMethods.cs + + + + + + AAEAAAAAAAAEAAAACAAAAAAAQAAAAAAAAAAAAAAAAAA= + Implementation\NullableDictionary.cs + + + + + + ABAAAAAAAABEAACAAAAAAAAQABAAEgAQAAIAASAAAgE= + Implementation\OLVListItem.cs + + + + + + AAAAAAAAAAAEAAAAAAAAAAAAABAIAAAQCAgACSAAAAk= + Implementation\OLVListSubItem.cs + + + + + + AAAAgEAAEAgAAABEAAZABAAGAAQAEAAAgAAAAAAIAAA= + Implementation\OlvListViewHitTestInfo.cs + + + + + + AAAAAAAAAAAAAACAAAAEAAAAAAAAAAAEAAAACAAAAAA= + Implementation\VirtualGroups.cs + + + + + + + AAAAABAAAAAAAACAAAAAAAAAAAAAAAAEAAAACAAAAAA= + Implementation\VirtualGroups.cs + + + + + + AIAAAAAAAAAAAAAAAAAAAgAIAAAQAAAAAAAEAEACAAA= + Implementation\VirtualGroups.cs + + + + + + + ABAAAAAAgAAAAAAgAAASQgAQAAAEAAAAAAAAAIAAgAA= + Implementation\VirtualListDataSource.cs + + + + + + + AABAAAAAAAAAAAAAAABCAAAAAAAEAAAAAAAAAAAAAAA= + Implementation\VirtualListDataSource.cs + + + + + + MgARTxAAJEcEWCbkoNwJbE6WTnSOEnOKQhSWGDJgsFk= + OLVColumn.cs + + + + + + AACgAIAABAAAAIABACCiAIAgAACAAgAAABAIAAAAgAE= + Rendering\Adornments.cs + + + + + + ABAAAAAAAIAAAAAAAEAAAAAAEAAAABAAAEABAAAAAAA= + Rendering\Adornments.cs + + + + + + QAIggAAEIAAgBIAIAAIASEACIABAACIIAAMACCADAsA= + Rendering\Adornments.cs + + + + + + AAEAAAAAAAABAgAAAQAABAAAAAQBAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + + AgAAAAAAEAAAAgAAAAAAAAAAAAAAAAAAAAAQAAIAAAA= + Rendering\Decorations.cs + + + + + + YAgAgCABAAAgA4AAAAIAgAAAAAAAAAAAAAIAACBIgAA= + Rendering\Decorations.cs + + + + + + AAAAgAAgAAAAIAAAAAAAAAAAAAAAAAAAAAAAAABAAIA= + Rendering\Decorations.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAA= + Rendering\Decorations.cs + + + + + + QAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAEAAAAA= + Rendering\Decorations.cs + + + + + + AAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + AAAAAAAAAAABAgAAAQAABAAAAAQAAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + + AAAAAAAAAAABAgAAAQAABAAAAAQAAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + + AAAAAAAAAAAAAgAAAAAAAIAAAACAAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + + AAAAAAAAAAAAAgAAAAAAABAAAAAQAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + + AAAAAAAAAIAAAgAAAAAAABAAAAAQAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + + AAAAAAAAAAAAAgAAAAIAAAACAAAAAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + AAAAAQAAAABAAAAAABAAAAAAAAAAABAAAAAAAAAAAAA= + Rendering\Renderers.cs + + + + + + + AAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Rendering\Renderers.cs + + + + + + AxLAQSEQZQhhAmA5IRBAASSQUhQgkFAAgJDGAAMDE8I= + Rendering\Renderers.cs + + + + + + QAggCAEAAEAAAIAwAAKAEAIQABAAEAAAAAAAAAAKAAA= + Rendering\Renderers.cs + + + + + + AAIAEBAAAQAAAAgAABAAEAAAAAAAAAAAABAAAAAAAAA= + Rendering\Renderers.cs + + + + + + AAAAAAQBIAAAAAAAAAAAAAAAAAAAAAAACBAAAAIAAAA= + Rendering\Renderers.cs + + + + + + AAAgAAAAAAAAAAAAAIAAAAAgIAAAAABBABAAAAAEIAA= + Rendering\Renderers.cs + + + + + + AAIAAQAAAAEAAgAAEAIAABAAAAAAAAAAIAEAACADAAA= + Rendering\Styles.cs + + + + + + + AAIAAQAAAAEAAgAAAAIAAAAAAAAAAAAAAAEAAAADAAA= + Rendering\Styles.cs + + + + + + + AAAAAAAAQAiAAAAAgAIAAAAAAAAIBAAgAAAAAAAAAAA= + Rendering\Styles.cs + + + + + + AAIAAQAAAAEAAAAAAAAAAAACACAAAgAgAAEAAAADAAA= + Rendering\Styles.cs + + + + + + AAAAAAAQAAgAAAAAAAAAAICAAIAIAAAABAAAAQAEAAA= + Rendering\Styles.cs + + + + + + BgCkgAAARAoAAEAyEAAAAAIAAAAIAhCAAQIERAgAASA= + SubControls\GlassPanelForm.cs + + + + + + MAAIOQAUABAAAACQiBQCKRgAACECAAoAwAAQxJAACaE= + SubControls\HeaderControl.cs + + + + + + AkAACAgAACCACAAAAAAAAIAAwAAIAAQCAAAAAAAAAAA= + SubControls\ToolStripCheckedListBox.cs + + + + + + CkoAByAwQQAggEvSAAIQiIWIBELAYOIpiAKQUIQDqEA= + SubControls\ToolTipControl.cs + + + + + + AAAAAEAAFDAAQTAUAACIBAoWEAAAAAAoAAAAAIEAAgA= + Utilities\ColumnSelectionForm.cs + + + + + + AAABAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAACAAAAAA= + Utilities\Generator.cs + + + + + + AAAAwABEAAAAAACIAIAQEAABEAAAAAEEggAAAAACQAA= + Utilities\TypedObjectListView.cs + + + + + + AAAACAAAAEAEAABAAEAQCAAAQAAEAEAAAAgAAAAAAAA= + Utilities\TypedObjectListView.cs + + + + + + AVkQQAAAAGIEAQAkKkHAEAgRH4gBgAGOCCEigAAgIAQ= + VirtualObjectListView.cs + + + + + + AAEAAAABACAEAQAEAAAAAAAgAQCAAAAAAAMIQAABEAA= + ObjectListView.DesignTime.cs + + + + + + AAAAAAAAAAAAAABAAAABAAAAAAAAQAAAAAAAAAAAAAA= + ObjectListView.DesignTime.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAIAAAAAAAA= + ObjectListView.DesignTime.cs + + + + + + AAAAAAAAAAAAAAIAAAABEAAAgQAAAAAAAAAAQAIAAIg= + + + + + + AAAAAAAAAAAAAgIAAAACAAEGAAAAAEAAAAAAABAAQAA= + DataTreeListView.cs + + + + + + BAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAQAA= + DragDrop\DragSource.cs + + + + + + AAAAAAAAAAAQQAAAAAIAEAAAAAEFAAAAgAAAAAAAAAA= + DragDrop\DropSink.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAQAAAAAAAAAAAAAAQAgAAAAAAAAAAAAAAAAAA= + Filtering\ICluster.cs + + + + + + AAAAAAAAAAAAAAAAABBAAAAAAAAAAAQBAQAAAAAAAAA= + Filtering\IClusteringStrategy.cs + + + + + + AAAAAAAAAAAAAACAAAAEAAAAAAAAAAAEAAAACAAAAAA= + Implementation\VirtualGroups.cs + + + + + + AIAAAAAAAAAAAAAAAAAAAgAIAAAQAAAAAAAEAEAAAAA= + Implementation\VirtualGroups.cs + + + + + + ABAAAAAAAAAAAAAgAAASAgAQAAAEAAAAAAAAAAAAgAA= + Implementation\VirtualListDataSource.cs + + + + + + AAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA= + Implementation\VirtualListDataSource.cs + + + + + + AAAAAAAAAAAAAAAAAQAAAAAAAAQAAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + AAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + AAAAAQAAAABAAAAAABAAAAAAAAAAABAAAAAAAAAAAAA= + Rendering\Renderers.cs + + + + + + AAAAAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAADAAA= + Rendering\Styles.cs + + + + + + AAAgAAAACAAAAAAAAAAAAAAAAAAQAAAAAAAAAEAAAAA= + CellEditing\CellEditKeyEngine.cs + + + + + + gAEAAAAQCAIAAAAAAIAAAAAAAUAQIABAAEAgAAAgACA= + CellEditing\CellEditKeyEngine.cs + + + + + + AAAAAQAAAAABAAAAAAAAAAAEAAQBBAAAAAIgAAEAAAA= + DragDrop\DropSink.cs + + + + + + AAAAAAAAJAAAAAAAAAAAAAIAAAAAACIEAAAAAAAAAAA= + Filtering\DateTimeClusteringStrategy.cs + + + + + + AEAAAAAAAAAAABAAEAQAARAAIQAAAAQAAAAAAEAAAAA= + Implementation\Groups.cs + + + + + + AgAAgAAAwABAAAAAAAUAAAIAgQAAAAAEACAgAAAFAAA= + Implementation\Groups.cs + + + + + + AAAAAAQAAAAAAAAAAAAAAQAAAAAAEAAAAIAAAAAAAAA= + Implementation\Groups.cs + + + + + + ASAAEEAAAAAAAAQAEAAAAAAAAAAAABAAAAAACAAAEAA= + Implementation\OlvListViewHitTestInfo.cs + + + + + + AAAEAAAAAAAAAAAAAAAAAAAQAAAABAAAAAAAAAAAAAA= + CellEditing\EditorRegistry.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAgA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAQAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAgAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAEAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAQABgAAAAACAQAAAAAAAAAAAAAAAAAAAAAAgAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAACAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAABAAAAAAgACAAAAACAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAABAAAAAAAAAA= + Implementation\Events.cs + + + + \ No newline at end of file diff --git a/ObjectListView/Implementation/Attributes.cs b/ObjectListView/Implementation/Attributes.cs new file mode 100644 index 00000000..fd8df368 --- /dev/null +++ b/ObjectListView/Implementation/Attributes.cs @@ -0,0 +1,335 @@ +/* + * Attributes - Attributes that can be attached to properties of models to allow columns to be + * built from them directly + * + * Author: Phillip Piper + * Date: 15/08/2009 22:01 + * + * Change log: + * v2.6 + * 2012-08-16 JPP - Added [OLVChildren] and [OLVIgnore] + * - OLV attributes can now only be set on properties + * v2.4 + * 2010-04-14 JPP - Allow Name property to be set + * + * v2.3 + * 2009-08-15 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// This attribute is used to mark a property of a model + /// class that should be noticed by Generator class. + /// + /// + /// All the attributes of this class match their equivalent properties on OLVColumn. + /// + [AttributeUsage(AttributeTargets.Property)] + public class OLVColumnAttribute : Attribute + { + #region Constructor + + // There are several property where we actually want nullable value (bool?, int?), + // but it seems attribute properties can't be nullable types. + // So we explicitly track if those properties have been set. + + /// + /// Create a new OLVColumnAttribute + /// + public OLVColumnAttribute() { + } + + /// + /// Create a new OLVColumnAttribute with the given title + /// + /// The title of the column + public OLVColumnAttribute(string title) { + this.Title = title; + } + + #endregion + + #region Public properties + + /// + /// + /// + public string AspectToStringFormat { + get { return aspectToStringFormat; } + set { aspectToStringFormat = value; } + } + private string aspectToStringFormat; + + /// + /// + /// + public bool CheckBoxes { + get { return checkBoxes; } + set { + checkBoxes = value; + this.IsCheckBoxesSet = true; + } + } + private bool checkBoxes; + internal bool IsCheckBoxesSet = false; + + /// + /// + /// + public int DisplayIndex { + get { return displayIndex; } + set { displayIndex = value; } + } + private int displayIndex = -1; + + /// + /// + /// + public bool FillsFreeSpace { + get { return fillsFreeSpace; } + set { fillsFreeSpace = value; } + } + private bool fillsFreeSpace; + + /// + /// + /// + public int FreeSpaceProportion { + get { return freeSpaceProportion; } + set { + freeSpaceProportion = value; + IsFreeSpaceProportionSet = true; + } + } + private int freeSpaceProportion; + internal bool IsFreeSpaceProportionSet = false; + + /// + /// An array of IComparables that mark the cutoff points for values when + /// grouping on this column. + /// + public object[] GroupCutoffs { + get { return groupCutoffs; } + set { groupCutoffs = value; } + } + private object[] groupCutoffs; + + /// + /// + /// + public string[] GroupDescriptions { + get { return groupDescriptions; } + set { groupDescriptions = value; } + } + private string[] groupDescriptions; + + /// + /// + /// + public string GroupWithItemCountFormat { + get { return groupWithItemCountFormat; } + set { groupWithItemCountFormat = value; } + } + private string groupWithItemCountFormat; + + /// + /// + /// + public string GroupWithItemCountSingularFormat { + get { return groupWithItemCountSingularFormat; } + set { groupWithItemCountSingularFormat = value; } + } + private string groupWithItemCountSingularFormat; + + /// + /// + /// + public bool Hyperlink { + get { return hyperlink; } + set { hyperlink = value; } + } + private bool hyperlink; + + /// + /// + /// + public string ImageAspectName { + get { return imageAspectName; } + set { imageAspectName = value; } + } + private string imageAspectName; + + /// + /// + /// + public bool IsEditable { + get { return isEditable; } + set { + isEditable = value; + this.IsEditableSet = true; + } + } + private bool isEditable = true; + internal bool IsEditableSet = false; + + /// + /// + /// + public bool IsVisible { + get { return isVisible; } + set { isVisible = value; } + } + private bool isVisible = true; + + /// + /// + /// + public bool IsTileViewColumn { + get { return isTileViewColumn; } + set { isTileViewColumn = value; } + } + private bool isTileViewColumn; + + /// + /// + /// + public int MaximumWidth { + get { return maximumWidth; } + set { maximumWidth = value; } + } + private int maximumWidth = -1; + + /// + /// + /// + public int MinimumWidth { + get { return minimumWidth; } + set { minimumWidth = value; } + } + private int minimumWidth = -1; + + /// + /// + /// + public String Name { + get { return name; } + set { name = value; } + } + private String name; + + /// + /// + /// + public HorizontalAlignment TextAlign { + get { return this.textAlign; } + set { + this.textAlign = value; + IsTextAlignSet = true; + } + } + private HorizontalAlignment textAlign = HorizontalAlignment.Left; + internal bool IsTextAlignSet = false; + + /// + /// + /// + public String Tag { + get { return tag; } + set { tag = value; } + } + private String tag; + + /// + /// + /// + public String Title { + get { return title; } + set { title = value; } + } + private String title; + + /// + /// + /// + public String ToolTipText { + get { return toolTipText; } + set { toolTipText = value; } + } + private String toolTipText; + + /// + /// + /// + public bool TriStateCheckBoxes { + get { return triStateCheckBoxes; } + set { + triStateCheckBoxes = value; + this.IsTriStateCheckBoxesSet = true; + } + } + private bool triStateCheckBoxes; + internal bool IsTriStateCheckBoxesSet = false; + + /// + /// + /// + public bool UseInitialLetterForGroup { + get { return useInitialLetterForGroup; } + set { useInitialLetterForGroup = value; } + } + private bool useInitialLetterForGroup; + + /// + /// + /// + public int Width { + get { return width; } + set { width = value; } + } + private int width = 150; + + #endregion + } + + /// + /// Properties marked with [OLVChildren] will be used as the children source in a TreeListView. + /// + [AttributeUsage(AttributeTargets.Property)] + public class OLVChildrenAttribute : Attribute + { + + } + + /// + /// Properties marked with [OLVIgnore] will not have columns generated for them. + /// + [AttributeUsage(AttributeTargets.Property)] + public class OLVIgnoreAttribute : Attribute + { + + } +} diff --git a/ObjectListView/Implementation/Comparers.cs b/ObjectListView/Implementation/Comparers.cs new file mode 100644 index 00000000..1ec33d03 --- /dev/null +++ b/ObjectListView/Implementation/Comparers.cs @@ -0,0 +1,330 @@ +/* + * Comparers - Various Comparer classes used within ObjectListView + * + * Author: Phillip Piper + * Date: 25/11/2008 17:15 + * + * Change log: + * v2.8.1 + * 2014-12-03 JPP - Added StringComparer + * v2.3 + * 2009-08-24 JPP - Added OLVGroupComparer + * 2009-06-01 JPP - ModelObjectComparer would crash if secondary sort column was null. + * 2008-12-20 JPP - Fixed bug with group comparisons when a group key was null (SF#2445761) + * 2008-11-25 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// ColumnComparer is the workhorse for all comparison between two values of a particular column. + /// If the column has a specific comparer, use that to compare the values. Otherwise, do + /// a case insensitive string compare of the string representations of the values. + /// + /// This class inherits from both IComparer and its generic counterpart + /// so that it can be used on untyped and typed collections. + /// This is used by normal (non-virtual) ObjectListViews. Virtual lists use + /// ModelObjectComparer + /// + public class ColumnComparer : IComparer, IComparer + { + /// + /// Gets or sets the method that will be used to compare two strings. + /// The default is to compare on the current culture, case-insensitive + /// + public static StringCompareDelegate StringComparer + { + get { return stringComparer; } + set { stringComparer = value; } + } + private static StringCompareDelegate stringComparer; + + /// + /// Create a ColumnComparer that will order the rows in a list view according + /// to the values in a given column + /// + /// The column whose values will be compared + /// The ordering for column values + public ColumnComparer(OLVColumn col, SortOrder order) + { + this.column = col; + this.sortOrder = order; + } + + /// + /// Create a ColumnComparer that will order the rows in a list view according + /// to the values in a given column, and by a secondary column if the primary + /// column is equal. + /// + /// The column whose values will be compared + /// The ordering for column values + /// The column whose values will be compared for secondary sorting + /// The ordering for secondary column values + public ColumnComparer(OLVColumn col, SortOrder order, OLVColumn col2, SortOrder order2) + : this(col, order) + { + // There is no point in secondary sorting on the same column + if (col != col2) + this.secondComparer = new ColumnComparer(col2, order2); + } + + /// + /// Compare two rows + /// + /// row1 + /// row2 + /// An ordering indication: -1, 0, 1 + public int Compare(object x, object y) + { + return this.Compare((OLVListItem)x, (OLVListItem)y); + } + + /// + /// Compare two rows + /// + /// row1 + /// row2 + /// An ordering indication: -1, 0, 1 + public int Compare(OLVListItem x, OLVListItem y) + { + if (this.sortOrder == SortOrder.None) + return 0; + + int result = 0; + object x1 = this.column.GetValue(x.RowObject); + object y1 = this.column.GetValue(y.RowObject); + + // Handle nulls. Null values come last + bool xIsNull = (x1 == null || x1 == System.DBNull.Value); + bool yIsNull = (y1 == null || y1 == System.DBNull.Value); + if (xIsNull || yIsNull) { + if (xIsNull && yIsNull) + result = 0; + else + result = (xIsNull ? -1 : 1); + } else { + result = this.CompareValues(x1, y1); + } + + if (this.sortOrder == SortOrder.Descending) + result = 0 - result; + + // If the result was equality, use the secondary comparer to resolve it + if (result == 0 && this.secondComparer != null) + result = this.secondComparer.Compare(x, y); + + return result; + } + + /// + /// Compare the actual values to be used for sorting + /// + /// The aspect extracted from the first row + /// The aspect extracted from the second row + /// An ordering indication: -1, 0, 1 + public int CompareValues(object x, object y) + { + // Force case insensitive compares on strings + String xAsString = x as String; + if (xAsString != null) + return CompareStrings(xAsString, y as String); + + IComparable comparable = x as IComparable; + return comparable != null ? comparable.CompareTo(y) : 0; + } + + private static int CompareStrings(string x, string y) + { + if (StringComparer == null) + return String.Compare(x, y, StringComparison.CurrentCultureIgnoreCase); + else + return StringComparer(x, y); + } + + private OLVColumn column; + private SortOrder sortOrder; + private ColumnComparer secondComparer; + } + + + /// + /// This comparer sort list view groups. OLVGroups have a "SortValue" property, + /// which is used if present. Otherwise, the titles of the groups will be compared. + /// + public class OLVGroupComparer : IComparer + { + /// + /// Create a group comparer + /// + /// The ordering for column values + public OLVGroupComparer(SortOrder order) { + this.sortOrder = order; + } + + /// + /// Compare the two groups. OLVGroups have a "SortValue" property, + /// which is used if present. Otherwise, the titles of the groups will be compared. + /// + /// group1 + /// group2 + /// An ordering indication: -1, 0, 1 + public int Compare(OLVGroup x, OLVGroup y) { + // If we can compare the sort values, do that. + // Otherwise do a case insensitive compare on the group header. + int result; + if (x.SortValue != null && y.SortValue != null) + result = x.SortValue.CompareTo(y.SortValue); + else + result = String.Compare(x.Header, y.Header, StringComparison.CurrentCultureIgnoreCase); + + if (this.sortOrder == SortOrder.Descending) + result = 0 - result; + + return result; + } + + private SortOrder sortOrder; + } + + /// + /// This comparer can be used to sort a collection of model objects by a given column + /// + /// + /// This is used by virtual ObjectListViews. Non-virtual lists use + /// ColumnComparer + /// + public class ModelObjectComparer : IComparer, IComparer + { + /// + /// Gets or sets the method that will be used to compare two strings. + /// The default is to compare on the current culture, case-insensitive + /// + public static StringCompareDelegate StringComparer + { + get { return stringComparer; } + set { stringComparer = value; } + } + private static StringCompareDelegate stringComparer; + + /// + /// Create a model object comparer + /// + /// + /// + public ModelObjectComparer(OLVColumn col, SortOrder order) + { + this.column = col; + this.sortOrder = order; + } + + /// + /// Create a model object comparer with a secondary sorting column + /// + /// + /// + /// + /// + public ModelObjectComparer(OLVColumn col, SortOrder order, OLVColumn col2, SortOrder order2) + : this(col, order) + { + // There is no point in secondary sorting on the same column + if (col != col2 && col2 != null && order2 != SortOrder.None) + this.secondComparer = new ModelObjectComparer(col2, order2); + } + + /// + /// Compare the two model objects + /// + /// + /// + /// + public int Compare(object x, object y) + { + int result = 0; + object x1 = this.column.GetValue(x); + object y1 = this.column.GetValue(y); + + if (this.sortOrder == SortOrder.None) + return 0; + + // Handle nulls. Null values come last + bool xIsNull = (x1 == null || x1 == System.DBNull.Value); + bool yIsNull = (y1 == null || y1 == System.DBNull.Value); + if (xIsNull || yIsNull) { + if (xIsNull && yIsNull) + result = 0; + else + result = (xIsNull ? -1 : 1); + } else { + result = this.CompareValues(x1, y1); + } + + if (this.sortOrder == SortOrder.Descending) + result = 0 - result; + + // If the result was equality, use the secondary comparer to resolve it + if (result == 0 && this.secondComparer != null) + result = this.secondComparer.Compare(x, y); + + return result; + } + + /// + /// Compare the actual values + /// + /// + /// + /// + public int CompareValues(object x, object y) + { + // Force case insensitive compares on strings + String xStr = x as String; + if (xStr != null) + return CompareStrings(xStr, y as String); + + IComparable comparable = x as IComparable; + return comparable != null ? comparable.CompareTo(y) : 0; + } + + private static int CompareStrings(string x, string y) + { + if (StringComparer == null) + return String.Compare(x, y, StringComparison.CurrentCultureIgnoreCase); + else + return StringComparer(x, y); + } + + private OLVColumn column; + private SortOrder sortOrder; + private ModelObjectComparer secondComparer; + + #region IComparer Members + + #endregion + } + +} \ No newline at end of file diff --git a/ObjectListView/Implementation/DataSourceAdapter.cs b/ObjectListView/Implementation/DataSourceAdapter.cs new file mode 100644 index 00000000..80f6da30 --- /dev/null +++ b/ObjectListView/Implementation/DataSourceAdapter.cs @@ -0,0 +1,628 @@ +/* + * DataSourceAdapter - A helper class that translates DataSource events for an ObjectListView + * + * Author: Phillip Piper + * Date: 20/09/2010 7:42 AM + * + * Change log: + * 2018-04-30 JPP - Sanity check upper limit against CurrencyManager rather than ListView just in + * case the CurrencyManager has gotten ahead of the ListView's contents. + * v2.9 + * 2015-10-31 JPP - Put back sanity check on upper limit of source items + * 2015-02-02 JPP - Made CreateColumnsFromSource() only rebuild columns when new ones were added + * v2.8.1 + * 2014-11-23 JPP - Honour initial CurrencyManager.Position when setting DataSource. + * 2014-10-27 JPP - Fix issue where SelectedObject was not sync'ed with CurrencyManager.Position (SF #129) + * v2.6 + * 2012-08-16 JPP - Unify common column creation functionality with Generator when possible + * + * 2010-09-20 JPP - Initial version + * + * Copyright (C) 2010-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.ComponentModel; +using System.Data; +using System.Windows.Forms; +using System.Diagnostics; + +namespace BrightIdeasSoftware +{ + /// + /// A helper class that translates DataSource events for an ObjectListView + /// + public class DataSourceAdapter : IDisposable + { + #region Life and death + + /// + /// Make a DataSourceAdapter + /// + public DataSourceAdapter(ObjectListView olv) { + if (olv == null) throw new ArgumentNullException("olv"); + + this.ListView = olv; + // ReSharper disable once DoNotCallOverridableMethodsInConstructor + this.BindListView(this.ListView); + } + + /// + /// Finalize this object + /// + ~DataSourceAdapter() { + this.Dispose(false); + } + + /// + /// Release all the resources used by this instance + /// + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Release all the resources used by this instance + /// + public virtual void Dispose(bool fromUser) { + this.UnbindListView(this.ListView); + this.UnbindDataSource(); + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets whether or not columns will be automatically generated to show the + /// columns when the DataSource is set. + /// + /// This must be set before the DataSource is set. It has no effect afterwards. + public bool AutoGenerateColumns { + get { return this.autoGenerateColumns; } + set { this.autoGenerateColumns = value; } + } + private bool autoGenerateColumns = true; + + /// + /// Get or set the DataSource that will be displayed in this list view. + /// + public virtual Object DataSource { + get { return dataSource; } + set { + dataSource = value; + this.RebindDataSource(true); + } + } + private Object dataSource; + + /// + /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data. + /// + /// If the data source is not a DataSet or DataViewManager, this property has no effect + public virtual string DataMember { + get { return dataMember; } + set { + if (dataMember != value) { + dataMember = value; + RebindDataSource(); + } + } + } + private string dataMember = ""; + + /// + /// Gets the ObjectListView upon which this adaptor will operate + /// + public ObjectListView ListView { + get { return listView; } + internal set { listView = value; } + } + private ObjectListView listView; + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the currency manager which is handling our binding context + /// + protected CurrencyManager CurrencyManager { + get { return currencyManager; } + set { currencyManager = value; } + } + private CurrencyManager currencyManager; + + #endregion + + #region Binding and unbinding + + /// + /// + /// + /// + protected virtual void BindListView(ObjectListView olv) { + if (olv == null) + return; + + olv.Freezing += new EventHandler(HandleListViewFreezing); + olv.SelectionChanged += new EventHandler(HandleListViewSelectionChanged); + olv.BindingContextChanged += new EventHandler(HandleListViewBindingContextChanged); + } + + /// + /// + /// + /// + protected virtual void UnbindListView(ObjectListView olv) { + if (olv == null) + return; + + olv.Freezing -= new EventHandler(HandleListViewFreezing); + olv.SelectionChanged -= new EventHandler(HandleListViewSelectionChanged); + olv.BindingContextChanged -= new EventHandler(HandleListViewBindingContextChanged); + } + + /// + /// + /// + protected virtual void BindDataSource() { + if (this.CurrencyManager == null) + return; + + this.CurrencyManager.MetaDataChanged += new EventHandler(HandleCurrencyManagerMetaDataChanged); + this.CurrencyManager.PositionChanged += new EventHandler(HandleCurrencyManagerPositionChanged); + this.CurrencyManager.ListChanged += new ListChangedEventHandler(CurrencyManagerListChanged); + } + + /// + /// + /// + protected virtual void UnbindDataSource() { + if (this.CurrencyManager == null) + return; + + this.CurrencyManager.MetaDataChanged -= new EventHandler(HandleCurrencyManagerMetaDataChanged); + this.CurrencyManager.PositionChanged -= new EventHandler(HandleCurrencyManagerPositionChanged); + this.CurrencyManager.ListChanged -= new ListChangedEventHandler(CurrencyManagerListChanged); + } + + #endregion + + #region Initialization + + /// + /// Our data source has changed. Figure out how to handle the new source + /// + protected virtual void RebindDataSource() { + RebindDataSource(false); + } + + /// + /// Our data source has changed. Figure out how to handle the new source + /// + protected virtual void RebindDataSource(bool forceDataInitialization) { + + CurrencyManager tempCurrencyManager = null; + if (this.ListView != null && this.ListView.BindingContext != null && this.DataSource != null) { + tempCurrencyManager = this.ListView.BindingContext[this.DataSource, this.DataMember] as CurrencyManager; + } + + // Has our currency manager changed? + if (this.CurrencyManager != tempCurrencyManager) { + this.UnbindDataSource(); + this.CurrencyManager = tempCurrencyManager; + this.BindDataSource(); + + // Our currency manager has changed so we have to initialize a new data source + forceDataInitialization = true; + } + + if (forceDataInitialization) + InitializeDataSource(); + } + + /// + /// The data source for this control has changed. Reconfigure the control for the new source + /// + protected virtual void InitializeDataSource() { + if (this.ListView.Frozen || this.CurrencyManager == null) + return; + + this.CreateColumnsFromSource(); + this.CreateMissingAspectGettersAndPutters(); + this.SetListContents(); + this.ListView.AutoSizeColumns(); + + // Fake a position change event so that the control matches any initial Position + this.HandleCurrencyManagerPositionChanged(null, null); + } + + /// + /// Take the contents of the currently bound list and put them into the control + /// + protected virtual void SetListContents() { + this.ListView.Objects = this.CurrencyManager.List; + } + + /// + /// Create columns for the listview based on what properties are available in the data source + /// + /// + /// This method will create columns if there is not already a column displaying that property. + /// + protected virtual void CreateColumnsFromSource() { + if (this.CurrencyManager == null) + return; + + // Don't generate any columns in design mode. If we do, the user will see them, + // but the Designer won't know about them and won't persist them, which is very confusing + if (this.ListView.IsDesignMode) + return; + + // Don't create columns if we've been told not to + if (!this.AutoGenerateColumns) + return; + + // Use a Generator to create columns + Generator generator = Generator.Instance as Generator ?? new Generator(); + + PropertyDescriptorCollection properties = this.CurrencyManager.GetItemProperties(); + if (properties.Count == 0) + return; + + bool wereColumnsAdded = false; + foreach (PropertyDescriptor property in properties) { + + if (!this.ShouldCreateColumn(property)) + continue; + + // Create a column + OLVColumn column = generator.MakeColumnFromPropertyDescriptor(property); + this.ConfigureColumn(column, property); + + // Add it to our list + this.ListView.AllColumns.Add(column); + wereColumnsAdded = true; + } + + if (wereColumnsAdded) + generator.PostCreateColumns(this.ListView); + } + + /// + /// Decide if a new column should be added to the control to display + /// the given property + /// + /// + /// + protected virtual bool ShouldCreateColumn(PropertyDescriptor property) { + + // Is there a column that already shows this property? If so, we don't show it again + if (this.ListView.AllColumns.Exists(delegate(OLVColumn x) { return x.AspectName == property.Name; })) + return false; + + // Relationships to other tables turn up as IBindibleLists. Don't make columns to show them. + // CHECK: Is this always true? What other things could be here? Constraints? Triggers? + if (property.PropertyType == typeof(IBindingList)) + return false; + + // Ignore anything marked with [OLVIgnore] + return property.Attributes[typeof(OLVIgnoreAttribute)] == null; + } + + /// + /// Configure the given column to show the given property. + /// The title and aspect name of the column are already filled in. + /// + /// + /// + protected virtual void ConfigureColumn(OLVColumn column, PropertyDescriptor property) { + + column.LastDisplayIndex = this.ListView.AllColumns.Count; + + // If our column is a BLOB, it could be an image, so assign a renderer to draw it. + // CONSIDER: Is this a common enough case to warrant this code? + if (property.PropertyType == typeof(System.Byte[])) + column.Renderer = new ImageRenderer(); + } + + /// + /// Generate aspect getters and putters for any columns that are missing them (and for which we have + /// enough information to actually generate a getter) + /// + protected virtual void CreateMissingAspectGettersAndPutters() { + foreach (OLVColumn x in this.ListView.AllColumns) { + OLVColumn column = x; // stack based variable accessible from closures + if (column.AspectGetter == null && !String.IsNullOrEmpty(column.AspectName)) { + column.AspectGetter = delegate(object row) { + // In most cases, rows will be DataRowView objects + DataRowView drv = row as DataRowView; + if (drv == null) + return column.GetAspectByName(row); + return (drv.Row.RowState == DataRowState.Detached) ? null : drv[column.AspectName]; + }; + } + if (column.IsEditable && column.AspectPutter == null && !String.IsNullOrEmpty(column.AspectName)) { + column.AspectPutter = delegate(object row, object newValue) { + // In most cases, rows will be DataRowView objects + DataRowView drv = row as DataRowView; + if (drv == null) + column.PutAspectByName(row, newValue); + else { + if (drv.Row.RowState != DataRowState.Detached) + drv[column.AspectName] = newValue; + } + }; + } + } + } + + #endregion + + #region Event Handlers + + /// + /// CurrencyManager ListChanged event handler. + /// Deals with fine-grained changes to list items. + /// + /// + /// It's actually difficult to deal with these changes in a fine-grained manner. + /// If our listview is grouped, then any change may make a new group appear or + /// an old group disappear. It is rarely enough to simply update the affected row. + /// + /// + /// + protected virtual void CurrencyManagerListChanged(object sender, ListChangedEventArgs e) { + Debug.Assert(sender == this.CurrencyManager); + + // Ignore changes make while frozen, since we will do a complete rebuild when we unfreeze + if (this.ListView.Frozen) + return; + + //System.Diagnostics.Debug.WriteLine(e.ListChangedType); + Stopwatch sw = Stopwatch.StartNew(); + switch (e.ListChangedType) { + + case ListChangedType.Reset: + this.HandleListChangedReset(e); + break; + + case ListChangedType.ItemChanged: + this.HandleListChangedItemChanged(e); + break; + + case ListChangedType.ItemAdded: + this.HandleListChangedItemAdded(e); + break; + + // An item has gone away. + case ListChangedType.ItemDeleted: + this.HandleListChangedItemDeleted(e); + break; + + // An item has changed its index. + case ListChangedType.ItemMoved: + this.HandleListChangedItemMoved(e); + break; + + // Something has changed in the metadata. + // CHECK: When are these events actually fired? + case ListChangedType.PropertyDescriptorAdded: + case ListChangedType.PropertyDescriptorChanged: + case ListChangedType.PropertyDescriptorDeleted: + this.HandleListChangedMetadataChanged(e); + break; + } + sw.Stop(); + System.Diagnostics.Debug.WriteLine(String.Format("PERF - Processing {0} event on {1} rows took {2}ms", e.ListChangedType, this.ListView.GetItemCount(), sw.ElapsedMilliseconds)); + + } + + /// + /// Handle PropertyDescriptor* events + /// + /// + protected virtual void HandleListChangedMetadataChanged(ListChangedEventArgs e) { + this.InitializeDataSource(); + } + + /// + /// Handle ItemMoved event + /// + /// + protected virtual void HandleListChangedItemMoved(ListChangedEventArgs e) { + // When is this actually triggered? + this.InitializeDataSource(); + } + + /// + /// Handle the ItemDeleted event + /// + /// + protected virtual void HandleListChangedItemDeleted(ListChangedEventArgs e) { + this.InitializeDataSource(); + } + + /// + /// Handle an ItemAdded event. + /// + /// + protected virtual void HandleListChangedItemAdded(ListChangedEventArgs e) { + // We get this event twice if certain grid controls are used to add a new row to a + // datatable: once when the editing of a new row begins, and once again when that + // editing commits. (If the user cancels the creation of the new row, we never see + // the second creation.) We detect this by seeing if this is a view on a row in a + // DataTable, and if it is, testing to see if it's a new row under creation. + + Object newRow = this.CurrencyManager.List[e.NewIndex]; + DataRowView drv = newRow as DataRowView; + if (drv == null || !drv.IsNew) { + // Either we're not dealing with a view on a data table, or this is the commit + // notification. Either way, this is the final notification, so we want to + // handle the new row now! + this.InitializeDataSource(); + } + } + + /// + /// Handle the Reset event + /// + /// + protected virtual void HandleListChangedReset(ListChangedEventArgs e) { + // The whole list has changed utterly, so reload it. + this.InitializeDataSource(); + } + + /// + /// Handle ItemChanged event. This is triggered when a single item + /// has changed, so just refresh that one item. + /// + /// + /// Even in this simple case, we should probably rebuild the list. + /// For example, the change could put the item into its own new group. + protected virtual void HandleListChangedItemChanged(ListChangedEventArgs e) { + // A single item has changed, so just refresh that. + //System.Diagnostics.Debug.WriteLine(String.Format("HandleListChangedItemChanged: {0}, {1}", e.NewIndex, e.PropertyDescriptor.Name)); + + Object changedRow = this.CurrencyManager.List[e.NewIndex]; + this.ListView.RefreshObject(changedRow); + } + + /// + /// The CurrencyManager calls this if the data source looks + /// different. We just reload everything. + /// + /// + /// + /// + /// CHECK: Do we need this if we are handle ListChanged metadata events? + /// + protected virtual void HandleCurrencyManagerMetaDataChanged(object sender, EventArgs e) { + this.InitializeDataSource(); + } + + /// + /// Called by the CurrencyManager when the currently selected item + /// changes. We update the ListView selection so that we stay in sync + /// with any other controls bound to the same source. + /// + /// + /// + protected virtual void HandleCurrencyManagerPositionChanged(object sender, EventArgs e) { + int index = this.CurrencyManager.Position; + + // Make sure the index is sane (-1 pops up from time to time) + if (index < 0 || index >= this.CurrencyManager.Count) + return; + + // Avoid recursion. If we are currently changing the index, don't + // start the process again. + if (this.isChangingIndex) + return; + + try { + this.isChangingIndex = true; + this.ChangePosition(index); + } + finally { + this.isChangingIndex = false; + } + } + private bool isChangingIndex = false; + + /// + /// Change the control's position (which is it's currently selected row) + /// to the nth row in the dataset + /// + /// The index of the row to be selected + protected virtual void ChangePosition(int index) { + // We can't use the index directly, since our listview may be sorted + this.ListView.SelectedObject = this.CurrencyManager.List[index]; + + // THINK: Do we always want to bring it into view? + if (this.ListView.SelectedIndices.Count > 0) + this.ListView.EnsureVisible(this.ListView.SelectedIndices[0]); + } + + #endregion + + #region ObjectListView event handlers + + /// + /// Handle the selection changing in our ListView. + /// We need to tell our currency manager about the new position. + /// + /// + /// + protected virtual void HandleListViewSelectionChanged(object sender, EventArgs e) { + // Prevent recursion + if (this.isChangingIndex) + return; + + // Sanity + if (this.CurrencyManager == null) + return; + + // If only one item is selected, tell the currency manager which item is selected. + // CurrencyManager can't handle multiple selection so there's nothing we can do + // if more than one row is selected. + if (this.ListView.SelectedIndices.Count != 1) + return; + + try { + this.isChangingIndex = true; + + // We can't use the selectedIndex directly, since our listview may be sorted and/or filtered + // So we have to find the index of the selected object within the original list. + this.CurrencyManager.Position = this.CurrencyManager.List.IndexOf(this.ListView.SelectedObject); + } finally { + this.isChangingIndex = false; + } + } + + /// + /// Handle the frozenness of our ListView changing. + /// + /// + /// + protected virtual void HandleListViewFreezing(object sender, FreezeEventArgs e) { + if (!alreadyFreezing && e.FreezeLevel == 0) { + try { + alreadyFreezing = true; + this.RebindDataSource(true); + } finally { + alreadyFreezing = false; + } + } + } + private bool alreadyFreezing = false; + + /// + /// Handle a change to the BindingContext of our ListView. + /// + /// + /// + protected virtual void HandleListViewBindingContextChanged(object sender, EventArgs e) { + this.RebindDataSource(false); + } + + #endregion + } +} diff --git a/ObjectListView/Implementation/Delegates.cs b/ObjectListView/Implementation/Delegates.cs new file mode 100644 index 00000000..52da3665 --- /dev/null +++ b/ObjectListView/Implementation/Delegates.cs @@ -0,0 +1,168 @@ +/* + * Delegates - All delegate definitions used in ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * v2.10 + * 2015-12-30 JPP - Added CellRendererGetterDelegate + * v2.? + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2015 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Windows.Forms; +using System.Drawing; + +namespace BrightIdeasSoftware { + + #region Delegate declarations + + /// + /// These delegates are used to extract an aspect from a row object + /// + public delegate Object AspectGetterDelegate(Object rowObject); + + /// + /// These delegates are used to put a changed value back into a model object + /// + public delegate void AspectPutterDelegate(Object rowObject, Object newValue); + + /// + /// These delegates can be used to convert an aspect value to a display string, + /// instead of using the default ToString() + /// + public delegate string AspectToStringConverterDelegate(Object value); + + /// + /// These delegates are used to get the tooltip for a cell + /// + public delegate String CellToolTipGetterDelegate(OLVColumn column, Object modelObject); + + /// + /// These delegates are used to the state of the checkbox for a row object. + /// + /// + /// For reasons known only to someone in Microsoft, we can only set + /// a boolean on the ListViewItem to indicate it's "checked-ness", but when + /// we receive update events, we have to use a tristate CheckState. So we can + /// be told about an indeterminate state, but we can't set it ourselves. + /// + /// As of version 2.0, we can now return indeterminate state. + /// + public delegate CheckState CheckStateGetterDelegate(Object rowObject); + + /// + /// These delegates are used to get the state of the checkbox for a row object. + /// + /// + /// + public delegate bool BooleanCheckStateGetterDelegate(Object rowObject); + + /// + /// These delegates are used to put a changed check state back into a model object + /// + public delegate CheckState CheckStatePutterDelegate(Object rowObject, CheckState newValue); + + /// + /// These delegates are used to put a changed check state back into a model object + /// + /// + /// + /// + public delegate bool BooleanCheckStatePutterDelegate(Object rowObject, bool newValue); + + /// + /// These delegates are used to get the renderer for a particular cell + /// + public delegate IRenderer CellRendererGetterDelegate(Object rowObject, OLVColumn column); + + /// + /// The callbacks for RightColumnClick events + /// + public delegate void ColumnRightClickEventHandler(object sender, ColumnClickEventArgs e); + + /// + /// This delegate will be used to own draw header column. + /// + public delegate bool HeaderDrawingDelegate(Graphics g, Rectangle r, int columnIndex, OLVColumn column, bool isPressed, HeaderStateStyle stateStyle); + + /// + /// This delegate is called when a group has been created but not yet made + /// into a real ListViewGroup. The user can take this opportunity to fill + /// in lots of other details about the group. + /// + public delegate void GroupFormatterDelegate(OLVGroup group, GroupingParameters parms); + + /// + /// These delegates are used to retrieve the object that is the key of the group to which the given row belongs. + /// + public delegate Object GroupKeyGetterDelegate(Object rowObject); + + /// + /// These delegates are used to convert a group key into a title for the group + /// + public delegate string GroupKeyToTitleConverterDelegate(Object groupKey); + + /// + /// These delegates are used to get the tooltip for a column header + /// + public delegate String HeaderToolTipGetterDelegate(OLVColumn column); + + /// + /// These delegates are used to fetch the image selector that should be used + /// to choose an image for this column. + /// + public delegate Object ImageGetterDelegate(Object rowObject); + + /// + /// These delegates are used to draw a cell + /// + public delegate bool RenderDelegate(EventArgs e, Graphics g, Rectangle r, Object rowObject); + + /// + /// These delegates are used to fetch a row object for virtual lists + /// + public delegate Object RowGetterDelegate(int rowIndex); + + /// + /// These delegates are used to format a listviewitem before it is added to the control. + /// + public delegate void RowFormatterDelegate(OLVListItem olvItem); + + /// + /// These delegates can be used to return the array of texts that should be searched for text filtering + /// + public delegate string[] SearchValueGetterDelegate(Object value); + + /// + /// These delegates are used to sort the listview in some custom fashion + /// + public delegate void SortDelegate(OLVColumn column, SortOrder sortOrder); + + /// + /// These delegates are used to order two strings. + /// x cannot be null. y can be null. + /// + public delegate int StringCompareDelegate(string x, string y); + + #endregion +} diff --git a/ObjectListView/Implementation/DragSource.cs b/ObjectListView/Implementation/DragSource.cs new file mode 100644 index 00000000..f0f47834 --- /dev/null +++ b/ObjectListView/Implementation/DragSource.cs @@ -0,0 +1,407 @@ +/* + * DragSource.cs - Add drag source functionality to an ObjectListView + * + * UNFINISHED + * + * Author: Phillip Piper + * Date: 2009-03-17 5:15 PM + * + * Change log: + * v2.3 + * 2009-07-06 JPP - Make sure Link is acceptable as an drop effect by default + * (since MS didn't make it part of the 'All' value) + * v2.2 + * 2009-04-15 JPP - Separated DragSource.cs into DropSink.cs + * 2009-03-17 JPP - Initial version + * + * Copyright (C) 2009 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace BrightIdeasSoftware +{ + /// + /// An IDragSource controls how drag out from the ObjectListView will behave + /// + public interface IDragSource + { + /// + /// A drag operation is beginning. Return the data object that will be used + /// for data transfer. Return null to prevent the drag from starting. + /// + /// + /// The returned object is later passed to the GetAllowedEffect() and EndDrag() + /// methods. + /// + /// What ObjectListView is being dragged from. + /// Which mouse button is down? + /// What item was directly dragged by the user? There may be more than just this + /// item selected. + /// The data object that will be used for data transfer. This will often be a subclass + /// of DataObject, but does not need to be. + Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item); + + /// + /// What operations are possible for this drag? This controls the icon shown during the drag + /// + /// The data object returned by StartDrag() + /// A combination of DragDropEffects flags + DragDropEffects GetAllowedEffects(Object dragObject); + + /// + /// The drag operation is complete. Do whatever is necessary to complete the action. + /// + /// The data object returned by StartDrag() + /// The value returned from GetAllowedEffects() + void EndDrag(Object dragObject, DragDropEffects effect); + } + + /// + /// A do-nothing implementation of IDragSource that can be safely subclassed. + /// + public class AbstractDragSource : IDragSource + { + #region IDragSource Members + + /// + /// See IDragSource documentation + /// + /// + /// + /// + /// + public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) { + return null; + } + + /// + /// See IDragSource documentation + /// + /// + /// + public virtual DragDropEffects GetAllowedEffects(Object data) { + return DragDropEffects.None; + } + + /// + /// See IDragSource documentation + /// + /// + /// + public virtual void EndDrag(Object dragObject, DragDropEffects effect) { + } + + #endregion + } + + /// + /// A reasonable implementation of IDragSource that provides normal + /// drag source functionality. It creates a data object that supports + /// inter-application dragging of text and HTML representation of + /// the dragged rows. It can optionally force a refresh of all dragged + /// rows when the drag is complete. + /// + /// Subclasses can override GetDataObject() to add new + /// data formats to the data transfer object. + public class SimpleDragSource : IDragSource + { + #region Constructors + + /// + /// Construct a SimpleDragSource + /// + public SimpleDragSource() { + } + + /// + /// Construct a SimpleDragSource that refreshes the dragged rows when + /// the drag is complete + /// + /// + public SimpleDragSource(bool refreshAfterDrop) { + this.RefreshAfterDrop = refreshAfterDrop; + } + + #endregion + + #region Public properties + + /// + /// Gets or sets whether the dragged rows should be refreshed when the + /// drag operation is complete. + /// + public bool RefreshAfterDrop { + get { return refreshAfterDrop; } + set { refreshAfterDrop = value; } + } + private bool refreshAfterDrop; + + #endregion + + #region IDragSource Members + + /// + /// Create a DataObject when the user does a left mouse drag operation. + /// See IDragSource for further information. + /// + /// + /// + /// + /// + public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) { + // We only drag on left mouse + if (button != MouseButtons.Left) + return null; + + return this.CreateDataObject(olv); + } + + /// + /// Which operations are allowed in the operation? By default, all operations are supported. + /// + /// + /// All opertions are supported + public virtual DragDropEffects GetAllowedEffects(Object data) { + return DragDropEffects.All | DragDropEffects.Link; // why didn't MS include 'Link' in 'All'?? + } + + /// + /// The drag operation is finished. Refreshe the dragged rows if so configured. + /// + /// + /// + public virtual void EndDrag(Object dragObject, DragDropEffects effect) { + OLVDataObject data = dragObject as OLVDataObject; + if (data == null) + return; + + if (this.RefreshAfterDrop) + data.ListView.RefreshObjects(data.ModelObjects); + } + + /// + /// Create a data object that will be used to as the data object + /// for the drag operation. + /// + /// + /// Subclasses can override this method add new formats to the data object. + /// + /// The ObjectListView that is the source of the drag + /// A data object for the drag + protected virtual object CreateDataObject(ObjectListView olv) { + OLVDataObject data = new OLVDataObject(olv); + data.CreateTextFormats(); + return data; + } + + #endregion + } + + /// + /// A data transfer object that knows how to transform a list of model + /// objects into a text and HTML representation. + /// + public class OLVDataObject : DataObject + { + #region Life and death + + /// + /// Create a data object from the selected objects in the given ObjectListView + /// + /// The source of the data object + public OLVDataObject(ObjectListView olv) : this(olv, olv.SelectedObjects) { + } + + /// + /// Create a data object which operates on the given model objects + /// in the given ObjectListView + /// + /// The source of the data object + /// The model objects to be put into the data object + public OLVDataObject(ObjectListView olv, IList modelObjects) { + this.objectListView = olv; + this.modelObjects = modelObjects; + this.includeHiddenColumns = olv.IncludeHiddenColumnsInDataTransfer; + this.includeColumnHeaders = olv.IncludeColumnHeadersInCopy; + } + + #endregion + + #region Properties + + /// + /// Gets or sets whether hidden columns will also be included in the text + /// and HTML representation. If this is false, only visible columns will + /// be included. + /// + public bool IncludeHiddenColumns { + get { return includeHiddenColumns; } + } + private bool includeHiddenColumns; + + /// + /// Gets or sets whether column headers will also be included in the text + /// and HTML representation. + /// + public bool IncludeColumnHeaders + { + get { return includeColumnHeaders; } + } + private bool includeColumnHeaders; + + /// + /// Gets the ObjectListView that is being used as the source of the data + /// + public ObjectListView ListView { + get { return objectListView; } + } + private ObjectListView objectListView; + + /// + /// Gets the model objects that are to be placed in the data object + /// + public IList ModelObjects { + get { return modelObjects; } + } + private IList modelObjects = new ArrayList(); + + #endregion + + /// + /// Put a text and HTML representation of our model objects + /// into the data object. + /// + public void CreateTextFormats() { + IList columns = this.IncludeHiddenColumns ? this.ListView.AllColumns : this.ListView.ColumnsInDisplayOrder; + + // Build text and html versions of the selection + StringBuilder sbText = new StringBuilder(); + StringBuilder sbHtml = new StringBuilder(""); + + // Include column headers + if (includeColumnHeaders) + { + sbHtml.Append(""); + } + + foreach (object modelObject in this.ModelObjects) + { + sbHtml.Append(""); + } + sbHtml.AppendLine("
"); + foreach (OLVColumn col in columns) + { + if (col != columns[0]) + { + sbText.Append("\t"); + sbHtml.Append(""); + } + string strValue = col.Text; + sbText.Append(strValue); + sbHtml.Append(strValue); //TODO: Should encode the string value + } + sbText.AppendLine(); + sbHtml.AppendLine("
"); + foreach (OLVColumn col in columns) { + if (col != columns[0]) { + sbText.Append("\t"); + sbHtml.Append(""); + } + string strValue = col.GetStringValue(modelObject); + sbText.Append(strValue); + sbHtml.Append(strValue); //TODO: Should encode the string value + } + sbText.AppendLine(); + sbHtml.AppendLine("
"); + + // Put both the text and html versions onto the clipboard. + // For some reason, SetText() with UnicodeText doesn't set the basic CF_TEXT format, + // but using SetData() does. + //this.SetText(sbText.ToString(), TextDataFormat.UnicodeText); + this.SetData(sbText.ToString()); + this.SetText(ConvertToHtmlFragment(sbHtml.ToString()), TextDataFormat.Html); + } + + /// + /// Make a HTML representation of our model objects + /// + public string CreateHtml() { + IList columns = this.ListView.ColumnsInDisplayOrder; + + // Build html version of the selection + StringBuilder sbHtml = new StringBuilder(""); + + foreach (object modelObject in this.ModelObjects) { + sbHtml.Append(""); + } + sbHtml.AppendLine("
"); + foreach (OLVColumn col in columns) { + if (col != columns[0]) { + sbHtml.Append(""); + } + string strValue = col.GetStringValue(modelObject); + sbHtml.Append(strValue); //TODO: Should encode the string value + } + sbHtml.AppendLine("
"); + + return sbHtml.ToString(); + } + + /// + /// Convert the fragment of HTML into the Clipboards HTML format. + /// + /// The HTML format is found here http://msdn2.microsoft.com/en-us/library/aa767917.aspx + /// + /// The HTML to put onto the clipboard. It must be valid HTML! + /// A string that can be put onto the clipboard and will be recognized as HTML + private string ConvertToHtmlFragment(string fragment) { + // Minimal implementation of HTML clipboard format + string source = "http://www.codeproject.com/KB/list/ObjectListView.aspx"; + + const String MARKER_BLOCK = + "Version:1.0\r\n" + + "StartHTML:{0,8}\r\n" + + "EndHTML:{1,8}\r\n" + + "StartFragment:{2,8}\r\n" + + "EndFragment:{3,8}\r\n" + + "StartSelection:{2,8}\r\n" + + "EndSelection:{3,8}\r\n" + + "SourceURL:{4}\r\n" + + "{5}"; + + int prefixLength = String.Format(MARKER_BLOCK, 0, 0, 0, 0, source, "").Length; + + const String DEFAULT_HTML_BODY = + "" + + "{0}"; + + string html = String.Format(DEFAULT_HTML_BODY, fragment); + int startFragment = prefixLength + html.IndexOf(fragment); + int endFragment = startFragment + fragment.Length; + + return String.Format(MARKER_BLOCK, prefixLength, prefixLength + html.Length, startFragment, endFragment, source, html); + } + } +} diff --git a/ObjectListView/Implementation/DropSink.cs b/ObjectListView/Implementation/DropSink.cs new file mode 100644 index 00000000..03818d82 --- /dev/null +++ b/ObjectListView/Implementation/DropSink.cs @@ -0,0 +1,1402 @@ +/* + * DropSink.cs - Add drop sink ability to an ObjectListView + * + * Author: Phillip Piper + * Date: 2009-03-17 5:15 PM + * + * Change log: + * 2010-08-24 JPP - Moved AcceptExternal property up to SimpleDragSource. + * v2.3 + * 2009-09-01 JPP - Correctly handle case where RefreshObjects() is called for + * objects that were children but are now roots. + * 2009-08-27 JPP - Added ModelDropEventArgs.RefreshObjects() to simplify updating after + * a drag-drop operation + * 2009-08-19 JPP - Changed to use OlvHitTest() + * v2.2.1 + * 2007-07-06 JPP - Added StandardDropActionFromKeys property to OlvDropEventArgs + * v2.2 + * 2009-05-17 JPP - Added a Handled flag to OlvDropEventArgs + * - Tweaked the appearance of the drop-on-background feedback + * 2009-04-15 JPP - Separated DragDrop.cs into DropSink.cs + * 2009-03-17 JPP - Initial version + * + * Copyright (C) 2009-2010 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// Objects that implement this interface can acts as the receiver for drop + /// operation for an ObjectListView. + /// + public interface IDropSink + { + /// + /// Gets or sets the ObjectListView that is the drop sink + /// + ObjectListView ListView { get; set; } + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + void DrawFeedback(Graphics g, Rectangle bounds); + + /// + /// The user has released the drop over this control + /// + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned + /// to the originator of the drag. + /// + /// + void Drop(DragEventArgs args); + + /// + /// A drag has entered this control. + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + void Enter(DragEventArgs args); + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// + void GiveFeedback(GiveFeedbackEventArgs args); + + /// + /// The drag has left the bounds of this control + /// + void Leave(); + + /// + /// The drag is moving over this control. + /// + /// This is where any drop target should be calculated. + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + /// + void Over(DragEventArgs args); + + /// + /// Should the drag be allowed to continue? + /// + /// + void QueryContinue(QueryContinueDragEventArgs args); + } + + /// + /// This is a do-nothing implementation of IDropSink that is a useful + /// base class for more sophisticated implementations. + /// + public class AbstractDropSink : IDropSink + { + #region IDropSink Members + + /// + /// Gets or sets the ObjectListView that is the drop sink + /// + public virtual ObjectListView ListView { + get { return listView; } + set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + public virtual void DrawFeedback(Graphics g, Rectangle bounds) { + } + + /// + /// The user has released the drop over this control + /// + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned + /// to the originator of the drag. + /// + /// + public virtual void Drop(DragEventArgs args) { + this.Cleanup(); + } + + /// + /// A drag has entered this control. + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + public virtual void Enter(DragEventArgs args) { + } + + /// + /// The drag has left the bounds of this control + /// + public virtual void Leave() { + this.Cleanup(); + } + + /// + /// The drag is moving over this control. + /// + /// This is where any drop target should be calculated. + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + /// + public virtual void Over(DragEventArgs args) { + } + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// You only need to override this if you want non-standard cursors. + /// The standard cursors are supplied automatically. + /// + public virtual void GiveFeedback(GiveFeedbackEventArgs args) { + args.UseDefaultCursors = true; + } + + /// + /// Should the drag be allowed to continue? + /// + /// + /// You only need to override this if you want the user to be able + /// to end the drop in some non-standard way, e.g. dragging to a + /// certain point even without releasing the mouse, or going outside + /// the bounds of the application. + /// + /// + public virtual void QueryContinue(QueryContinueDragEventArgs args) { + } + + + #endregion + + #region Commands + + /// + /// This is called when the mouse leaves the drop region and after the + /// drop has completed. + /// + protected virtual void Cleanup() { + } + + #endregion + } + + /// + /// The enum indicates which target has been found for a drop operation + /// + [Flags] + public enum DropTargetLocation + { + /// + /// No applicable target has been found + /// + None = 0, + + /// + /// The list itself is the target of the drop + /// + Background = 0x01, + + /// + /// An item is the target + /// + Item = 0x02, + + /// + /// Between two items (or above the top item or below the bottom item) + /// can be the target. This is not actually ever a target, only a value indicate + /// that it is valid to drop between items + /// + BetweenItems = 0x04, + + /// + /// Above an item is the target + /// + AboveItem = 0x08, + + /// + /// Below an item is the target + /// + BelowItem = 0x10, + + /// + /// A subitem is the target of the drop + /// + SubItem = 0x20, + + /// + /// On the right of an item is the target (not currently used) + /// + RightOfItem = 0x40, + + /// + /// On the left of an item is the target (not currently used) + /// + LeftOfItem = 0x80 + } + + /// + /// This class represents a simple implementation of a drop sink. + /// + /// + /// Actually, it's far from simple and can do quite a lot in its own right. + /// + public class SimpleDropSink : AbstractDropSink + { + #region Life and death + + /// + /// Make a new drop sink + /// + public SimpleDropSink() { + this.timer = new Timer(); + this.timer.Interval = 250; + this.timer.Tick += new EventHandler(this.timer_Tick); + + this.CanDropOnItem = true; + //this.CanDropOnSubItem = true; + //this.CanDropOnBackground = true; + //this.CanDropBetween = true; + + this.FeedbackColor = Color.FromArgb(180, Color.MediumBlue); + this.billboard = new BillboardOverlay(); + } + + #endregion + + #region Public properties + + /// + /// Get or set the locations where a drop is allowed to occur (OR-ed together) + /// + public DropTargetLocation AcceptableLocations { + get { return this.acceptableLocations; } + set { this.acceptableLocations = value; } + } + private DropTargetLocation acceptableLocations; + + /// + /// Gets or sets whether this sink allows model objects to be dragged from other lists + /// + public bool AcceptExternal { + get { return this.acceptExternal; } + set { this.acceptExternal = value; } + } + private bool acceptExternal = true; + + /// + /// Gets or sets whether the ObjectListView should scroll when the user drags + /// something near to the top or bottom rows. + /// + public bool AutoScroll { + get { return this.autoScroll; } + set { this.autoScroll = value; } + } + private bool autoScroll = true; + + /// + /// Gets the billboard overlay that will be used to display feedback + /// messages during a drag operation. + /// + /// Set this to null to stop the feedback. + public BillboardOverlay Billboard { + get { return this.billboard; } + set { this.billboard = value; } + } + private BillboardOverlay billboard; + + /// + /// Get or set whether a drop can occur between items of the list + /// + public bool CanDropBetween { + get { return (this.AcceptableLocations & DropTargetLocation.BetweenItems) == DropTargetLocation.BetweenItems; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.BetweenItems; + else + this.AcceptableLocations &= ~DropTargetLocation.BetweenItems; + } + } + + /// + /// Get or set whether a drop can occur on the listview itself + /// + public bool CanDropOnBackground { + get { return (this.AcceptableLocations & DropTargetLocation.Background) == DropTargetLocation.Background; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.Background; + else + this.AcceptableLocations &= ~DropTargetLocation.Background; + } + } + + /// + /// Get or set whether a drop can occur on items in the list + /// + public bool CanDropOnItem { + get { return (this.AcceptableLocations & DropTargetLocation.Item) == DropTargetLocation.Item; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.Item; + else + this.AcceptableLocations &= ~DropTargetLocation.Item; + } + } + + /// + /// Get or set whether a drop can occur on a subitem in the list + /// + public bool CanDropOnSubItem { + get { return (this.AcceptableLocations & DropTargetLocation.SubItem) == DropTargetLocation.SubItem; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.SubItem; + else + this.AcceptableLocations &= ~DropTargetLocation.SubItem; + } + } + + /// + /// Get or set the index of the item that is the target of the drop + /// + public int DropTargetIndex { + get { return dropTargetIndex; } + set { + if (this.dropTargetIndex != value) { + this.dropTargetIndex = value; + this.ListView.Invalidate(); + } + } + } + private int dropTargetIndex = -1; + + /// + /// Get the item that is the target of the drop + /// + public OLVListItem DropTargetItem { + get { + return this.ListView.GetItem(this.DropTargetIndex); + } + } + + /// + /// Get or set the location of the target of the drop + /// + public DropTargetLocation DropTargetLocation { + get { return dropTargetLocation; } + set { + if (this.dropTargetLocation != value) { + this.dropTargetLocation = value; + this.ListView.Invalidate(); + } + } + } + private DropTargetLocation dropTargetLocation; + + /// + /// Get or set the index of the subitem that is the target of the drop + /// + public int DropTargetSubItemIndex { + get { return dropTargetSubItemIndex; } + set { + if (this.dropTargetSubItemIndex != value) { + this.dropTargetSubItemIndex = value; + this.ListView.Invalidate(); + } + } + } + private int dropTargetSubItemIndex = -1; + + /// + /// Get or set the color that will be used to provide drop feedback + /// + public Color FeedbackColor { + get { return this.feedbackColor; } + set { this.feedbackColor = value; } + } + private Color feedbackColor; + + /// + /// Get whether the alt key was down during this drop event + /// + public bool IsAltDown { + get { return (this.KeyState & 32) == 32; } + } + + /// + /// Get whether any modifier key was down during this drop event + /// + public bool IsAnyModifierDown { + get { return (this.KeyState & (4 + 8 + 32)) != 0; } + } + + /// + /// Get whether the control key was down during this drop event + /// + public bool IsControlDown { + get { return (this.KeyState & 8) == 8; } + } + + /// + /// Get whether the left mouse button was down during this drop event + /// + public bool IsLeftMouseButtonDown { + get { return (this.KeyState & 1) == 1; } + } + + /// + /// Get whether the right mouse button was down during this drop event + /// + public bool IsMiddleMouseButtonDown { + get { return (this.KeyState & 16) == 16; } + } + + /// + /// Get whether the right mouse button was down during this drop event + /// + public bool IsRightMouseButtonDown { + get { return (this.KeyState & 2) == 2; } + } + + /// + /// Get whether the shift key was down during this drop event + /// + public bool IsShiftDown { + get { return (this.KeyState & 4) == 4; } + } + + /// + /// Get or set the state of the keys during this drop event + /// + public int KeyState { + get { return this.keyState; } + set { this.keyState = value; } + } + private int keyState; + + #endregion + + #region Events + + /// + /// Triggered when the sink needs to know if a drop can occur. + /// + /// + /// Handlers should set Effect to indicate what is possible. + /// Handlers can change any of the DropTarget* setttings to change + /// the target of the drop. + /// + public event EventHandler CanDrop; + + /// + /// Triggered when the drop is made. + /// + public event EventHandler Dropped; + + /// + /// Triggered when the sink needs to know if a drop can occur + /// AND the source is an ObjectListView + /// + /// + /// Handlers should set Effect to indicate what is possible. + /// Handlers can change any of the DropTarget* setttings to change + /// the target of the drop. + /// + public event EventHandler ModelCanDrop; + + /// + /// Triggered when the drop is made. + /// AND the source is an ObjectListView + /// + public event EventHandler ModelDropped; + + #endregion + + #region DropSink Interface + + /// + /// Cleanup the drop sink when the mouse has left the control or + /// the drag has finished. + /// + protected override void Cleanup() { + this.DropTargetLocation = DropTargetLocation.None; + this.ListView.FullRowSelect = this.originalFullRowSelect; + this.Billboard.Text = null; + } + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + public override void DrawFeedback(Graphics g, Rectangle bounds) { + g.SmoothingMode = ObjectListView.SmoothingMode; + + switch (this.DropTargetLocation) { + case DropTargetLocation.Background: + this.DrawFeedbackBackgroundTarget(g, bounds); + break; + case DropTargetLocation.Item: + this.DrawFeedbackItemTarget(g, bounds); + break; + case DropTargetLocation.AboveItem: + this.DrawFeedbackAboveItemTarget(g, bounds); + break; + case DropTargetLocation.BelowItem: + this.DrawFeedbackBelowItemTarget(g, bounds); + break; + } + + if (this.Billboard != null) { + this.Billboard.Draw(this.ListView, g, bounds); + } + } + + /// + /// The user has released the drop over this control + /// + /// + public override void Drop(DragEventArgs args) { + this.TriggerDroppedEvent(args); + this.timer.Stop(); + this.Cleanup(); + } + + /// + /// A drag has entered this control. + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + public override void Enter(DragEventArgs args) { + //System.Diagnostics.Debug.WriteLine("Enter"); + + /* + * When FullRowSelect is true, we have two problems: + * 1) GetItemRect(ItemOnly) returns the whole row rather than just the icon/text, which messes + * up our calculation of the drop rectangle. + * 2) during the drag, the Timer events will not fire! This is the major problem, since without + * those events we can't autoscroll. + * + * The first problem we can solve through coding, but the second is more difficult. + * We avoid both problems by turning off FullRowSelect during the drop operation. + */ + this.originalFullRowSelect = this.ListView.FullRowSelect; + this.ListView.FullRowSelect = false; + + // Setup our drop event args block + this.dropEventArgs = new ModelDropEventArgs(); + this.dropEventArgs.DropSink = this; + this.dropEventArgs.ListView = this.ListView; + this.dropEventArgs.DataObject = args.Data; + OLVDataObject olvData = args.Data as OLVDataObject; + if (olvData != null) { + this.dropEventArgs.SourceListView = olvData.ListView; + this.dropEventArgs.SourceModels = olvData.ModelObjects; + } + + this.Over(args); + } + + /// + /// The drag is moving over this control. + /// + /// + public override void Over(DragEventArgs args) { + //System.Diagnostics.Debug.WriteLine("Over"); + this.KeyState = args.KeyState; + Point pt = this.ListView.PointToClient(new Point(args.X, args.Y)); + args.Effect = this.CalculateDropAction(args, pt); + this.CheckScrolling(pt); + } + + #endregion + + #region Events + + /// + /// Trigger the Dropped events + /// + /// + protected virtual void TriggerDroppedEvent(DragEventArgs args) { + this.dropEventArgs.Handled = false; + + // If the source is an ObjectListView, trigger the ModelDropped event + if (this.dropEventArgs.SourceListView != null) + this.OnModelDropped(this.dropEventArgs); + + if (!this.dropEventArgs.Handled) + this.OnDropped(this.dropEventArgs); + } + + /// + /// Trigger CanDrop + /// + /// + protected virtual void OnCanDrop(OlvDropEventArgs args) { + if (this.CanDrop != null) + this.CanDrop(this, args); + } + + /// + /// Trigger Dropped + /// + /// + protected virtual void OnDropped(OlvDropEventArgs args) { + if (this.Dropped != null) + this.Dropped(this, args); + } + + /// + /// Trigger ModelCanDrop + /// + /// + protected virtual void OnModelCanDrop(ModelDropEventArgs args) { + + // Don't allow drops from other list, if that's what's configured + if (!this.AcceptExternal && args.SourceListView != null && args.SourceListView != this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + args.InfoMessage = "This list doesn't accept drops from other lists"; + return; + } + + if (this.ModelCanDrop != null) + this.ModelCanDrop(this, args); + } + + /// + /// Trigger ModelDropped + /// + /// + protected virtual void OnModelDropped(ModelDropEventArgs args) { + if (this.ModelDropped != null) + this.ModelDropped(this, args); + } + + #endregion + + #region Implementation + + private void timer_Tick(object sender, EventArgs e) { + this.HandleTimerTick(); + } + + /// + /// Handle the timer tick event, which is sent when the listview should + /// scroll + /// + protected virtual void HandleTimerTick() { + + // If the mouse has been released, stop scrolling. + // This is only necessary if the mouse is released outside of the control. + // If the mouse is released inside the control, we would receive a Drop event. + if ((this.IsLeftMouseButtonDown && (Control.MouseButtons & MouseButtons.Left) != MouseButtons.Left) || + (this.IsMiddleMouseButtonDown && (Control.MouseButtons & MouseButtons.Middle) != MouseButtons.Middle) || + (this.IsRightMouseButtonDown && (Control.MouseButtons & MouseButtons.Right) != MouseButtons.Right)) { + this.timer.Stop(); + this.Cleanup(); + return; + } + + // Auto scrolling will continune while the mouse is close to the ListView + const int GRACE_PERIMETER = 30; + + Point pt = this.ListView.PointToClient(Cursor.Position); + Rectangle r2 = this.ListView.ClientRectangle; + r2.Inflate(GRACE_PERIMETER, GRACE_PERIMETER); + if (r2.Contains(pt)) { + this.ListView.LowLevelScroll(0, this.scrollAmount); + } + } + + /// + /// When the mouse is at the given point, what should the target of the drop be? + /// + /// This method should update the DropTarget* members of the given arg block + /// + /// The mouse point, in client co-ordinates + protected virtual void CalculateDropTarget(OlvDropEventArgs args, Point pt) { + const int SMALL_VALUE = 3; + DropTargetLocation location = DropTargetLocation.None; + int targetIndex = -1; + int targetSubIndex = 0; + + if (this.CanDropOnBackground) + location = DropTargetLocation.Background; + + // Which item is the mouse over? + // If it is not over any item, it's over the background. + //ListViewHitTestInfo info = this.ListView.HitTest(pt.X, pt.Y); + OlvListViewHitTestInfo info = this.ListView.OlvHitTest(pt.X, pt.Y); + if (info.Item != null && this.CanDropOnItem) { + location = DropTargetLocation.Item; + targetIndex = info.Item.Index; + if (info.SubItem != null && this.CanDropOnSubItem) + targetSubIndex = info.Item.SubItems.IndexOf(info.SubItem); + } + + // Check to see if the mouse is "between" rows. + // ("between" is somewhat loosely defined) + if (this.CanDropBetween && this.ListView.GetItemCount() > 0) { + + // If the mouse is over an item, check to see if it is near the top or bottom + if (location == DropTargetLocation.Item) { + if (pt.Y - SMALL_VALUE <= info.Item.Bounds.Top) + location = DropTargetLocation.AboveItem; + if (pt.Y + SMALL_VALUE >= info.Item.Bounds.Bottom) + location = DropTargetLocation.BelowItem; + } else { + // Is there an item a little below the mouse? + // If so, we say the drop point is above that row + info = this.ListView.OlvHitTest(pt.X, pt.Y + SMALL_VALUE); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.AboveItem; + } else { + // Is there an item a little above the mouse? + info = this.ListView.OlvHitTest(pt.X, pt.Y - SMALL_VALUE); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.BelowItem; + } + } + } + } + + args.DropTargetLocation = location; + args.DropTargetIndex = targetIndex; + args.DropTargetSubItemIndex = targetSubIndex; + } + + /// + /// What sort of action is possible when the mouse is at the given point? + /// + /// + /// + /// + /// + /// + public virtual DragDropEffects CalculateDropAction(DragEventArgs args, Point pt) { + this.CalculateDropTarget(this.dropEventArgs, pt); + + this.dropEventArgs.MouseLocation = pt; + this.dropEventArgs.InfoMessage = null; + this.dropEventArgs.Handled = false; + + if (this.dropEventArgs.SourceListView != null) { + this.dropEventArgs.TargetModel = this.ListView.GetModelObject(this.dropEventArgs.DropTargetIndex); + this.OnModelCanDrop(this.dropEventArgs); + } + + if (!this.dropEventArgs.Handled) + this.OnCanDrop(this.dropEventArgs); + + this.UpdateAfterCanDropEvent(this.dropEventArgs); + + return this.dropEventArgs.Effect; + } + + /// + /// Based solely on the state of the modifier keys, what drop operation should + /// be used? + /// + /// The drop operation that matches the state of the keys + public DragDropEffects CalculateStandardDropActionFromKeys() { + if (this.IsControlDown) { + if (this.IsShiftDown) + return DragDropEffects.Link; + else + return DragDropEffects.Copy; + } else { + return DragDropEffects.Move; + } + } + + /// + /// Should the listview be made to scroll when the mouse is at the given point? + /// + /// + protected virtual void CheckScrolling(Point pt) { + if (!this.AutoScroll) + return; + + Rectangle r = this.ListView.ContentRectangle; + int rowHeight = this.ListView.RowHeightEffective; + int close = rowHeight; + + // In Tile view, using the whole row height is too much + if (this.ListView.View == View.Tile) + close /= 2; + + if (pt.Y <= (r.Top + close)) { + // Scroll faster if the mouse is closer to the top + this.timer.Interval = ((pt.Y <= (r.Top + close / 2)) ? 100 : 350); + this.timer.Start(); + this.scrollAmount = -rowHeight; + } else { + if (pt.Y >= (r.Bottom - close)) { + this.timer.Interval = ((pt.Y >= (r.Bottom - close / 2)) ? 100 : 350); + this.timer.Start(); + this.scrollAmount = rowHeight; + } else { + this.timer.Stop(); + } + } + } + + /// + /// Update the state of our sink to reflect the information that + /// may have been written into the drop event args. + /// + /// + protected virtual void UpdateAfterCanDropEvent(OlvDropEventArgs args) { + this.DropTargetIndex = args.DropTargetIndex; + this.DropTargetLocation = args.DropTargetLocation; + this.DropTargetSubItemIndex = args.DropTargetSubItemIndex; + + if (this.Billboard != null) { + Point pt = args.MouseLocation; + pt.Offset(5, 5); + if (this.Billboard.Text != this.dropEventArgs.InfoMessage || this.Billboard.Location != pt) { + this.Billboard.Text = this.dropEventArgs.InfoMessage; + this.Billboard.Location = pt; + this.ListView.Invalidate(); + } + } + } + + #endregion + + #region Rendering + + /// + /// Draw the feedback that shows that the background is the target + /// + /// + /// + protected virtual void DrawFeedbackBackgroundTarget(Graphics g, Rectangle bounds) { + float penWidth = 12.0f; + Rectangle r = bounds; + r.Inflate((int)-penWidth / 2, (int)-penWidth / 2); + using (Pen p = new Pen(Color.FromArgb(128, this.FeedbackColor), penWidth)) { + using (GraphicsPath path = this.GetRoundedRect(r, 30.0f)) { + g.DrawPath(p, path); + } + } + } + + /// + /// Draw the feedback that shows that an item (or a subitem) is the target + /// + /// + /// + /// + /// DropTargetItem and DropTargetSubItemIndex tells what is the target + /// + protected virtual void DrawFeedbackItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + r.Inflate(1, 1); + float diameter = r.Height / 3; + using (GraphicsPath path = this.GetRoundedRect(r, diameter)) { + using (SolidBrush b = new SolidBrush(Color.FromArgb(48, this.FeedbackColor))) { + g.FillPath(b, path); + } + using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { + g.DrawPath(p, path); + } + } + } + + /// + /// Draw the feedback that shows the drop will occur before target + /// + /// + /// + protected virtual void DrawFeedbackAboveItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Left, r.Top, r.Right, r.Top); + } + + /// + /// Draw the feedback that shows the drop will occur after target + /// + /// + /// + protected virtual void DrawFeedbackBelowItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Left, r.Bottom, r.Right, r.Bottom); + } + + /// + /// Return a GraphicPath that is round corner rectangle. + /// + /// + /// + /// + protected GraphicsPath GetRoundedRect(Rectangle rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + + return path; + } + + /// + /// Calculate the target rectangle when the given item (and possible subitem) + /// is the target of the drop. + /// + /// + /// + /// + protected virtual Rectangle CalculateDropTargetRectangle(OLVListItem item, int subItem) { + if (subItem > 0) + return item.SubItems[subItem].Bounds; + + Rectangle r = this.ListView.CalculateCellTextBounds(item, subItem); + + // Allow for indent + if (item.IndentCount > 0) { + int indentWidth = this.ListView.SmallImageSize.Width; + r.X += (indentWidth * item.IndentCount); + r.Width -= (indentWidth * item.IndentCount); + } + + return r; + } + + /// + /// Draw a "between items" line at the given co-ordinates + /// + /// + /// + /// + /// + /// + protected virtual void DrawBetweenLine(Graphics g, int x1, int y1, int x2, int y2) { + using (Brush b = new SolidBrush(this.FeedbackColor)) { + int x = x1; + int y = y1; + using (GraphicsPath gp = new GraphicsPath()) { + gp.AddLine( + x, y + 5, + x, y - 5); + gp.AddBezier( + x, y - 6, + x + 3, y - 2, + x + 6, y - 1, + x + 11, y); + gp.AddBezier( + x + 11, y, + x + 6, y + 1, + x + 3, y + 2, + x, y + 6); + gp.CloseFigure(); + g.FillPath(b, gp); + } + x = x2; + y = y2; + using (GraphicsPath gp = new GraphicsPath()) { + gp.AddLine( + x, y + 6, + x, y - 6); + gp.AddBezier( + x, y - 7, + x - 3, y - 2, + x - 6, y - 1, + x - 11, y); + gp.AddBezier( + x - 11, y, + x - 6, y + 1, + x - 3, y + 2, + x, y + 7); + gp.CloseFigure(); + g.FillPath(b, gp); + } + } + using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { + g.DrawLine(p, x1, y1, x2, y2); + } + } + + #endregion + + private Timer timer; + private int scrollAmount; + private bool originalFullRowSelect; + private ModelDropEventArgs dropEventArgs; + } + + /// + /// This drop sink allows items within the same list to be rearranged, + /// as well as allowing items to be dropped from other lists. + /// + /// + /// + /// This class can only be used on plain ObjectListViews and FastObjectListViews. + /// The other flavours have no way to implement the insert operation that is required. + /// + /// + /// This class does not work with grouping. + /// + /// + /// This class works when the OLV is sorted, but it is up to the programmer + /// to decide what rearranging such lists "means". Example: if the control is sorting + /// students by academic grade, and the user drags a "Fail" grade student up amonst the "A+" + /// students, it is the responsibility of the programmer to makes the appropriate changes + /// to the model and redraw/rebuild the control so that the users action makes sense. + /// + /// + /// Users of this class should listen for the CanDrop event to decide + /// if models from another OLV can be moved to OLV under this sink. + /// + /// + public class RearrangingDropSink : SimpleDropSink + { + /// + /// Create a RearrangingDropSink + /// + public RearrangingDropSink() { + this.CanDropBetween = true; + this.CanDropOnBackground = true; + this.CanDropOnItem = false; + } + + /// + /// Create a RearrangingDropSink + /// + /// + public RearrangingDropSink(bool acceptDropsFromOtherLists) + : this() { + this.AcceptExternal = acceptDropsFromOtherLists; + } + + /// + /// Trigger OnModelCanDrop + /// + /// + protected override void OnModelCanDrop(ModelDropEventArgs args) { + base.OnModelCanDrop(args); + + if (args.Handled) + return; + + args.Effect = DragDropEffects.Move; + + // Don't allow drops from other list, if that's what's configured + if (!this.AcceptExternal && args.SourceListView != this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + args.InfoMessage = "This list doesn't accept drops from other lists"; + } + + // If we are rearranging a list, don't allow drops on the background + if (args.DropTargetLocation == DropTargetLocation.Background && args.SourceListView == this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + } + } + + /// + /// Trigger OnModelDropped + /// + /// + protected override void OnModelDropped(ModelDropEventArgs args) { + base.OnModelDropped(args); + + if (!args.Handled) + this.RearrangeModels(args); + } + + /// + /// Do the work of processing the dropped items + /// + /// + public virtual void RearrangeModels(ModelDropEventArgs args) { + switch (args.DropTargetLocation) { + case DropTargetLocation.AboveItem: + this.ListView.MoveObjects(args.DropTargetIndex, args.SourceModels); + break; + case DropTargetLocation.BelowItem: + this.ListView.MoveObjects(args.DropTargetIndex + 1, args.SourceModels); + break; + case DropTargetLocation.Background: + this.ListView.AddObjects(args.SourceModels); + break; + default: + return; + } + + if (args.SourceListView != this.ListView) { + args.SourceListView.RemoveObjects(args.SourceModels); + } + } + } + + /// + /// When a drop sink needs to know if something can be dropped, or + /// to notify that a drop has occured, it uses an instance of this class. + /// + public class OlvDropEventArgs : EventArgs + { + /// + /// Create a OlvDropEventArgs + /// + public OlvDropEventArgs() { + } + + #region Data Properties + + /// + /// Get the data object that is being dragged + /// + public object DataObject { + get { return this.dataObject; } + internal set { this.dataObject = value; } + } + private object dataObject; + + /// + /// Get the drop sink that originated this event + /// + public SimpleDropSink DropSink { + get { return this.dropSink; } + internal set { this.dropSink = value; } + } + private SimpleDropSink dropSink; + + /// + /// Get or set the index of the item that is the target of the drop + /// + public int DropTargetIndex { + get { return dropTargetIndex; } + set { this.dropTargetIndex = value; } + } + private int dropTargetIndex = -1; + + /// + /// Get or set the location of the target of the drop + /// + public DropTargetLocation DropTargetLocation { + get { return dropTargetLocation; } + set { this.dropTargetLocation = value; } + } + private DropTargetLocation dropTargetLocation; + + /// + /// Get or set the index of the subitem that is the target of the drop + /// + public int DropTargetSubItemIndex { + get { return dropTargetSubItemIndex; } + set { this.dropTargetSubItemIndex = value; } + } + private int dropTargetSubItemIndex = -1; + + /// + /// Get the item that is the target of the drop + /// + public OLVListItem DropTargetItem { + get { + return this.ListView.GetItem(this.DropTargetIndex); + } + set { + if (value == null) + this.DropTargetIndex = -1; + else + this.DropTargetIndex = value.Index; + } + } + + /// + /// Get or set the drag effect that should be used for this operation + /// + public DragDropEffects Effect { + get { return this.effect; } + set { this.effect = value; } + } + private DragDropEffects effect; + + /// + /// Get or set if this event was handled. No further processing will be done for a handled event. + /// + public bool Handled { + get { return this.handled; } + set { this.handled = value; } + } + private bool handled; + + /// + /// Get or set the feedback message for this operation + /// + /// + /// If this is not null, it will be displayed as a feedback message + /// during the drag. + /// + public string InfoMessage { + get { return this.infoMessage; } + set { this.infoMessage = value; } + } + private string infoMessage; + + /// + /// Get the ObjectListView that is being dropped on + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Get the location of the mouse (in target ListView co-ords) + /// + public Point MouseLocation { + get { return this.mouseLocation; } + internal set { this.mouseLocation = value; } + } + private Point mouseLocation; + + /// + /// Get the drop action indicated solely by the state of the modifier keys + /// + public DragDropEffects StandardDropActionFromKeys { + get { + return this.DropSink.CalculateStandardDropActionFromKeys(); + } + } + + #endregion + } + + /// + /// These events are triggered when the drag source is an ObjectListView. + /// + public class ModelDropEventArgs : OlvDropEventArgs + { + /// + /// Create a ModelDropEventArgs + /// + public ModelDropEventArgs() + { + } + + /// + /// Gets the model objects that are being dragged. + /// + public IList SourceModels { + get { return this.dragModels; } + internal set { + this.dragModels = value; + TreeListView tlv = this.SourceListView as TreeListView; + if (tlv != null) { + foreach (object model in this.SourceModels) { + object parent = tlv.GetParent(model); + if (!toBeRefreshed.Contains(parent)) + toBeRefreshed.Add(parent); + } + } + } + } + private IList dragModels; + private ArrayList toBeRefreshed = new ArrayList(); + + /// + /// Gets the ObjectListView that is the source of the dragged objects. + /// + public ObjectListView SourceListView { + get { return this.sourceListView; } + internal set { this.sourceListView = value; } + } + private ObjectListView sourceListView; + + /// + /// Get the model object that is being dropped upon. + /// + /// This is only value for TargetLocation == Item + public object TargetModel { + get { return this.targetModel; } + internal set { this.targetModel = value; } + } + private object targetModel; + + /// + /// Refresh all the objects involved in the operation + /// + public void RefreshObjects() { + TreeListView tlv = this.SourceListView as TreeListView; + if (tlv != null) { + foreach (object model in this.SourceModels) { + object parent = tlv.GetParent(model); + if (!toBeRefreshed.Contains(parent)) + toBeRefreshed.Add(parent); + } + } + toBeRefreshed.AddRange(this.SourceModels); + if (this.ListView == this.SourceListView) { + toBeRefreshed.Add(this.TargetModel); + this.ListView.RefreshObjects(toBeRefreshed); + } else { + this.SourceListView.RefreshObjects(toBeRefreshed); + this.ListView.RefreshObject(this.TargetModel); + } + } + } +} diff --git a/ObjectListView/Implementation/Enums.cs b/ObjectListView/Implementation/Enums.cs new file mode 100644 index 00000000..572b4b4e --- /dev/null +++ b/ObjectListView/Implementation/Enums.cs @@ -0,0 +1,104 @@ +/* + * Enums - All enum definitions used in ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware { + + public partial class ObjectListView { + /// + /// How does a user indicate that they want to edit cells? + /// + public enum CellEditActivateMode { + /// + /// This list cannot be edited. F2 does nothing. + /// + None = 0, + + /// + /// A single click on a subitem will edit the value. Single clicking the primary column, + /// selects the row just like normal. The user must press F2 to edit the primary column. + /// + SingleClick = 1, + + /// + /// Double clicking a subitem or the primary column will edit that cell. + /// F2 will edit the primary column. + /// + DoubleClick = 2, + + /// + /// Pressing F2 is the only way to edit the cells. Once the primary column is being edited, + /// the other cells in the row can be edited by pressing Tab. + /// + F2Only = 3, + + /// + /// A single click on a any cell will edit the value, even the primary column. + /// + SingleClickAlways = 4, + } + + /// + /// These values specify how column selection will be presented to the user + /// + public enum ColumnSelectBehaviour { + /// + /// No column selection will be presented + /// + None, + + /// + /// The columns will be show in the main menu + /// + InlineMenu, + + /// + /// The columns will be shown in a submenu + /// + Submenu, + + /// + /// A model dialog will be presented to allow the user to choose columns + /// + ModelDialog, + + /* + * NonModelDialog is just a little bit tricky since the OLV can change views while the dialog is showing + * So, just comment this out for the time being. + + /// + /// A non-model dialog will be presented to allow the user to choose columns + /// + NonModelDialog + * + */ + } + } +} \ No newline at end of file diff --git a/ObjectListView/Implementation/Events.cs b/ObjectListView/Implementation/Events.cs new file mode 100644 index 00000000..7a1c466a --- /dev/null +++ b/ObjectListView/Implementation/Events.cs @@ -0,0 +1,2514 @@ +/* + * Events - All the events that can be triggered within an ObjectListView. + * + * Author: Phillip Piper + * Date: 17/10/2008 9:15 PM + * + * Change log: + * v2.8.0 + * 2014-05-20 JPP - Added IsHyperlinkEventArgs.IsHyperlink + * v2.6 + * 2012-04-17 JPP - Added group state change and group expansion events + * v2.5 + * 2010-08-08 JPP - CellEdit validation and finish events now have NewValue property. + * v2.4 + * 2010-03-04 JPP - Added filtering events + * v2.3 + * 2009-08-16 JPP - Added group events + * 2009-08-08 JPP - Added HotItem event + * 2009-07-24 JPP - Added Hyperlink events + * - Added Formatting events + * v2.2.1 + * 2009-06-13 JPP - Added Cell events + * - Moved all event parameter blocks to this file. + * - Added Handled property to AfterSearchEventArgs + * v2.2 + * 2009-06-01 JPP - Added ColumnToGroupBy and GroupByOrder to sorting events + - Gave all event descriptions + * 2009-04-23 JPP - Added drag drop events + * v2.1 + * 2009-01-18 JPP - Moved SelectionChanged event to this file + * v2.0 + * 2008-12-06 JPP - Added searching events + * 2008-12-01 JPP - Added secondary sort information to Before/AfterSorting events + * 2008-10-17 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace BrightIdeasSoftware { + /// + /// The callbacks for CellEditing events + /// + /// this + /// We could replace this with EventHandler<CellEditEventArgs> but that would break all + /// cell editing event code from v1.x. + /// + public delegate void CellEditEventHandler(object sender, CellEditEventArgs e); + + public partial class ObjectListView { + //----------------------------------------------------------------------------------- + + #region Events + + /// + /// Triggered after a ObjectListView has been searched by the user typing into the list + /// + [Category("ObjectListView"), + Description("This event is triggered after the control has done a search-by-typing action.")] + public event EventHandler AfterSearching; + + /// + /// Triggered after a ObjectListView has been sorted + /// + [Category("ObjectListView"), + Description("This event is triggered after the items in the list have been sorted.")] + public event EventHandler AfterSorting; + + /// + /// Triggered before a ObjectListView is searched by the user typing into the list + /// + /// + /// Set Cancelled to true to prevent the searching from taking place. + /// Changing StringToFind or StartSearchFrom will change the subsequent search. + /// + [Category("ObjectListView"), + Description("This event is triggered before the control does a search-by-typing action.")] + public event EventHandler BeforeSearching; + + /// + /// Triggered before a ObjectListView is sorted + /// + /// + /// Set Cancelled to true to prevent the sort from taking place. + /// Changing ColumnToSort or SortOrder will change the subsequent sort. + /// + [Category("ObjectListView"), + Description("This event is triggered before the items in the list are sorted.")] + public event EventHandler BeforeSorting; + + /// + /// Triggered after a ObjectListView has created groups + /// + [Category("ObjectListView"), + Description("This event is triggered after the groups are created.")] + public event EventHandler AfterCreatingGroups; + + /// + /// Triggered before a ObjectListView begins to create groups + /// + /// + /// Set Groups to prevent the default group creation process + /// + [Category("ObjectListView"), + Description("This event is triggered before the groups are created.")] + public event EventHandler BeforeCreatingGroups; + + /// + /// Triggered just before a ObjectListView creates groups + /// + /// + /// You can make changes to the groups, which have been created, before those + /// groups are created within the listview. + /// + [Category("ObjectListView"), + Description("This event is triggered when the groups are just about to be created.")] + public event EventHandler AboutToCreateGroups; + + /// + /// Triggered when a button in a cell is left clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when the user left clicks a button.")] + public event EventHandler ButtonClick; + + /// + /// This event is triggered when the user moves a drag over an ObjectListView that + /// has a SimpleDropSink installed as the drop handler. + /// + /// + /// Handlers for this event should set the Effect argument and optionally the + /// InfoMsg property. They can also change any of the DropTarget* settings to change + /// the target of the drop. + /// + [Category("ObjectListView"), + Description("Can the user drop the currently dragged items at the current mouse location?")] + public event EventHandler CanDrop; + + /// + /// Triggered when a cell has finished being edited. + /// + [Category("ObjectListView"), + Description("This event is triggered cell edit operation has completely finished")] + public event CellEditEventHandler CellEditFinished; + + /// + /// Triggered when a cell is about to finish being edited. + /// + /// If Cancel is already true, the user is cancelling the edit operation. + /// Set Cancel to true to prevent the value from the cell being written into the model. + /// You cannot prevent the editing from finishing within this event -- you need + /// the CellEditValidating event for that. + [Category("ObjectListView"), + Description("This event is triggered cell edit operation is finishing.")] + public event CellEditEventHandler CellEditFinishing; + + /// + /// Triggered when a cell is about to be edited. + /// + /// Set Cancel to true to prevent the cell being edited. + /// You can change the Control to be something completely different. + [Category("ObjectListView"), + Description("This event is triggered when cell edit is about to begin.")] + public event CellEditEventHandler CellEditStarting; + + /// + /// Triggered when a cell editor needs to be validated + /// + /// + /// If this event is cancelled, focus will remain on the cell editor. + /// + [Category("ObjectListView"), + Description("This event is triggered when a cell editor is about to lose focus and its new contents need to be validated.")] + public event CellEditEventHandler CellEditValidating; + + /// + /// Triggered when a cell is left clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when the user left clicks a cell.")] + public event EventHandler CellClick; + + /// + /// Triggered when the mouse is above a cell. + /// + [Category("ObjectListView"), + Description("This event is triggered when the mouse is over a cell.")] + public event EventHandler CellOver; + + /// + /// Triggered when a cell is right clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when the user right clicks a cell.")] + public event EventHandler CellRightClick; + + /// + /// This event is triggered when a cell needs a tool tip. + /// + [Category("ObjectListView"), + Description("This event is triggered when a cell needs a tool tip.")] + public event EventHandler CellToolTipShowing; + + /// + /// This event is triggered when a checkbox is checked/unchecked on a subitem + /// + [Category("ObjectListView"), + Description("This event is triggered when a checkbox is checked/unchecked on a subitem.")] + public event EventHandler SubItemChecking; + + /// + /// Triggered when a column header is right clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when the user right clicks a column header.")] + public event ColumnRightClickEventHandler ColumnRightClick; + + /// + /// This event is triggered when the user releases a drag over an ObjectListView that + /// has a SimpleDropSink installed as the drop handler. + /// + [Category("ObjectListView"), + Description("This event is triggered when the user dropped items onto the control.")] + public event EventHandler Dropped; + + /// + /// This event is triggered when the control needs to filter its collection of objects. + /// + [Category("ObjectListView"), + Description("This event is triggered when the control needs to filter its collection of objects.")] + public event EventHandler Filter; + + /// + /// This event is triggered when a cell needs to be formatted. + /// + [Category("ObjectListView"), + Description("This event is triggered when a cell needs to be formatted.")] + public event EventHandler FormatCell; + + /// + /// This event is triggered when the frozenness of the control changes. + /// + [Category("ObjectListView"), + Description("This event is triggered when frozenness of the control changes.")] + public event EventHandler Freezing; + + /// + /// This event is triggered when a row needs to be formatted. + /// + [Category("ObjectListView"), + Description("This event is triggered when a row needs to be formatted.")] + public event EventHandler FormatRow; + + /// + /// This event is triggered when a group is about to collapse or expand. + /// This can be cancelled to prevent the expansion. + /// + [Category("ObjectListView"), + Description("This event is triggered when a group is about to collapse or expand.")] + public event EventHandler GroupExpandingCollapsing; + + /// + /// This event is triggered when a group changes state. + /// + [Category("ObjectListView"), + Description("This event is triggered when a group changes state.")] + public event EventHandler GroupStateChanged; + + /// + /// This event is triggered when a header checkbox is changing value + /// + [Category("ObjectListView"), + Description("This event is triggered when a header checkbox changes value.")] + public event EventHandler HeaderCheckBoxChanging; + + /// + /// This event is triggered when a header needs a tool tip. + /// + [Category("ObjectListView"), + Description("This event is triggered when a header needs a tool tip.")] + public event EventHandler HeaderToolTipShowing; + + /// + /// Triggered when the "hot" item changes + /// + [Category("ObjectListView"), + Description("This event is triggered when the hot item changed.")] + public event EventHandler HotItemChanged; + + /// + /// Triggered when a hyperlink cell is clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when a hyperlink cell is clicked.")] + public event EventHandler HyperlinkClicked; + + /// + /// Triggered when the task text of a group is clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when the task text of a group is clicked.")] + public event EventHandler GroupTaskClicked; + + /// + /// Is the value in the given cell a hyperlink. + /// + [Category("ObjectListView"), + Description("This event is triggered when the control needs to know if a given cell contains a hyperlink.")] + public event EventHandler IsHyperlink; + + /// + /// Some new objects are about to be added to an ObjectListView. + /// + [Category("ObjectListView"), + Description("This event is triggered when objects are about to be added to the control")] + public event EventHandler ItemsAdding; + + /// + /// The contents of the ObjectListView has changed. + /// + [Category("ObjectListView"), + Description("This event is triggered when the contents of the control have changed.")] + public event EventHandler ItemsChanged; + + /// + /// The contents of the ObjectListView is about to change via a SetObjects call + /// + /// + /// Set Cancelled to true to prevent the contents of the list changing. This does not work with virtual lists. + /// + [Category("ObjectListView"), + Description("This event is triggered when the contents of the control changes.")] + public event EventHandler ItemsChanging; + + /// + /// Some objects are about to be removed from an ObjectListView. + /// + [Category("ObjectListView"), + Description("This event is triggered when objects are removed from the control.")] + public event EventHandler ItemsRemoving; + + /// + /// This event is triggered when the user moves a drag over an ObjectListView that + /// has a SimpleDropSink installed as the drop handler, and when the source control + /// for the drag was an ObjectListView. + /// + /// + /// Handlers for this event should set the Effect argument and optionally the + /// InfoMsg property. They can also change any of the DropTarget* settings to change + /// the target of the drop. + /// + [Category("ObjectListView"), + Description("Can the dragged collection of model objects be dropped at the current mouse location")] + public event EventHandler ModelCanDrop; + + /// + /// This event is triggered when the user releases a drag over an ObjectListView that + /// has a SimpleDropSink installed as the drop handler and when the source control + /// for the drag was an ObjectListView. + /// + [Category("ObjectListView"), + Description("A collection of model objects from a ObjectListView has been dropped on this control")] + public event EventHandler ModelDropped; + + /// + /// This event is triggered once per user action that changes the selection state + /// of one or more rows. + /// + [Category("ObjectListView"), + Description("This event is triggered once per user action that changes the selection state of one or more rows.")] + public event EventHandler SelectionChanged; + + /// + /// This event is triggered when the contents of the ObjectListView has scrolled. + /// + [Category("ObjectListView"), + Description("This event is triggered when the contents of the ObjectListView has scrolled.")] + public event EventHandler Scroll; + + #endregion + + //----------------------------------------------------------------------------------- + + #region OnEvents + + /// + /// + /// + /// + protected virtual void OnAboutToCreateGroups(CreateGroupsEventArgs e) { + if (this.AboutToCreateGroups != null) + this.AboutToCreateGroups(this, e); + } + + /// + /// + /// + /// + protected virtual void OnBeforeCreatingGroups(CreateGroupsEventArgs e) { + if (this.BeforeCreatingGroups != null) + this.BeforeCreatingGroups(this, e); + } + + /// + /// + /// + /// + protected virtual void OnAfterCreatingGroups(CreateGroupsEventArgs e) { + if (this.AfterCreatingGroups != null) + this.AfterCreatingGroups(this, e); + } + + /// + /// + /// + /// + protected virtual void OnAfterSearching(AfterSearchingEventArgs e) { + if (this.AfterSearching != null) + this.AfterSearching(this, e); + } + + /// + /// + /// + /// + protected virtual void OnAfterSorting(AfterSortingEventArgs e) { + if (this.AfterSorting != null) + this.AfterSorting(this, e); + } + + /// + /// + /// + /// + protected virtual void OnBeforeSearching(BeforeSearchingEventArgs e) { + if (this.BeforeSearching != null) + this.BeforeSearching(this, e); + } + + /// + /// + /// + /// + protected virtual void OnBeforeSorting(BeforeSortingEventArgs e) { + if (this.BeforeSorting != null) + this.BeforeSorting(this, e); + } + + /// + /// + /// + /// + protected virtual void OnButtonClick(CellClickEventArgs args) { + if (this.ButtonClick != null) + this.ButtonClick(this, args); + } + + /// + /// + /// + /// + protected virtual void OnCanDrop(OlvDropEventArgs args) { + if (this.CanDrop != null) + this.CanDrop(this, args); + } + + /// + /// + /// + /// + protected virtual void OnCellClick(CellClickEventArgs args) { + if (this.CellClick != null) + this.CellClick(this, args); + } + + /// + /// + /// + /// + protected virtual void OnCellOver(CellOverEventArgs args) { + if (this.CellOver != null) + this.CellOver(this, args); + } + + /// + /// + /// + /// + protected virtual void OnCellRightClick(CellRightClickEventArgs args) { + if (this.CellRightClick != null) + this.CellRightClick(this, args); + } + + /// + /// + /// + /// + protected virtual void OnCellToolTip(ToolTipShowingEventArgs args) { + if (this.CellToolTipShowing != null) + this.CellToolTipShowing(this, args); + } + + /// + /// + /// + /// + protected virtual void OnSubItemChecking(SubItemCheckingEventArgs args) { + if (this.SubItemChecking != null) + this.SubItemChecking(this, args); + } + + /// + /// + /// + /// + protected virtual void OnColumnRightClick(ColumnRightClickEventArgs e) { + if (this.ColumnRightClick != null) + this.ColumnRightClick(this, e); + } + + /// + /// + /// + /// + protected virtual void OnDropped(OlvDropEventArgs args) { + if (this.Dropped != null) + this.Dropped(this, args); + } + + /// + /// + /// + /// + internal protected virtual void OnFilter(FilterEventArgs e) { + if (this.Filter != null) + this.Filter(this, e); + } + + /// + /// + /// + /// + protected virtual void OnFormatCell(FormatCellEventArgs args) { + if (this.FormatCell != null) + this.FormatCell(this, args); + } + + /// + /// + /// + /// + protected virtual void OnFormatRow(FormatRowEventArgs args) { + if (this.FormatRow != null) + this.FormatRow(this, args); + } + + /// + /// + /// + /// + protected virtual void OnFreezing(FreezeEventArgs args) { + if (this.Freezing != null) + this.Freezing(this, args); + } + + /// + /// + /// + /// + protected virtual void OnGroupExpandingCollapsing(GroupExpandingCollapsingEventArgs args) { + if (this.GroupExpandingCollapsing != null) + this.GroupExpandingCollapsing(this, args); + } + + /// + /// + /// + /// + protected virtual void OnGroupStateChanged(GroupStateChangedEventArgs args) { + if (this.GroupStateChanged != null) + this.GroupStateChanged(this, args); + } + + /// + /// + /// + /// + protected virtual void OnHeaderCheckBoxChanging(HeaderCheckBoxChangingEventArgs args) { + if (this.HeaderCheckBoxChanging != null) + this.HeaderCheckBoxChanging(this, args); + } + + /// + /// + /// + /// + protected virtual void OnHeaderToolTip(ToolTipShowingEventArgs args) { + if (this.HeaderToolTipShowing != null) + this.HeaderToolTipShowing(this, args); + } + + /// + /// + /// + /// + protected virtual void OnHotItemChanged(HotItemChangedEventArgs e) { + if (this.HotItemChanged != null) + this.HotItemChanged(this, e); + } + + /// + /// + /// + /// + protected virtual void OnHyperlinkClicked(HyperlinkClickedEventArgs e) { + if (this.HyperlinkClicked != null) + this.HyperlinkClicked(this, e); + } + + /// + /// + /// + /// + protected virtual void OnGroupTaskClicked(GroupTaskClickedEventArgs e) { + if (this.GroupTaskClicked != null) + this.GroupTaskClicked(this, e); + } + + /// + /// + /// + /// + protected virtual void OnIsHyperlink(IsHyperlinkEventArgs e) { + if (this.IsHyperlink != null) + this.IsHyperlink(this, e); + } + + /// + /// + /// + /// + protected virtual void OnItemsAdding(ItemsAddingEventArgs e) { + if (this.ItemsAdding != null) + this.ItemsAdding(this, e); + } + + /// + /// + /// + /// + protected virtual void OnItemsChanged(ItemsChangedEventArgs e) { + if (this.ItemsChanged != null) + this.ItemsChanged(this, e); + } + + /// + /// + /// + /// + protected virtual void OnItemsChanging(ItemsChangingEventArgs e) { + if (this.ItemsChanging != null) + this.ItemsChanging(this, e); + } + + /// + /// + /// + /// + protected virtual void OnItemsRemoving(ItemsRemovingEventArgs e) { + if (this.ItemsRemoving != null) + this.ItemsRemoving(this, e); + } + + /// + /// + /// + /// + protected virtual void OnModelCanDrop(ModelDropEventArgs args) { + if (this.ModelCanDrop != null) + this.ModelCanDrop(this, args); + } + + /// + /// + /// + /// + protected virtual void OnModelDropped(ModelDropEventArgs args) { + if (this.ModelDropped != null) + this.ModelDropped(this, args); + } + + /// + /// + /// + /// + protected virtual void OnSelectionChanged(EventArgs e) { + if (this.SelectionChanged != null) + this.SelectionChanged(this, e); + } + + /// + /// + /// + /// + protected virtual void OnScroll(ScrollEventArgs e) { + if (this.Scroll != null) + this.Scroll(this, e); + } + + + /// + /// Tell the world when a cell is about to be edited. + /// + protected virtual void OnCellEditStarting(CellEditEventArgs e) { + if (this.CellEditStarting != null) + this.CellEditStarting(this, e); + } + + /// + /// Tell the world when a cell is about to finish being edited. + /// + protected virtual void OnCellEditorValidating(CellEditEventArgs e) { + // Hack. ListView is an imperfect control container. It does not manage validation + // perfectly. If the ListView is part of a TabControl, and the cell editor loses + // focus by the user clicking on another tab, the TabControl processes the click + // and switches tabs, even if this Validating event cancels. This results in the + // strange situation where the cell editor is active, but isn't visible. When the + // user switches back to the tab with the ListView, composite controls like spin + // controls, DateTimePicker and ComboBoxes do not work properly. Specifically, + // keyboard input still works fine, but the controls do not respond to mouse + // input. SO, if the validation fails, we have to specifically give focus back to + // the cell editor. (this is the Select() call in the code below). + // But (there is always a 'but'), doing that changes the focus so the cell editor + // triggers another Validating event -- which fails again. From the user's point + // of view, they click away from the cell editor, and the validating code + // complains twice. So we only trigger a Validating event if more than 0.1 seconds + // has elapsed since the last validate event. + // I know it's a hack. I'm very open to hear a neater solution. + + // Also, this timed response stops us from sending a series of validation events + // if the user clicks and holds on the OLV scroll bar. + //System.Diagnostics.Debug.WriteLine(Environment.TickCount - lastValidatingEvent); + if ((Environment.TickCount - lastValidatingEvent) < 100) { + e.Cancel = true; + } else { + lastValidatingEvent = Environment.TickCount; + if (this.CellEditValidating != null) + this.CellEditValidating(this, e); + } + + lastValidatingEvent = Environment.TickCount; + } + + private int lastValidatingEvent = 0; + + /// + /// Tell the world when a cell is about to finish being edited. + /// + protected virtual void OnCellEditFinishing(CellEditEventArgs e) { + if (this.CellEditFinishing != null) + this.CellEditFinishing(this, e); + } + + /// + /// Tell the world when a cell has finished being edited. + /// + protected virtual void OnCellEditFinished(CellEditEventArgs e) { + if (this.CellEditFinished != null) + this.CellEditFinished(this, e); + } + + #endregion + } + + public partial class TreeListView { + + #region Events + + /// + /// This event is triggered when user input requests the expansion of a list item. + /// + [Category("ObjectListView"), + Description("This event is triggered when a branch is about to expand.")] + public event EventHandler Expanding; + + /// + /// This event is triggered when user input requests the collapse of a list item. + /// + [Category("ObjectListView"), + Description("This event is triggered when a branch is about to collapsed.")] + public event EventHandler Collapsing; + + /// + /// This event is triggered after the expansion of a list item due to user input. + /// + [Category("ObjectListView"), + Description("This event is triggered when a branch has been expanded.")] + public event EventHandler Expanded; + + /// + /// This event is triggered after the collapse of a list item due to user input. + /// + [Category("ObjectListView"), + Description("This event is triggered when a branch has been collapsed.")] + public event EventHandler Collapsed; + + #endregion + + #region OnEvents + + /// + /// Trigger the expanding event + /// + /// + protected virtual void OnExpanding(TreeBranchExpandingEventArgs e) { + if (this.Expanding != null) + this.Expanding(this, e); + } + + /// + /// Trigger the collapsing event + /// + /// + protected virtual void OnCollapsing(TreeBranchCollapsingEventArgs e) { + if (this.Collapsing != null) + this.Collapsing(this, e); + } + + /// + /// Trigger the expanded event + /// + /// + protected virtual void OnExpanded(TreeBranchExpandedEventArgs e) { + if (this.Expanded != null) + this.Expanded(this, e); + } + + /// + /// Trigger the collapsed event + /// + /// + protected virtual void OnCollapsed(TreeBranchCollapsedEventArgs e) { + if (this.Collapsed != null) + this.Collapsed(this, e); + } + + #endregion + } + + //----------------------------------------------------------------------------------- + + #region Event Parameter Blocks + + /// + /// Let the world know that a cell edit operation is beginning or ending + /// + public class CellEditEventArgs : EventArgs { + /// + /// Create an event args + /// + /// + /// + /// + /// + /// + public CellEditEventArgs(OLVColumn column, Control control, Rectangle cellBounds, OLVListItem item, int subItemIndex) { + this.Control = control; + this.column = column; + this.cellBounds = cellBounds; + this.listViewItem = item; + this.rowObject = item.RowObject; + this.subItemIndex = subItemIndex; + this.value = column.GetValue(item.RowObject); + } + + /// + /// Change this to true to cancel the cell editing operation. + /// + /// + /// During the CellEditStarting event, setting this to true will prevent the cell from being edited. + /// During the CellEditFinishing event, if this value is already true, this indicates that the user has + /// cancelled the edit operation and that the handler should perform cleanup only. Setting this to true, + /// will prevent the ObjectListView from trying to write the new value into the model object. + /// + public bool Cancel; + + /// + /// During the CellEditStarting event, this can be modified to be the control that you want + /// to edit the value. You must fully configure the control before returning from the event, + /// including its bounds and the value it is showing. + /// During the CellEditFinishing event, you can use this to get the value that the user + /// entered and commit that value to the model. Changing the control during the finishing + /// event has no effect. + /// + public Control Control; + + /// + /// The column of the cell that is going to be or has been edited. + /// + public OLVColumn Column { + get { return this.column; } + } + + private OLVColumn column; + + /// + /// The model object of the row of the cell that is going to be or has been edited. + /// + public Object RowObject { + get { return this.rowObject; } + } + + private Object rowObject; + + /// + /// The listview item of the cell that is going to be or has been edited. + /// + public OLVListItem ListViewItem { + get { return this.listViewItem; } + } + + private OLVListItem listViewItem; + + /// + /// The data value of the cell as it stands in the control. + /// + /// Only validate during Validating and Finishing events. + public Object NewValue { + get { return this.newValue; } + set { this.newValue = value; } + } + + private Object newValue; + + /// + /// The index of the cell that is going to be or has been edited. + /// + public int SubItemIndex { + get { return this.subItemIndex; } + } + + private int subItemIndex; + + /// + /// The data value of the cell before the edit operation began. + /// + public Object Value { + get { return this.value; } + } + + private Object value; + + /// + /// The bounds of the cell that is going to be or has been edited. + /// + public Rectangle CellBounds { + get { return this.cellBounds; } + } + + private Rectangle cellBounds; + + /// + /// Gets or sets whether the control used for editing should be auto matically disposed + /// when the cell edit operation finishes. Defaults to true + /// + /// If the control is expensive to create, you might want to cache it and reuse for + /// for various cells. If so, you don't want ObjectListView to dispose of the control automatically + public bool AutoDispose { + get { return autoDispose; } + set { autoDispose = value; } + } + + private bool autoDispose = true; + } + + /// + /// Event blocks for events that can be cancelled + /// + public class CancellableEventArgs : EventArgs { + /// + /// Has this event been cancelled by the event handler? + /// + public bool Canceled; + } + + /// + /// BeforeSorting + /// + public class BeforeSortingEventArgs : CancellableEventArgs { + /// + /// Create BeforeSortingEventArgs + /// + /// + /// + /// + /// + public BeforeSortingEventArgs(OLVColumn column, SortOrder order, OLVColumn column2, SortOrder order2) { + this.ColumnToGroupBy = column; + this.GroupByOrder = order; + this.ColumnToSort = column; + this.SortOrder = order; + this.SecondaryColumnToSort = column2; + this.SecondarySortOrder = order2; + } + + /// + /// Create BeforeSortingEventArgs + /// + /// + /// + /// + /// + /// + /// + public BeforeSortingEventArgs(OLVColumn groupColumn, SortOrder groupOrder, OLVColumn column, SortOrder order, OLVColumn column2, SortOrder order2) { + this.ColumnToGroupBy = groupColumn; + this.GroupByOrder = groupOrder; + this.ColumnToSort = column; + this.SortOrder = order; + this.SecondaryColumnToSort = column2; + this.SecondarySortOrder = order2; + } + + /// + /// Did the event handler already do the sorting for us? + /// + public bool Handled; + + /// + /// What column will be used for grouping + /// + public OLVColumn ColumnToGroupBy; + + /// + /// How will groups be ordered + /// + public SortOrder GroupByOrder; + + /// + /// What column will be used for sorting + /// + public OLVColumn ColumnToSort; + + /// + /// What order will be used for sorting. None means no sorting. + /// + public SortOrder SortOrder; + + /// + /// What column will be used for secondary sorting? + /// + public OLVColumn SecondaryColumnToSort; + + /// + /// What order will be used for secondary sorting? + /// + public SortOrder SecondarySortOrder; + } + + /// + /// Sorting has just occurred. + /// + public class AfterSortingEventArgs : EventArgs { + /// + /// Create a AfterSortingEventArgs + /// + /// + /// + /// + /// + /// + /// + public AfterSortingEventArgs(OLVColumn groupColumn, SortOrder groupOrder, OLVColumn column, SortOrder order, OLVColumn column2, SortOrder order2) { + this.columnToGroupBy = groupColumn; + this.groupByOrder = groupOrder; + this.columnToSort = column; + this.sortOrder = order; + this.secondaryColumnToSort = column2; + this.secondarySortOrder = order2; + } + + /// + /// Create a AfterSortingEventArgs + /// + /// + public AfterSortingEventArgs(BeforeSortingEventArgs args) { + this.columnToGroupBy = args.ColumnToGroupBy; + this.groupByOrder = args.GroupByOrder; + this.columnToSort = args.ColumnToSort; + this.sortOrder = args.SortOrder; + this.secondaryColumnToSort = args.SecondaryColumnToSort; + this.secondarySortOrder = args.SecondarySortOrder; + } + + /// + /// What column was used for grouping? + /// + public OLVColumn ColumnToGroupBy { + get { return columnToGroupBy; } + } + + private OLVColumn columnToGroupBy; + + /// + /// What ordering was used for grouping? + /// + public SortOrder GroupByOrder { + get { return groupByOrder; } + } + + private SortOrder groupByOrder; + + /// + /// What column was used for sorting? + /// + public OLVColumn ColumnToSort { + get { return columnToSort; } + } + + private OLVColumn columnToSort; + + /// + /// What ordering was used for sorting? + /// + public SortOrder SortOrder { + get { return sortOrder; } + } + + private SortOrder sortOrder; + + /// + /// What column was used for secondary sorting? + /// + public OLVColumn SecondaryColumnToSort { + get { return secondaryColumnToSort; } + } + + private OLVColumn secondaryColumnToSort; + + /// + /// What order was used for secondary sorting? + /// + public SortOrder SecondarySortOrder { + get { return secondarySortOrder; } + } + + private SortOrder secondarySortOrder; + } + + /// + /// This event is triggered when the contents of a list have changed + /// and we want the world to have a chance to filter the list. + /// + public class FilterEventArgs : EventArgs { + /// + /// Create a FilterEventArgs + /// + /// + public FilterEventArgs(IEnumerable objects) { + this.Objects = objects; + } + + /// + /// Gets or sets what objects are being filtered + /// + public IEnumerable Objects; + + /// + /// Gets or sets what objects survived the filtering + /// + public IEnumerable FilteredObjects; + } + + /// + /// This event is triggered after the items in the list have been changed, + /// either through SetObjects, AddObjects or RemoveObjects. + /// + public class ItemsChangedEventArgs : EventArgs { + /// + /// Create a ItemsChangedEventArgs + /// + public ItemsChangedEventArgs() { } + + /// + /// Constructor for this event when used by a virtual list + /// + /// + /// + public ItemsChangedEventArgs(int oldObjectCount, int newObjectCount) { + this.oldObjectCount = oldObjectCount; + this.newObjectCount = newObjectCount; + } + + /// + /// Gets how many items were in the list before it changed + /// + public int OldObjectCount { + get { return oldObjectCount; } + } + + private int oldObjectCount; + + /// + /// Gets how many objects are in the list after the change. + /// + public int NewObjectCount { + get { return newObjectCount; } + } + + private int newObjectCount; + } + + /// + /// This event is triggered by AddObjects before any change has been made to the list. + /// + public class ItemsAddingEventArgs : CancellableEventArgs { + /// + /// Create an ItemsAddingEventArgs + /// + /// + public ItemsAddingEventArgs(ICollection objectsToAdd) { + this.ObjectsToAdd = objectsToAdd; + } + + /// + /// Create an ItemsAddingEventArgs + /// + /// + /// + public ItemsAddingEventArgs(int index, ICollection objectsToAdd) { + this.Index = index; + this.ObjectsToAdd = objectsToAdd; + } + + /// + /// Gets or sets where the collection is going to be inserted. + /// + public int Index; + + /// + /// Gets or sets the objects to be added to the list + /// + public ICollection ObjectsToAdd; + } + + /// + /// This event is triggered by SetObjects before any change has been made to the list. + /// + /// + /// When used with a virtual list, OldObjects will always be null. + /// + public class ItemsChangingEventArgs : CancellableEventArgs { + /// + /// Create ItemsChangingEventArgs + /// + /// + /// + public ItemsChangingEventArgs(IEnumerable oldObjects, IEnumerable newObjects) { + this.oldObjects = oldObjects; + this.NewObjects = newObjects; + } + + /// + /// Gets the objects that were in the list before it change. + /// For virtual lists, this will always be null. + /// + public IEnumerable OldObjects { + get { return oldObjects; } + } + + private IEnumerable oldObjects; + + /// + /// Gets or sets the objects that will be in the list after it changes. + /// + public IEnumerable NewObjects; + } + + /// + /// This event is triggered by RemoveObjects before any change has been made to the list. + /// + public class ItemsRemovingEventArgs : CancellableEventArgs { + /// + /// Create an ItemsRemovingEventArgs + /// + /// + public ItemsRemovingEventArgs(ICollection objectsToRemove) { + this.ObjectsToRemove = objectsToRemove; + } + + /// + /// Gets or sets the objects that will be removed + /// + public ICollection ObjectsToRemove; + } + + /// + /// Triggered after the user types into a list + /// + public class AfterSearchingEventArgs : EventArgs { + /// + /// Create an AfterSearchingEventArgs + /// + /// + /// + public AfterSearchingEventArgs(string stringToFind, int indexSelected) { + this.stringToFind = stringToFind; + this.indexSelected = indexSelected; + } + + /// + /// Gets the string that was actually searched for + /// + public string StringToFind { + get { return this.stringToFind; } + } + + private string stringToFind; + + /// + /// Gets or sets whether an the event handler already handled this event + /// + public bool Handled; + + /// + /// Gets the index of the row that was selected by the search. + /// -1 means that no row was matched + /// + public int IndexSelected { + get { return this.indexSelected; } + } + + private int indexSelected; + } + + /// + /// Triggered when the user types into a list + /// + public class BeforeSearchingEventArgs : CancellableEventArgs { + /// + /// Create BeforeSearchingEventArgs + /// + /// + /// + public BeforeSearchingEventArgs(string stringToFind, int startSearchFrom) { + this.StringToFind = stringToFind; + this.StartSearchFrom = startSearchFrom; + } + + /// + /// Gets or sets the string that will be found by the search routine + /// + /// Modifying this value does not modify the memory of what the user has typed. + /// When the user next presses a character, the search string will revert to what + /// the user has actually typed. + public string StringToFind; + + /// + /// Gets or sets the index of the first row that will be considered to matching. + /// + public int StartSearchFrom; + } + + /// + /// The parameter block when telling the world about a cell based event + /// + public class CellEventArgs : EventArgs { + /// + /// Gets the ObjectListView that is the source of the event + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + + private ObjectListView listView; + + /// + /// Gets the model object under the cell + /// + /// This is null for events triggered by the header. + public object Model { + get { return this.model; } + internal set { this.model = value; } + } + + private object model; + + /// + /// Gets the row index of the cell + /// + /// This is -1 for events triggered by the header. + public int RowIndex { + get { return this.rowIndex; } + internal set { this.rowIndex = value; } + } + + private int rowIndex = -1; + + /// + /// Gets the column index of the cell + /// + /// This is -1 when the view is not in details view. + public int ColumnIndex { + get { return this.columnIndex; } + internal set { this.columnIndex = value; } + } + + private int columnIndex = -1; + + /// + /// Gets the column of the cell + /// + /// This is null when the view is not in details view. + public OLVColumn Column { + get { return this.column; } + internal set { this.column = value; } + } + + private OLVColumn column; + + /// + /// Gets the location of the mouse at the time of the event + /// + public Point Location { + get { return this.location; } + internal set { this.location = value; } + } + + private Point location; + + /// + /// Gets the state of the modifier keys at the time of the event + /// + public Keys ModifierKeys { + get { return this.modifierKeys; } + internal set { this.modifierKeys = value; } + } + + private Keys modifierKeys; + + /// + /// Gets the item of the cell + /// + public OLVListItem Item { + get { return item; } + internal set { this.item = value; } + } + + private OLVListItem item; + + /// + /// Gets the subitem of the cell + /// + /// This is null when the view is not in details view and + /// for event triggered by the header + public OLVListSubItem SubItem { + get { return subItem; } + internal set { this.subItem = value; } + } + + private OLVListSubItem subItem; + + /// + /// Gets the HitTest object that determined which cell was hit + /// + public OlvListViewHitTestInfo HitTest { + get { return hitTest; } + internal set { hitTest = value; } + } + + private OlvListViewHitTestInfo hitTest; + + /// + /// Gets or set if this event completely handled. If it was, no further processing + /// will be done for it. + /// + public bool Handled; + } + + /// + /// Tells the world that a cell was clicked + /// + public class CellClickEventArgs : CellEventArgs { + /// + /// Gets or sets the number of clicks associated with this event + /// + public int ClickCount { + get { return this.clickCount; } + set { this.clickCount = value; } + } + + private int clickCount; + } + + /// + /// Tells the world that a cell was right clicked + /// + public class CellRightClickEventArgs : CellEventArgs { + /// + /// Gets or sets the menu that should be displayed as a result of this event. + /// + /// The menu will be positioned at Location, so changing that property changes + /// where the menu will be displayed. + public ContextMenuStrip MenuStrip; + } + + /// + /// Tell the world that the mouse is over a given cell + /// + public class CellOverEventArgs : CellEventArgs { } + + /// + /// Tells the world that the frozen-ness of the ObjectListView has changed. + /// + public class FreezeEventArgs : EventArgs { + /// + /// Make a FreezeEventArgs + /// + /// + public FreezeEventArgs(int freeze) { + this.FreezeLevel = freeze; + } + + /// + /// How frozen is the control? 0 means that the control is unfrozen, + /// more than 0 indicates froze. + /// + public int FreezeLevel { + get { return this.freezeLevel; } + set { this.freezeLevel = value; } + } + + private int freezeLevel; + } + + /// + /// The parameter block when telling the world that a tool tip is about to be shown. + /// + public class ToolTipShowingEventArgs : CellEventArgs { + /// + /// Gets the tooltip control that is triggering the tooltip event + /// + public ToolTipControl ToolTipControl { + get { return this.toolTipControl; } + internal set { this.toolTipControl = value; } + } + + private ToolTipControl toolTipControl; + + /// + /// Gets or sets the text should be shown on the tooltip for this event + /// + /// Setting this to empty or null prevents any tooltip from showing + public string Text; + + /// + /// In what direction should the text for this tooltip be drawn? + /// + public RightToLeft RightToLeft; + + /// + /// Should the tooltip for this event been shown in bubble style? + /// + /// This doesn't work reliable under Vista + public bool? IsBalloon; + + /// + /// What color should be used for the background of the tooltip + /// + /// Setting this does nothing under Vista + public Color? BackColor; + + /// + /// What color should be used for the foreground of the tooltip + /// + /// Setting this does nothing under Vista + public Color? ForeColor; + + /// + /// What string should be used as the title for the tooltip for this event? + /// + public string Title; + + /// + /// Which standard icon should be used for the tooltip for this event + /// + public ToolTipControl.StandardIcons? StandardIcon; + + /// + /// How many milliseconds should the tooltip remain before it automatically + /// disappears. + /// + public int? AutoPopDelay; + + /// + /// What font should be used to draw the text of the tooltip? + /// + public Font Font; + } + + /// + /// Common information to all hyperlink events + /// + public class HyperlinkEventArgs : EventArgs { + //TODO: Unified with CellEventArgs + + /// + /// Gets the ObjectListView that is the source of the event + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + + private ObjectListView listView; + + /// + /// Gets the model object under the cell + /// + public object Model { + get { return this.model; } + internal set { this.model = value; } + } + + private object model; + + /// + /// Gets the row index of the cell + /// + public int RowIndex { + get { return this.rowIndex; } + internal set { this.rowIndex = value; } + } + + private int rowIndex = -1; + + /// + /// Gets the column index of the cell + /// + /// This is -1 when the view is not in details view. + public int ColumnIndex { + get { return this.columnIndex; } + internal set { this.columnIndex = value; } + } + + private int columnIndex = -1; + + /// + /// Gets the column of the cell + /// + /// This is null when the view is not in details view. + public OLVColumn Column { + get { return this.column; } + internal set { this.column = value; } + } + + private OLVColumn column; + + /// + /// Gets the item of the cell + /// + public OLVListItem Item { + get { return item; } + internal set { this.item = value; } + } + + private OLVListItem item; + + /// + /// Gets the subitem of the cell + /// + /// This is null when the view is not in details view + public OLVListSubItem SubItem { + get { return subItem; } + internal set { this.subItem = value; } + } + + private OLVListSubItem subItem; + + /// + /// Gets the ObjectListView that is the source of the event + /// + public string Url { + get { return this.url; } + internal set { this.url = value; } + } + + private string url; + + /// + /// Gets or set if this event completely handled. If it was, no further processing + /// will be done for it. + /// + public bool Handled { + get { return handled; } + set { handled = value; } + } + + private bool handled; + + } + + /// + /// + /// + public class IsHyperlinkEventArgs : EventArgs { + /// + /// Gets the ObjectListView that is the source of the event + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + + private ObjectListView listView; + + /// + /// Gets the model object under the cell + /// + public object Model { + get { return this.model; } + internal set { this.model = value; } + } + + private object model; + + /// + /// Gets the column of the cell + /// + /// This is null when the view is not in details view. + public OLVColumn Column { + get { return this.column; } + internal set { this.column = value; } + } + + private OLVColumn column; + + /// + /// Gets the text of the cell + /// + public string Text { + get { return this.text; } + internal set { this.text = value; } + } + + private string text; + + /// + /// Gets or sets whether or not this cell is a hyperlink. + /// Defaults to true for enabled rows and false for disabled rows. + /// + public bool IsHyperlink { + get { return this.isHyperlink; } + set { this.isHyperlink = value; } + } + + private bool isHyperlink; + + /// + /// Gets or sets the url that should be invoked when this cell is clicked. + /// + /// Setting this to None or String.Empty means that this cell is not a hyperlink + public string Url; + } + + /// + /// + public class FormatRowEventArgs : EventArgs { + //TODO: Unified with CellEventArgs + + /// + /// Gets the ObjectListView that is the source of the event + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + + private ObjectListView listView; + + /// + /// Gets the item of the cell + /// + public OLVListItem Item { + get { return item; } + internal set { this.item = value; } + } + + private OLVListItem item; + + /// + /// Gets the model object under the cell + /// + public object Model { + get { return this.Item.RowObject; } + } + + /// + /// Gets the row index of the cell + /// + public int RowIndex { + get { return this.rowIndex; } + internal set { this.rowIndex = value; } + } + + private int rowIndex = -1; + + /// + /// Gets the display index of the row + /// + public int DisplayIndex { + get { return this.displayIndex; } + internal set { this.displayIndex = value; } + } + + private int displayIndex = -1; + + /// + /// Should events be triggered for each cell in this row? + /// + public bool UseCellFormatEvents { + get { return useCellFormatEvents; } + set { useCellFormatEvents = value; } + } + + private bool useCellFormatEvents; + } + + /// + /// Parameter block for FormatCellEvent + /// + public class FormatCellEventArgs : FormatRowEventArgs { + /// + /// Gets the column index of the cell + /// + /// This is -1 when the view is not in details view. + public int ColumnIndex { + get { return this.columnIndex; } + internal set { this.columnIndex = value; } + } + + private int columnIndex = -1; + + /// + /// Gets the column of the cell + /// + /// This is null when the view is not in details view. + public OLVColumn Column { + get { return this.column; } + internal set { this.column = value; } + } + + private OLVColumn column; + + /// + /// Gets the subitem of the cell + /// + /// This is null when the view is not in details view + public OLVListSubItem SubItem { + get { return subItem; } + internal set { this.subItem = value; } + } + + private OLVListSubItem subItem; + + /// + /// Gets the model value that is being displayed by the cell. + /// + /// This is null when the view is not in details view + public object CellValue { + get { return this.SubItem == null ? null : this.SubItem.ModelValue; } + } + } + + /// + /// The event args when a hyperlink is clicked + /// + public class HyperlinkClickedEventArgs : CellEventArgs { + /// + /// Gets the url that was associated with this cell. + /// + public string Url { + get { return url; } + set { url = value; } + } + + private string url; + + } + + /// + /// The event args when the check box in a column header is changing + /// + public class HeaderCheckBoxChangingEventArgs : CancelEventArgs { + + /// + /// Get the column whose checkbox is changing + /// + public OLVColumn Column { + get { return column; } + internal set { column = value; } + } + + private OLVColumn column; + + /// + /// Get or set the new state that should be used by the column + /// + public CheckState NewCheckState { + get { return newCheckState; } + set { newCheckState = value; } + } + + private CheckState newCheckState; + } + + /// + /// The event args when the hot item changed + /// + public class HotItemChangedEventArgs : EventArgs { + /// + /// Gets or set if this event completely handled. If it was, no further processing + /// will be done for it. + /// + public bool Handled { + get { return handled; } + set { handled = value; } + } + + private bool handled; + + /// + /// Gets the part of the cell that the mouse is over + /// + public HitTestLocation HotCellHitLocation { + get { return newHotCellHitLocation; } + internal set { newHotCellHitLocation = value; } + } + + private HitTestLocation newHotCellHitLocation; + + /// + /// Gets an extended indication of the part of item/subitem/group that the mouse is currently over + /// + public virtual HitTestLocationEx HotCellHitLocationEx { + get { return this.hotCellHitLocationEx; } + internal set { this.hotCellHitLocationEx = value; } + } + + private HitTestLocationEx hotCellHitLocationEx; + + /// + /// Gets the index of the column that the mouse is over + /// + /// In non-details view, this will always be 0. + public int HotColumnIndex { + get { return newHotColumnIndex; } + internal set { newHotColumnIndex = value; } + } + + private int newHotColumnIndex; + + /// + /// Gets the index of the row that the mouse is over + /// + public int HotRowIndex { + get { return newHotRowIndex; } + internal set { newHotRowIndex = value; } + } + + private int newHotRowIndex; + + /// + /// Gets the group that the mouse is over + /// + public OLVGroup HotGroup { + get { return hotGroup; } + internal set { hotGroup = value; } + } + + private OLVGroup hotGroup; + + /// + /// Gets the part of the cell that the mouse used to be over + /// + public HitTestLocation OldHotCellHitLocation { + get { return oldHotCellHitLocation; } + internal set { oldHotCellHitLocation = value; } + } + + private HitTestLocation oldHotCellHitLocation; + + /// + /// Gets an extended indication of the part of item/subitem/group that the mouse used to be over + /// + public virtual HitTestLocationEx OldHotCellHitLocationEx { + get { return this.oldHotCellHitLocationEx; } + internal set { this.oldHotCellHitLocationEx = value; } + } + + private HitTestLocationEx oldHotCellHitLocationEx; + + /// + /// Gets the index of the column that the mouse used to be over + /// + public int OldHotColumnIndex { + get { return oldHotColumnIndex; } + internal set { oldHotColumnIndex = value; } + } + + private int oldHotColumnIndex; + + /// + /// Gets the index of the row that the mouse used to be over + /// + public int OldHotRowIndex { + get { return oldHotRowIndex; } + internal set { oldHotRowIndex = value; } + } + + private int oldHotRowIndex; + + /// + /// Gets the group that the mouse used to be over + /// + public OLVGroup OldHotGroup { + get { return oldHotGroup; } + internal set { oldHotGroup = value; } + } + + private OLVGroup oldHotGroup; + + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object. + /// + /// 2 + public override string ToString() { + return string.Format("NewHotCellHitLocation: {0}, HotCellHitLocationEx: {1}, NewHotColumnIndex: {2}, NewHotRowIndex: {3}, HotGroup: {4}", this.newHotCellHitLocation, this.hotCellHitLocationEx, this.newHotColumnIndex, this.newHotRowIndex, this.hotGroup); + } + } + + /// + /// Let the world know that a checkbox on a subitem is changing + /// + public class SubItemCheckingEventArgs : CancellableEventArgs { + /// + /// Create a new event block + /// + /// + /// + /// + /// + /// + public SubItemCheckingEventArgs(OLVColumn column, OLVListItem item, int subItemIndex, CheckState currentValue, CheckState newValue) { + this.column = column; + this.listViewItem = item; + this.subItemIndex = subItemIndex; + this.currentValue = currentValue; + this.newValue = newValue; + } + + /// + /// The column of the cell that is having its checkbox changed. + /// + public OLVColumn Column { + get { return this.column; } + } + + private OLVColumn column; + + /// + /// The model object of the row of the cell that is having its checkbox changed. + /// + public Object RowObject { + get { return this.listViewItem.RowObject; } + } + + /// + /// The listview item of the cell that is having its checkbox changed. + /// + public OLVListItem ListViewItem { + get { return this.listViewItem; } + } + + private OLVListItem listViewItem; + + /// + /// The current check state of the cell. + /// + public CheckState CurrentValue { + get { return this.currentValue; } + } + + private CheckState currentValue; + + /// + /// The proposed new check state of the cell. + /// + public CheckState NewValue { + get { return this.newValue; } + set { this.newValue = value; } + } + + private CheckState newValue; + + /// + /// The index of the cell that is going to be or has been edited. + /// + public int SubItemIndex { + get { return this.subItemIndex; } + } + + private int subItemIndex; + } + + /// + /// This event argument block is used when groups are created for a list. + /// + public class CreateGroupsEventArgs : EventArgs { + /// + /// Create a CreateGroupsEventArgs + /// + /// + public CreateGroupsEventArgs(GroupingParameters parms) { + this.parameters = parms; + } + + /// + /// Gets the settings that control the creation of groups + /// + public GroupingParameters Parameters { + get { return this.parameters; } + } + + private GroupingParameters parameters; + + /// + /// Gets or sets the groups that should be used + /// + public IList Groups { + get { return this.groups; } + set { this.groups = value; } + } + + private IList groups; + + /// + /// Has this event been cancelled by the event handler? + /// + public bool Canceled { + get { return canceled; } + set { canceled = value; } + } + + private bool canceled; + + } + + /// + /// This event argument block is used when the text of a group task is clicked + /// + public class GroupTaskClickedEventArgs : EventArgs { + /// + /// Create a GroupTaskClickedEventArgs + /// + /// + public GroupTaskClickedEventArgs(OLVGroup group) { + this.group = group; + } + + /// + /// Gets which group was clicked + /// + public OLVGroup Group { + get { return this.group; } + } + + private readonly OLVGroup group; + } + + /// + /// This event argument block is used when a group is about to expand or collapse + /// + public class GroupExpandingCollapsingEventArgs : CancellableEventArgs { + /// + /// Create a GroupExpandingCollapsingEventArgs + /// + /// + public GroupExpandingCollapsingEventArgs(OLVGroup group) { + if (group == null) throw new ArgumentNullException("group"); + this.olvGroup = group; + } + + /// + /// Gets which group is expanding/collapsing + /// + public OLVGroup Group { + get { return this.olvGroup; } + } + + private readonly OLVGroup olvGroup; + + /// + /// Gets whether this event is going to expand the group. + /// If this is false, the group must be collapsing. + /// + public bool IsExpanding { + get { return this.Group.Collapsed; } + } + } + + /// + /// This event argument block is used when the state of group has changed (collapsed, selected) + /// + public class GroupStateChangedEventArgs : EventArgs { + /// + /// Create a GroupStateChangedEventArgs + /// + /// + /// + /// + public GroupStateChangedEventArgs(OLVGroup group, GroupState oldState, GroupState newState) { + this.group = group; + this.oldState = oldState; + this.newState = newState; + } + + /// + /// Gets whether the group was collapsed by this event + /// + public bool Collapsed { + get { + return ((oldState & GroupState.LVGS_COLLAPSED) != GroupState.LVGS_COLLAPSED) && + ((newState & GroupState.LVGS_COLLAPSED) == GroupState.LVGS_COLLAPSED); + } + } + + /// + /// Gets whether the group was focused by this event + /// + public bool Focused { + get { + return ((oldState & GroupState.LVGS_FOCUSED) != GroupState.LVGS_FOCUSED) && + ((newState & GroupState.LVGS_FOCUSED) == GroupState.LVGS_FOCUSED); + } + } + + /// + /// Gets whether the group was selected by this event + /// + public bool Selected { + get { + return ((oldState & GroupState.LVGS_SELECTED) != GroupState.LVGS_SELECTED) && + ((newState & GroupState.LVGS_SELECTED) == GroupState.LVGS_SELECTED); + } + } + + /// + /// Gets whether the group was uncollapsed by this event + /// + public bool Uncollapsed { + get { + return ((oldState & GroupState.LVGS_COLLAPSED) == GroupState.LVGS_COLLAPSED) && + ((newState & GroupState.LVGS_COLLAPSED) != GroupState.LVGS_COLLAPSED); + } + } + + /// + /// Gets whether the group was unfocused by this event + /// + public bool Unfocused { + get { + return ((oldState & GroupState.LVGS_FOCUSED) == GroupState.LVGS_FOCUSED) && + ((newState & GroupState.LVGS_FOCUSED) != GroupState.LVGS_FOCUSED); + } + } + + /// + /// Gets whether the group was unselected by this event + /// + public bool Unselected { + get { + return ((oldState & GroupState.LVGS_SELECTED) == GroupState.LVGS_SELECTED) && + ((newState & GroupState.LVGS_SELECTED) != GroupState.LVGS_SELECTED); + } + } + + /// + /// Gets which group had its state changed + /// + public OLVGroup Group { + get { return this.group; } + } + + private readonly OLVGroup group; + + /// + /// Gets the previous state of the group + /// + public GroupState OldState { + get { return this.oldState; } + } + + private readonly GroupState oldState; + + + /// + /// Gets the new state of the group + /// + public GroupState NewState { + get { return this.newState; } + } + + private readonly GroupState newState; + } + + /// + /// This event argument block is used when a branch of a tree is about to be expanded + /// + public class TreeBranchExpandingEventArgs : CancellableEventArgs { + /// + /// Create a new event args + /// + /// + /// + public TreeBranchExpandingEventArgs(object model, OLVListItem item) { + this.Model = model; + this.Item = item; + } + + /// + /// Gets the model that is about to expand. If null, all branches are going to be expanded. + /// + public object Model { + get { return model; } + private set { model = value; } + } + + private object model; + + /// + /// Gets the OLVListItem that is about to be expanded + /// + public OLVListItem Item { + get { return item; } + private set { item = value; } + } + + private OLVListItem item; + + } + + /// + /// This event argument block is used when a branch of a tree has just been expanded + /// + public class TreeBranchExpandedEventArgs : EventArgs { + /// + /// Create a new event args + /// + /// + /// + public TreeBranchExpandedEventArgs(object model, OLVListItem item) { + this.Model = model; + this.Item = item; + } + + /// + /// Gets the model that is was expanded. If null, all branches were expanded. + /// + public object Model { + get { return model; } + private set { model = value; } + } + + private object model; + + /// + /// Gets the OLVListItem that was expanded + /// + public OLVListItem Item { + get { return item; } + private set { item = value; } + } + + private OLVListItem item; + + } + + /// + /// This event argument block is used when a branch of a tree is about to be collapsed + /// + public class TreeBranchCollapsingEventArgs : CancellableEventArgs { + /// + /// Create a new event args + /// + /// + /// + public TreeBranchCollapsingEventArgs(object model, OLVListItem item) { + this.Model = model; + this.Item = item; + } + + /// + /// Gets the model that is about to collapse. If this is null, all models are going to collapse. + /// + public object Model { + get { return model; } + private set { model = value; } + } + + private object model; + + /// + /// Gets the OLVListItem that is about to be collapsed. Can be null + /// + public OLVListItem Item { + get { return item; } + private set { item = value; } + } + + private OLVListItem item; + } + + + /// + /// This event argument block is used when a branch of a tree has just been collapsed + /// + public class TreeBranchCollapsedEventArgs : EventArgs { + /// + /// Create a new event args + /// + /// + /// + public TreeBranchCollapsedEventArgs(object model, OLVListItem item) { + this.Model = model; + this.Item = item; + } + + /// + /// Gets the model that is was collapsed. If null, all branches were collapsed + /// + public object Model { + get { return model; } + private set { model = value; } + } + + private object model; + + /// + /// Gets the OLVListItem that was collapsed + /// + public OLVListItem Item { + get { return item; } + private set { item = value; } + } + + private OLVListItem item; + + } + + /// + /// Tells the world that a column header was right clicked + /// + public class ColumnRightClickEventArgs : ColumnClickEventArgs { + public ColumnRightClickEventArgs(int columnIndex, ToolStripDropDown menu, Point location) : base(columnIndex) { + MenuStrip = menu; + Location = location; + } + + /// + /// Set this to true to cancel the right click operation. + /// + public bool Cancel; + + /// + /// Gets or sets the menu that should be displayed as a result of this event. + /// + /// The menu will be positioned at Location, so changing that property changes + /// where the menu will be displayed. + public ToolStripDropDown MenuStrip; + + /// + /// Gets the location of the mouse at the time of the event + /// + public Point Location; + } + + #endregion +} diff --git a/ObjectListView/Implementation/GroupingParameters.cs b/ObjectListView/Implementation/GroupingParameters.cs new file mode 100644 index 00000000..a87f217c --- /dev/null +++ b/ObjectListView/Implementation/GroupingParameters.cs @@ -0,0 +1,204 @@ +/* + * GroupingParameters - All the data that is used to create groups in an ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; + +namespace BrightIdeasSoftware { + + /// + /// This class contains all the settings used when groups are created + /// + public class GroupingParameters { + /// + /// Create a GroupingParameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public GroupingParameters(ObjectListView olv, OLVColumn groupByColumn, SortOrder groupByOrder, + OLVColumn column, SortOrder order, OLVColumn secondaryColumn, SortOrder secondaryOrder, + string titleFormat, string titleSingularFormat, bool sortItemsByPrimaryColumn) { + this.ListView = olv; + this.GroupByColumn = groupByColumn; + this.GroupByOrder = groupByOrder; + this.PrimarySort = column; + this.PrimarySortOrder = order; + this.SecondarySort = secondaryColumn; + this.SecondarySortOrder = secondaryOrder; + this.SortItemsByPrimaryColumn = sortItemsByPrimaryColumn; + this.TitleFormat = titleFormat; + this.TitleSingularFormat = titleSingularFormat; + } + + /// + /// Gets or sets the ObjectListView being grouped + /// + public ObjectListView ListView { + get { return this.listView; } + set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Gets or sets the column used to create groups + /// + public OLVColumn GroupByColumn { + get { return this.groupByColumn; } + set { this.groupByColumn = value; } + } + private OLVColumn groupByColumn; + + /// + /// In what order will the groups themselves be sorted? + /// + public SortOrder GroupByOrder { + get { return this.groupByOrder; } + set { this.groupByOrder = value; } + } + private SortOrder groupByOrder; + + /// + /// If this is set, this comparer will be used to order the groups + /// + public IComparer GroupComparer { + get { return this.groupComparer; } + set { this.groupComparer = value; } + } + private IComparer groupComparer; + + /// + /// If this is set, this comparer will be used to order items within each group + /// + public IComparer ItemComparer { + get { return this.itemComparer; } + set { this.itemComparer = value; } + } + private IComparer itemComparer; + + /// + /// Gets or sets the column that will be the primary sort + /// + public OLVColumn PrimarySort { + get { return this.primarySort; } + set { this.primarySort = value; } + } + private OLVColumn primarySort; + + /// + /// Gets or sets the ordering for the primary sort + /// + public SortOrder PrimarySortOrder { + get { return this.primarySortOrder; } + set { this.primarySortOrder = value; } + } + private SortOrder primarySortOrder; + + /// + /// Gets or sets the column used for secondary sorting + /// + public OLVColumn SecondarySort { + get { return this.secondarySort; } + set { this.secondarySort = value; } + } + private OLVColumn secondarySort; + + /// + /// Gets or sets the ordering for the secondary sort + /// + public SortOrder SecondarySortOrder { + get { return this.secondarySortOrder; } + set { this.secondarySortOrder = value; } + } + private SortOrder secondarySortOrder; + + /// + /// Gets or sets the title format used for groups with zero or more than one element + /// + public string TitleFormat { + get { return this.titleFormat; } + set { this.titleFormat = value; } + } + private string titleFormat; + + /// + /// Gets or sets the title format used for groups with only one element + /// + public string TitleSingularFormat { + get { return this.titleSingularFormat; } + set { this.titleSingularFormat = value; } + } + private string titleSingularFormat; + + /// + /// Gets or sets whether the items should be sorted by the primary column + /// + public bool SortItemsByPrimaryColumn { + get { return this.sortItemsByPrimaryColumn; } + set { this.sortItemsByPrimaryColumn = value; } + } + private bool sortItemsByPrimaryColumn; + + /// + /// Create an OLVGroup for the given information + /// + /// + /// + /// + /// + public OLVGroup CreateGroup(object key, int count, bool hasCollapsibleGroups) { + string title = GroupByColumn.ConvertGroupKeyToTitle(key); + if (!String.IsNullOrEmpty(TitleFormat)) + { + string format = (count == 1 ? TitleSingularFormat : TitleFormat); + try + { + title = String.Format(format, title, count); + } + catch (FormatException) + { + title = "Invalid group format: " + format; + } + } + OLVGroup lvg = new OLVGroup(title); + lvg.Column = GroupByColumn; + lvg.Collapsible = hasCollapsibleGroups; + lvg.Key = key; + lvg.SortValue = key as IComparable; + return lvg; + } + } +} diff --git a/ObjectListView/Implementation/Groups.cs b/ObjectListView/Implementation/Groups.cs new file mode 100644 index 00000000..33a7cb21 --- /dev/null +++ b/ObjectListView/Implementation/Groups.cs @@ -0,0 +1,761 @@ +/* + * Groups - Enhancements to the normal ListViewGroup + * + * Author: Phillip Piper + * Date: 22/08/2009 6:03PM + * + * Change log: + * v2.3 + * 2009-09-09 JPP - Added Collapsed and Collapsible properties + * 2009-09-01 JPP - Cleaned up code, added more docs + * - Works under VS2005 again + * 2009-08-22 JPP - Initial version + * + * To do: + * - Implement subseting + * - Implement footer items + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Windows.Forms; +using System.Runtime.InteropServices; + +namespace BrightIdeasSoftware +{ + /// + /// These values indicate what is the state of the group. These values + /// are taken directly from the SDK and many are not used by ObjectListView. + /// + [Flags] + public enum GroupState + { + /// + /// Normal + /// + LVGS_NORMAL = 0x0, + + /// + /// Collapsed + /// + LVGS_COLLAPSED = 0x1, + + /// + /// Hidden + /// + LVGS_HIDDEN = 0x2, + + /// + /// NoHeader + /// + LVGS_NOHEADER = 0x4, + + /// + /// Can be collapsed + /// + LVGS_COLLAPSIBLE = 0x8, + + /// + /// Has focus + /// + LVGS_FOCUSED = 0x10, + + /// + /// Is Selected + /// + LVGS_SELECTED = 0x20, + + /// + /// Is subsetted + /// + LVGS_SUBSETED = 0x40, + + /// + /// Subset link has focus + /// + LVGS_SUBSETLINKFOCUSED = 0x80, + + /// + /// All styles + /// + LVGS_ALL = 0xFFFF + } + + /// + /// This mask indicates which members of a LVGROUP have valid data. These values + /// are taken directly from the SDK and many are not used by ObjectListView. + /// + [Flags] + public enum GroupMask + { + /// + /// No mask + /// + LVGF_NONE = 0, + + /// + /// Group has header + /// + LVGF_HEADER = 1, + + /// + /// Group has footer + /// + LVGF_FOOTER = 2, + + /// + /// Group has state + /// + LVGF_STATE = 4, + + /// + /// + /// + LVGF_ALIGN = 8, + + /// + /// + /// + LVGF_GROUPID = 0x10, + + /// + /// pszSubtitle is valid + /// + LVGF_SUBTITLE = 0x00100, + + /// + /// pszTask is valid + /// + LVGF_TASK = 0x00200, + + /// + /// pszDescriptionTop is valid + /// + LVGF_DESCRIPTIONTOP = 0x00400, + + /// + /// pszDescriptionBottom is valid + /// + LVGF_DESCRIPTIONBOTTOM = 0x00800, + + /// + /// iTitleImage is valid + /// + LVGF_TITLEIMAGE = 0x01000, + + /// + /// iExtendedImage is valid + /// + LVGF_EXTENDEDIMAGE = 0x02000, + + /// + /// iFirstItem and cItems are valid + /// + LVGF_ITEMS = 0x04000, + + /// + /// pszSubsetTitle is valid + /// + LVGF_SUBSET = 0x08000, + + /// + /// readonly, cItems holds count of items in visible subset, iFirstItem is valid + /// + LVGF_SUBSETITEMS = 0x10000 + } + + /// + /// This mask indicates which members of a GROUPMETRICS structure are valid + /// + [Flags] + public enum GroupMetricsMask + { + /// + /// + /// + LVGMF_NONE = 0, + + /// + /// + /// + LVGMF_BORDERSIZE = 1, + + /// + /// + /// + LVGMF_BORDERCOLOR = 2, + + /// + /// + /// + LVGMF_TEXTCOLOR = 4 + } + + /// + /// Instances of this class enhance the capabilities of a normal ListViewGroup, + /// enabling the functionality that was released in v6 of the common controls. + /// + /// + /// + /// In this implementation (2009-09), these objects are essentially passive. + /// Setting properties does not automatically change the associated group in + /// the listview. Collapsed and Collapsible are two exceptions to this and + /// give immediate results. + /// + /// + /// This really should be a subclass of ListViewGroup, but that class is + /// sealed (why is that?). So this class provides the same interface as a + /// ListViewGroup, plus many other new properties. + /// + /// + public class OLVGroup + { + #region Creation + + /// + /// Create an OLVGroup + /// + public OLVGroup() : this("Default group header") { + } + + /// + /// Create a group with the given title + /// + /// Title of the group + public OLVGroup(string header) { + this.Header = header; + this.Id = OLVGroup.nextId++; + this.TitleImage = -1; + this.ExtendedImage = -1; + } + private static int nextId; + + #endregion + + #region Public properties + + /// + /// Gets or sets the bottom description of the group + /// + /// + /// + /// Descriptions only appear when group is centered and there is a title image + /// + /// + /// THIS PROPERTY IS CURRENTLY NOT USED. + /// + /// + public string BottomDescription { + get { return this.bottomDescription; } + set { this.bottomDescription = value; } + } + private string bottomDescription; + + /// + /// Gets or sets whether or not this group is collapsed + /// + public bool Collapsed { + get { return this.GetOneState(GroupState.LVGS_COLLAPSED); } + set { this.SetOneState(value, GroupState.LVGS_COLLAPSED); } + } + + /// + /// Gets or sets whether or not this group can be collapsed + /// + public bool Collapsible { + get { return this.GetOneState(GroupState.LVGS_COLLAPSIBLE); } + set { this.SetOneState(value, GroupState.LVGS_COLLAPSIBLE); } + } + + /// + /// Gets or sets the column that was used to construct this group. + /// + public OLVColumn Column { + get { return this.column; } + set { this.column = value; } + } + private OLVColumn column; + + /// + /// Gets or sets some representation of the contents of this group + /// + /// This is user defined (like Tag) + public IList Contents { + get { return this.contents; } + set { this.contents = value; } + } + private IList contents; + + /// + /// Gets whether this group has been created. + /// + public bool Created { + get { return this.ListView != null; } + } + + /// + /// Gets or sets the int or string that will select the extended image to be shown against the title + /// + public object ExtendedImage { + get { return this.extendedImage; } + set { this.extendedImage = value; } + } + private object extendedImage; + + /// + /// Gets or sets the footer of the group + /// + public string Footer { + get { return this.footer; } + set { this.footer = value; } + } + private string footer; + + /// + /// Gets the internal id of our associated ListViewGroup. + /// + public int GroupId { + get { + if (this.ListViewGroup == null) + return this.Id; + + // Use reflection to get around the access control on the ID property + if (OLVGroup.groupIdPropInfo == null) { + OLVGroup.groupIdPropInfo = typeof(ListViewGroup).GetProperty("ID", + BindingFlags.NonPublic | BindingFlags.Instance); + System.Diagnostics.Debug.Assert(OLVGroup.groupIdPropInfo != null); + } + + int? groupId = OLVGroup.groupIdPropInfo.GetValue(this.ListViewGroup, null) as int?; + return groupId.HasValue ? groupId.Value : -1; + } + } + private static PropertyInfo groupIdPropInfo; + + /// + /// Gets or sets the header of the group + /// + public string Header { + get { return this.header; } + set { this.header = value; } + } + private string header; + + /// + /// Gets or sets the horizontal alignment of the group header + /// + public HorizontalAlignment HeaderAlignment { + get { return this.headerAlignment; } + set { this.headerAlignment = value; } + } + private HorizontalAlignment headerAlignment; + + /// + /// Gets or sets the internally created id of the group + /// + public int Id { + get { return this.id; } + set { this.id = value; } + } + private int id; + + /// + /// Gets or sets ListViewItems that are members of this group + /// + /// Listener of the BeforeCreatingGroups event can populate this collection. + /// It is only used on non-virtual lists. + public IList Items { + get { return this.items; } + set { this.items = value; } + } + private IList items = new List(); + + /// + /// Gets or sets the key that was used to partition objects into this group + /// + /// This is user defined (like Tag) + public object Key { + get { return this.key; } + set { this.key = value; } + } + private object key; + + /// + /// Gets the ObjectListView that this group belongs to + /// + /// If this is null, the group has not yet been created. + public ObjectListView ListView { + get { return this.listView; } + protected set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Gets or sets the name of the group + /// + /// As of 2009-09-01, this property is not used. + public string Name { + get { return this.name; } + set { this.name = value; } + } + private string name; + + /// + /// Gets or sets whether this group is focused + /// + public bool Focused + { + get { return this.GetOneState(GroupState.LVGS_FOCUSED); } + set { this.SetOneState(value, GroupState.LVGS_FOCUSED); } + } + + /// + /// Gets or sets whether this group is selected + /// + public bool Selected + { + get { return this.GetOneState(GroupState.LVGS_SELECTED); } + set { this.SetOneState(value, GroupState.LVGS_SELECTED); } + } + + /// + /// Gets or sets the text that will show that this group is subsetted + /// + /// + /// As of WinSDK v7.0, subsetting of group is officially unimplemented. + /// We can get around this using undocumented interfaces and may do so. + /// + public string SubsetTitle { + get { return this.subsetTitle; } + set { this.subsetTitle = value; } + } + private string subsetTitle; + + /// + /// Gets or set the subtitleof the task + /// + public string Subtitle { + get { return this.subtitle; } + set { this.subtitle = value; } + } + private string subtitle; + + /// + /// Gets or sets the value by which this group will be sorted. + /// + public IComparable SortValue { + get { return this.sortValue; } + set { this.sortValue = value; } + } + private IComparable sortValue; + + /// + /// Gets or sets the state of the group + /// + public GroupState State { + get { return this.state; } + set { this.state = value; } + } + private GroupState state; + + /// + /// Gets or sets which bits of State are valid + /// + public GroupState StateMask { + get { return this.stateMask; } + set { this.stateMask = value; } + } + private GroupState stateMask; + + /// + /// Gets or sets whether this group is showing only a subset of its elements + /// + /// + /// As of WinSDK v7.0, this property officially does nothing. + /// + public bool Subseted { + get { return this.GetOneState(GroupState.LVGS_SUBSETED); } + set { this.SetOneState(value, GroupState.LVGS_SUBSETED); } + } + + /// + /// Gets or sets the user-defined data attached to this group + /// + public object Tag { + get { return this.tag; } + set { this.tag = value; } + } + private object tag; + + /// + /// Gets or sets the task of this group + /// + /// This task is the clickable text that appears on the right margin + /// of the group header. + public string Task { + get { return this.task; } + set { this.task = value; } + } + private string task; + + /// + /// Gets or sets the int or string that will select the image to be shown against the title + /// + public object TitleImage { + get { return this.titleImage; } + set { this.titleImage = value; } + } + private object titleImage; + + /// + /// Gets or sets the top description of the group + /// + /// + /// Descriptions only appear when group is centered and there is a title image + /// + public string TopDescription { + get { return this.topDescription; } + set { this.topDescription = value; } + } + private string topDescription; + + /// + /// Gets or sets the number of items that are within this group. + /// + /// This should only be used for virtual groups. + public int VirtualItemCount { + get { return this.virtualItemCount; } + set { this.virtualItemCount = value; } + } + private int virtualItemCount; + + #endregion + + #region Protected properties + + /// + /// Gets or sets the ListViewGroup that is shadowed by this group. + /// + /// For virtual groups, this will always be null. + protected ListViewGroup ListViewGroup { + get { return this.listViewGroup; } + set { this.listViewGroup = value; } + } + private ListViewGroup listViewGroup; + #endregion + + #region Calculations/Conversions + + /// + /// Calculate the index into the group image list of the given image selector + /// + /// + /// + public int GetImageIndex(object imageSelector) { + if (imageSelector == null || this.ListView == null || this.ListView.GroupImageList == null) + return -1; + + if (imageSelector is Int32) + return (int)imageSelector; + + String imageSelectorAsString = imageSelector as String; + if (imageSelectorAsString != null) + return this.ListView.GroupImageList.Images.IndexOfKey(imageSelectorAsString); + + return -1; + } + + /// + /// Convert this object to a string representation + /// + /// + public override string ToString() { + return this.Header; + } + + #endregion + + #region Commands + + /// + /// Insert a native group into the underlying Windows control, + /// *without* using a ListViewGroup + /// + /// + /// This is used when creating virtual groups + public void InsertGroupNewStyle(ObjectListView olv) { + this.ListView = olv; + NativeMethods.InsertGroup(olv, this.AsNativeGroup(true)); + } + + /// + /// Insert a native group into the underlying control via a ListViewGroup + /// + /// + public void InsertGroupOldStyle(ObjectListView olv) { + this.ListView = olv; + + // Create/update the associated ListViewGroup + if (this.ListViewGroup == null) + this.ListViewGroup = new ListViewGroup(); + this.ListViewGroup.Header = this.Header; + this.ListViewGroup.HeaderAlignment = this.HeaderAlignment; + this.ListViewGroup.Name = this.Name; + + // Remember which OLVGroup created the ListViewGroup + this.ListViewGroup.Tag = this; + + // Add the group to the control + olv.Groups.Add(this.ListViewGroup); + + // Add any extra information + NativeMethods.SetGroupInfo(olv, this.GroupId, this.AsNativeGroup(false)); + } + + /// + /// Change the members of the group to match the current contents of Items, + /// using a ListViewGroup + /// + public void SetItemsOldStyle() { + List list = this.Items as List; + if (list == null) { + foreach (OLVListItem item in this.Items) { + this.ListViewGroup.Items.Add(item); + } + } else { + this.ListViewGroup.Items.AddRange(list.ToArray()); + } + } + + #endregion + + #region Implementation + + /// + /// Create a native LVGROUP structure that matches this group + /// + internal NativeMethods.LVGROUP2 AsNativeGroup(bool withId) { + + NativeMethods.LVGROUP2 group = new NativeMethods.LVGROUP2(); + group.cbSize = (uint)Marshal.SizeOf(typeof(NativeMethods.LVGROUP2)); + group.mask = (uint)(GroupMask.LVGF_HEADER ^ GroupMask.LVGF_ALIGN ^ GroupMask.LVGF_STATE); + group.pszHeader = this.Header; + group.uAlign = (uint)this.HeaderAlignment; + group.stateMask = (uint)this.StateMask; + group.state = (uint)this.State; + + if (withId) { + group.iGroupId = this.GroupId; + group.mask ^= (uint)GroupMask.LVGF_GROUPID; + } + + if (!String.IsNullOrEmpty(this.Footer)) { + group.pszFooter = this.Footer; + group.mask ^= (uint)GroupMask.LVGF_FOOTER; + } + + if (!String.IsNullOrEmpty(this.Subtitle)) { + group.pszSubtitle = this.Subtitle; + group.mask ^= (uint)GroupMask.LVGF_SUBTITLE; + } + + if (!String.IsNullOrEmpty(this.Task)) { + group.pszTask = this.Task; + group.mask ^= (uint)GroupMask.LVGF_TASK; + } + + if (!String.IsNullOrEmpty(this.TopDescription)) { + group.pszDescriptionTop = this.TopDescription; + group.mask ^= (uint)GroupMask.LVGF_DESCRIPTIONTOP; + } + + if (!String.IsNullOrEmpty(this.BottomDescription)) { + group.pszDescriptionBottom = this.BottomDescription; + group.mask ^= (uint)GroupMask.LVGF_DESCRIPTIONBOTTOM; + } + + int imageIndex = this.GetImageIndex(this.TitleImage); + if (imageIndex >= 0) { + group.iTitleImage = imageIndex; + group.mask ^= (uint)GroupMask.LVGF_TITLEIMAGE; + } + + imageIndex = this.GetImageIndex(this.ExtendedImage); + if (imageIndex >= 0) { + group.iExtendedImage = imageIndex; + group.mask ^= (uint)GroupMask.LVGF_EXTENDEDIMAGE; + } + + if (!String.IsNullOrEmpty(this.SubsetTitle)) { + group.pszSubsetTitle = this.SubsetTitle; + group.mask ^= (uint)GroupMask.LVGF_SUBSET; + } + + if (this.VirtualItemCount > 0) { + group.cItems = this.VirtualItemCount; + group.mask ^= (uint)GroupMask.LVGF_ITEMS; + } + + return group; + } + + private bool GetOneState(GroupState mask) { + if (this.Created) + this.State = this.GetState(); + return (this.State & mask) == mask; + } + + /// + /// Get the current state of this group from the underlying control + /// + protected GroupState GetState() { + return NativeMethods.GetGroupState(this.ListView, this.GroupId, GroupState.LVGS_ALL); + } + + /// + /// Get the current state of this group from the underlying control + /// + protected int SetState(GroupState newState, GroupState mask) { + NativeMethods.LVGROUP2 group = new NativeMethods.LVGROUP2(); + group.cbSize = ((uint)Marshal.SizeOf(typeof(NativeMethods.LVGROUP2))); + group.mask = (uint)GroupMask.LVGF_STATE; + group.state = (uint)newState; + group.stateMask = (uint)mask; + return NativeMethods.SetGroupInfo(this.ListView, this.GroupId, group); + } + + private void SetOneState(bool value, GroupState mask) + { + this.StateMask ^= mask; + if (value) + this.State ^= mask; + else + this.State &= ~mask; + + if (this.Created) + this.SetState(this.State, mask); + } + + #endregion + + } +} diff --git a/ObjectListView/Implementation/Munger.cs b/ObjectListView/Implementation/Munger.cs new file mode 100644 index 00000000..8b81aec4 --- /dev/null +++ b/ObjectListView/Implementation/Munger.cs @@ -0,0 +1,568 @@ +/* + * Munger - An Interface pattern on getting and setting values from object through Reflection + * + * Author: Phillip Piper + * Date: 28/11/2008 17:15 + * + * Change log: + * v2.5.1 + * 2012-05-01 JPP - Added IgnoreMissingAspects property + * v2.5 + * 2011-05-20 JPP - Accessing through an indexer when the target had both a integer and + * a string indexer didn't work reliably. + * v2.4.1 + * 2010-08-10 JPP - Refactored into Munger/SimpleMunger. 3x faster! + * v2.3 + * 2009-02-15 JPP - Made Munger a public class + * 2009-01-20 JPP - Made the Munger capable of handling indexed access. + * Incidentally, this removed the ugliness that the last change introduced. + * 2009-01-18 JPP - Handle target objects from a DataListView (normally DataRowViews) + * v2.0 + * 2008-11-28 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace BrightIdeasSoftware +{ + /// + /// An instance of Munger gets a value from or puts a value into a target object. The property + /// to be peeked (or poked) is determined from a string. The peeking or poking is done using reflection. + /// + /// + /// Name of the aspect to be peeked can be a field, property or parameterless method. The name of an + /// aspect to poke can be a field, writable property or single parameter method. + /// + /// Aspect names can be dotted to chain a series of references. + /// + /// Order.Customer.HomeAddress.State + /// + public class Munger + { + #region Life and death + + /// + /// Create a do nothing Munger + /// + public Munger() + { + } + + /// + /// Create a Munger that works on the given aspect name + /// + /// The name of the + public Munger(String aspectName) + { + this.AspectName = aspectName; + } + + #endregion + + #region Static utility methods + + /// + /// A helper method to put the given value into the given aspect of the given object. + /// + /// This method catches and silently ignores any errors that occur + /// while modifying the target object + /// The object to be modified + /// The name of the property/field to be modified + /// The value to be assigned + /// Did the modification work? + public static bool PutProperty(object target, string propertyName, object value) { + try { + Munger munger = new Munger(propertyName); + return munger.PutValue(target, value); + } + catch (MungerException) { + // Not a lot we can do about this. Something went wrong in the bowels + // of the property. Let's take the ostrich approach and just ignore it :-) + + // Normally, we would never just silently ignore an exception. + // However, in this case, this is a utility method that explicitly + // contracts to catch and ignore errors. If this is not acceptable, + // the programmer should not use this method. + } + + return false; + } + + /// + /// Gets or sets whether Mungers will silently ignore missing aspect errors. + /// + /// + /// + /// By default, if a Munger is asked to fetch a field/property/method + /// that does not exist from a model, it returns an error message, since that + /// condition is normally a programming error. There are some use cases where + /// this is not an error, and the munger should simply keep quiet. + /// + /// By default this is true during release builds. + /// + public static bool IgnoreMissingAspects { + get { return ignoreMissingAspects; } + set { ignoreMissingAspects = value; } + } + private static bool ignoreMissingAspects +#if !DEBUG + = true +#endif + ; + + #endregion + + #region Public properties + + /// + /// The name of the aspect that is to be peeked or poked. + /// + /// + /// + /// This name can be a field, property or parameter-less method. + /// + /// + /// The name can be dotted, which chains references. If any link in the chain returns + /// null, the entire chain is considered to return null. + /// + /// + /// "DateOfBirth" + /// "Owner.HomeAddress.Postcode" + public string AspectName + { + get { return aspectName; } + set { + aspectName = value; + + // Clear any cache + aspectParts = null; + } + } + private string aspectName; + + #endregion + + + #region Public interface + + /// + /// Extract the value indicated by our AspectName from the given target. + /// + /// If the aspect name is null or empty, this will return null. + /// The object that will be peeked + /// The value read from the target + public Object GetValue(Object target) { + if (this.Parts.Count == 0) + return null; + + try { + return this.EvaluateParts(target, this.Parts); + } catch (MungerException ex) { + if (Munger.IgnoreMissingAspects) + return null; + + return String.Format("'{0}' is not a parameter-less method, property or field of type '{1}'", + ex.Munger.AspectName, ex.Target.GetType()); + } + } + + /// + /// Extract the value indicated by our AspectName from the given target, raising exceptions + /// if the munger fails. + /// + /// If the aspect name is null or empty, this will return null. + /// The object that will be peeked + /// The value read from the target + public Object GetValueEx(Object target) { + if (this.Parts.Count == 0) + return null; + + return this.EvaluateParts(target, this.Parts); + } + + /// + /// Poke the given value into the given target indicated by our AspectName. + /// + /// + /// + /// If the AspectName is a dotted path, all the selectors bar the last + /// are used to find the object that should be updated, and the last + /// selector is used as the property to update on that object. + /// + /// + /// So, if 'target' is a Person and the AspectName is "HomeAddress.Postcode", + /// this method will first fetch "HomeAddress" property, and then try to set the + /// "Postcode" property on the home address object. + /// + /// + /// The object that will be poked + /// The value that will be poked into the target + /// bool indicating whether the put worked + public bool PutValue(Object target, Object value) + { + if (this.Parts.Count == 0) + return false; + + SimpleMunger lastPart = this.Parts[this.Parts.Count - 1]; + + if (this.Parts.Count > 1) { + List parts = new List(this.Parts); + parts.RemoveAt(parts.Count - 1); + try { + target = this.EvaluateParts(target, parts); + } catch (MungerException ex) { + this.ReportPutValueException(ex); + return false; + } + } + + if (target != null) { + try { + return lastPart.PutValue(target, value); + } catch (MungerException ex) { + this.ReportPutValueException(ex); + } + } + + return false; + } + + #endregion + + #region Implementation + + /// + /// Gets the list of SimpleMungers that match our AspectName + /// + private IList Parts { + get { + if (aspectParts == null) + aspectParts = BuildParts(this.AspectName); + return aspectParts; + } + } + private IList aspectParts; + + /// + /// Convert a possibly dotted AspectName into a list of SimpleMungers + /// + /// + /// + private IList BuildParts(string aspect) { + List parts = new List(); + if (!String.IsNullOrEmpty(aspect)) { + foreach (string part in aspect.Split('.')) { + parts.Add(new SimpleMunger(part.Trim())); + } + } + return parts; + } + + /// + /// Evaluate the given chain of SimpleMungers against an initial target. + /// + /// + /// + /// + private object EvaluateParts(object target, IList parts) { + foreach (SimpleMunger part in parts) { + if (target == null) + break; + target = part.GetValue(target); + } + return target; + } + + private void ReportPutValueException(MungerException ex) { + //TODO: How should we report this error? + System.Diagnostics.Debug.WriteLine("PutValue failed"); + System.Diagnostics.Debug.WriteLine(String.Format("- Culprit aspect: {0}", ex.Munger.AspectName)); + System.Diagnostics.Debug.WriteLine(String.Format("- Target: {0} of type {1}", ex.Target, ex.Target.GetType())); + System.Diagnostics.Debug.WriteLine(String.Format("- Inner exception: {0}", ex.InnerException)); + } + + #endregion + } + + /// + /// A SimpleMunger deals with a single property/field/method on its target. + /// + /// + /// Munger uses a chain of these resolve a dotted aspect name. + /// + public class SimpleMunger + { + #region Life and death + + /// + /// Create a SimpleMunger + /// + /// + public SimpleMunger(String aspectName) + { + this.aspectName = aspectName; + } + + #endregion + + #region Public properties + + /// + /// The name of the aspect that is to be peeked or poked. + /// + /// + /// + /// This name can be a field, property or method. + /// When using a method to get a value, the method must be parameter-less. + /// When using a method to set a value, the method must accept 1 parameter. + /// + /// + /// It cannot be a dotted name. + /// + /// + public string AspectName { + get { return aspectName; } + } + private readonly string aspectName; + + #endregion + + #region Public interface + + /// + /// Get a value from the given target + /// + /// + /// + public Object GetValue(Object target) { + if (target == null) + return null; + + this.ResolveName(target, this.AspectName, 0); + + try { + if (this.resolvedPropertyInfo != null) + return this.resolvedPropertyInfo.GetValue(target, null); + + if (this.resolvedMethodInfo != null) + return this.resolvedMethodInfo.Invoke(target, null); + + if (this.resolvedFieldInfo != null) + return this.resolvedFieldInfo.GetValue(target); + + // If that didn't work, try to use the indexer property. + // This covers things like dictionaries and DataRows. + if (this.indexerPropertyInfo != null) + return this.indexerPropertyInfo.GetValue(target, new object[] { this.AspectName }); + } catch (Exception ex) { + // Lots of things can do wrong in these invocations + throw new MungerException(this, target, ex); + } + + // If we get to here, we couldn't find a match for the aspect + throw new MungerException(this, target, new MissingMethodException()); + } + + /// + /// Poke the given value into the given target indicated by our AspectName. + /// + /// The object that will be poked + /// The value that will be poked into the target + /// bool indicating if the put worked + public bool PutValue(object target, object value) { + if (target == null) + return false; + + this.ResolveName(target, this.AspectName, 1); + + try { + if (this.resolvedPropertyInfo != null) { + this.resolvedPropertyInfo.SetValue(target, value, null); + return true; + } + + if (this.resolvedMethodInfo != null) { + this.resolvedMethodInfo.Invoke(target, new object[] { value }); + return true; + } + + if (this.resolvedFieldInfo != null) { + this.resolvedFieldInfo.SetValue(target, value); + return true; + } + + // If that didn't work, try to use the indexer property. + // This covers things like dictionaries and DataRows. + if (this.indexerPropertyInfo != null) { + this.indexerPropertyInfo.SetValue(target, value, new object[] { this.AspectName }); + return true; + } + } catch (Exception ex) { + // Lots of things can do wrong in these invocations + throw new MungerException(this, target, ex); + } + + return false; + } + + #endregion + + #region Implementation + + private void ResolveName(object target, string name, int numberMethodParameters) { + + if (cachedTargetType == target.GetType() && cachedName == name && cachedNumberParameters == numberMethodParameters) + return; + + cachedTargetType = target.GetType(); + cachedName = name; + cachedNumberParameters = numberMethodParameters; + + resolvedFieldInfo = null; + resolvedPropertyInfo = null; + resolvedMethodInfo = null; + indexerPropertyInfo = null; + + const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance /*| BindingFlags.NonPublic*/; + + foreach (PropertyInfo pinfo in target.GetType().GetProperties(flags)) { + if (pinfo.Name == name) { + resolvedPropertyInfo = pinfo; + return; + } + + // See if we can find an string indexer property while we are here. + // We also need to allow for old style keyed collections. + if (indexerPropertyInfo == null && pinfo.Name == "Item") { + ParameterInfo[] par = pinfo.GetGetMethod().GetParameters(); + if (par.Length > 0) { + Type parameterType = par[0].ParameterType; + if (parameterType == typeof(string) || parameterType == typeof(object)) + indexerPropertyInfo = pinfo; + } + } + } + + foreach (FieldInfo info in target.GetType().GetFields(flags)) { + if (info.Name == name) { + resolvedFieldInfo = info; + return; + } + } + + foreach (MethodInfo info in target.GetType().GetMethods(flags)) { + if (info.Name == name && info.GetParameters().Length == numberMethodParameters) { + resolvedMethodInfo = info; + return; + } + } + } + + private Type cachedTargetType; + private string cachedName; + private int cachedNumberParameters; + + private FieldInfo resolvedFieldInfo; + private PropertyInfo resolvedPropertyInfo; + private MethodInfo resolvedMethodInfo; + private PropertyInfo indexerPropertyInfo; + + #endregion + } + + /// + /// These exceptions are raised when a munger finds something it cannot process + /// + public class MungerException : ApplicationException + { + /// + /// Create a MungerException + /// + /// + /// + /// + public MungerException(SimpleMunger munger, object target, Exception ex) + : base("Munger failed", ex) { + this.munger = munger; + this.target = target; + } + + /// + /// Get the munger that raised the exception + /// + public SimpleMunger Munger { + get { return munger; } + } + private readonly SimpleMunger munger; + + /// + /// Gets the target that threw the exception + /// + public object Target { + get { return target; } + } + private readonly object target; + } + + /* + * We don't currently need this + * 2010-08-06 + * + + internal class SimpleBinder : Binder + { + public override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, System.Globalization.CultureInfo culture) { + //return Type.DefaultBinder.BindToField( + throw new NotImplementedException(); + } + + public override object ChangeType(object value, Type type, System.Globalization.CultureInfo culture) { + throw new NotImplementedException(); + } + + public override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string[] names, out object state) { + throw new NotImplementedException(); + } + + public override void ReorderArgumentArray(ref object[] args, object state) { + throw new NotImplementedException(); + } + + public override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers) { + throw new NotImplementedException(); + } + + public override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers) { + if (match == null) + throw new ArgumentNullException("match"); + + if (match.Length == 0) + return null; + + return match[0]; + } + } + */ + +} diff --git a/ObjectListView/Implementation/NativeMethods.cs b/ObjectListView/Implementation/NativeMethods.cs new file mode 100644 index 00000000..d48588ff --- /dev/null +++ b/ObjectListView/Implementation/NativeMethods.cs @@ -0,0 +1,1223 @@ +/* + * NativeMethods - All the Windows SDK structures and imports + * + * Author: Phillip Piper + * Date: 10/10/2006 + * + * Change log: + * v2.8.0 + * 2014-05-21 JPP - Added DeselectOneItem + * - Added new imagelist drawing + * v2.3 + * 2006-10-10 JPP - Initial version + * + * To do: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// Wrapper for all native method calls on ListView controls + /// + internal static class NativeMethods + { + #region Constants + + private const int LVM_FIRST = 0x1000; + private const int LVM_GETCOLUMN = LVM_FIRST + 95; + private const int LVM_GETCOUNTPERPAGE = LVM_FIRST + 40; + private const int LVM_GETGROUPINFO = LVM_FIRST + 149; + private const int LVM_GETGROUPSTATE = LVM_FIRST + 92; + private const int LVM_GETHEADER = LVM_FIRST + 31; + private const int LVM_GETTOOLTIPS = LVM_FIRST + 78; + private const int LVM_GETTOPINDEX = LVM_FIRST + 39; + private const int LVM_HITTEST = LVM_FIRST + 18; + private const int LVM_INSERTGROUP = LVM_FIRST + 145; + private const int LVM_REMOVEALLGROUPS = LVM_FIRST + 160; + private const int LVM_SCROLL = LVM_FIRST + 20; + private const int LVM_SETBKIMAGE = LVM_FIRST + 0x8A; + private const int LVM_SETCOLUMN = LVM_FIRST + 96; + private const int LVM_SETEXTENDEDLISTVIEWSTYLE = LVM_FIRST + 54; + private const int LVM_SETGROUPINFO = LVM_FIRST + 147; + private const int LVM_SETGROUPMETRICS = LVM_FIRST + 155; + private const int LVM_SETIMAGELIST = LVM_FIRST + 3; + private const int LVM_SETITEM = LVM_FIRST + 76; + private const int LVM_SETITEMCOUNT = LVM_FIRST + 47; + private const int LVM_SETITEMSTATE = LVM_FIRST + 43; + private const int LVM_SETSELECTEDCOLUMN = LVM_FIRST + 140; + private const int LVM_SETTOOLTIPS = LVM_FIRST + 74; + private const int LVM_SUBITEMHITTEST = LVM_FIRST + 57; + private const int LVS_EX_SUBITEMIMAGES = 0x0002; + + private const int LVIF_TEXT = 0x0001; + private const int LVIF_IMAGE = 0x0002; + private const int LVIF_PARAM = 0x0004; + private const int LVIF_STATE = 0x0008; + private const int LVIF_INDENT = 0x0010; + private const int LVIF_NORECOMPUTE = 0x0800; + + private const int LVIS_SELECTED = 2; + + private const int LVCF_FMT = 0x0001; + private const int LVCF_WIDTH = 0x0002; + private const int LVCF_TEXT = 0x0004; + private const int LVCF_SUBITEM = 0x0008; + private const int LVCF_IMAGE = 0x0010; + private const int LVCF_ORDER = 0x0020; + private const int LVCFMT_LEFT = 0x0000; + private const int LVCFMT_RIGHT = 0x0001; + private const int LVCFMT_CENTER = 0x0002; + private const int LVCFMT_JUSTIFYMASK = 0x0003; + + private const int LVCFMT_IMAGE = 0x0800; + private const int LVCFMT_BITMAP_ON_RIGHT = 0x1000; + private const int LVCFMT_COL_HAS_IMAGES = 0x8000; + + private const int LVBKIF_SOURCE_NONE = 0x0; + private const int LVBKIF_SOURCE_HBITMAP = 0x1; + private const int LVBKIF_SOURCE_URL = 0x2; + private const int LVBKIF_SOURCE_MASK = 0x3; + private const int LVBKIF_STYLE_NORMAL = 0x0; + private const int LVBKIF_STYLE_TILE = 0x10; + private const int LVBKIF_STYLE_MASK = 0x10; + private const int LVBKIF_FLAG_TILEOFFSET = 0x100; + private const int LVBKIF_TYPE_WATERMARK = 0x10000000; + private const int LVBKIF_FLAG_ALPHABLEND = 0x20000000; + + private const int LVSICF_NOINVALIDATEALL = 1; + private const int LVSICF_NOSCROLL = 2; + + private const int HDM_FIRST = 0x1200; + private const int HDM_HITTEST = HDM_FIRST + 6; + private const int HDM_GETITEMRECT = HDM_FIRST + 7; + private const int HDM_GETITEM = HDM_FIRST + 11; + private const int HDM_SETITEM = HDM_FIRST + 12; + + private const int HDI_WIDTH = 0x0001; + private const int HDI_TEXT = 0x0002; + private const int HDI_FORMAT = 0x0004; + private const int HDI_BITMAP = 0x0010; + private const int HDI_IMAGE = 0x0020; + + private const int HDF_LEFT = 0x0000; + private const int HDF_RIGHT = 0x0001; + private const int HDF_CENTER = 0x0002; + private const int HDF_JUSTIFYMASK = 0x0003; + private const int HDF_RTLREADING = 0x0004; + private const int HDF_STRING = 0x4000; + private const int HDF_BITMAP = 0x2000; + private const int HDF_BITMAP_ON_RIGHT = 0x1000; + private const int HDF_IMAGE = 0x0800; + private const int HDF_SORTUP = 0x0400; + private const int HDF_SORTDOWN = 0x0200; + + private const int SB_HORZ = 0; + private const int SB_VERT = 1; + private const int SB_CTL = 2; + private const int SB_BOTH = 3; + + private const int SIF_RANGE = 0x0001; + private const int SIF_PAGE = 0x0002; + private const int SIF_POS = 0x0004; + private const int SIF_DISABLENOSCROLL = 0x0008; + private const int SIF_TRACKPOS = 0x0010; + private const int SIF_ALL = (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS); + + private const int ILD_NORMAL = 0x0; + private const int ILD_TRANSPARENT = 0x1; + private const int ILD_MASK = 0x10; + private const int ILD_IMAGE = 0x20; + private const int ILD_BLEND25 = 0x2; + private const int ILD_BLEND50 = 0x4; + + const int SWP_NOSIZE = 1; + const int SWP_NOMOVE = 2; + const int SWP_NOZORDER = 4; + const int SWP_NOREDRAW = 8; + const int SWP_NOACTIVATE = 16; + public const int SWP_FRAMECHANGED = 32; + + const int SWP_ZORDERONLY = SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_NOACTIVATE; + const int SWP_SIZEONLY = SWP_NOMOVE | SWP_NOREDRAW | SWP_NOZORDER | SWP_NOACTIVATE; + const int SWP_UPDATE_FRAME = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOZORDER | SWP_FRAMECHANGED; + + #endregion + + #region Structures + + [StructLayout(LayoutKind.Sequential)] + public struct HDITEM + { + public int mask; + public int cxy; + public IntPtr pszText; + public IntPtr hbm; + public int cchTextMax; + public int fmt; + public IntPtr lParam; + public int iImage; + public int iOrder; + //if (_WIN32_IE >= 0x0500) + public int type; + public IntPtr pvFilter; + } + + [StructLayout(LayoutKind.Sequential)] + public class HDHITTESTINFO + { + public int pt_x; + public int pt_y; + public int flags; + public int iItem; + } + + [StructLayout(LayoutKind.Sequential)] + public class HDLAYOUT + { + public IntPtr prc; + public IntPtr pwpos; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IMAGELISTDRAWPARAMS + { + public int cbSize; + public IntPtr himl; + public int i; + public IntPtr hdcDst; + public int x; + public int y; + public int cx; + public int cy; + public int xBitmap; + public int yBitmap; + public uint rgbBk; + public uint rgbFg; + public uint fStyle; + public uint dwRop; + public uint fState; + public uint Frame; + public uint crEffect; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVBKIMAGE + { + public int ulFlags; + public IntPtr hBmp; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszImage; + public int cchImageMax; + public int xOffset; + public int yOffset; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVCOLUMN + { + public int mask; + public int fmt; + public int cx; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszText; + public int cchTextMax; + public int iSubItem; + // These are available in Common Controls >= 0x0300 + public int iImage; + public int iOrder; + }; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVFINDINFO + { + public int flags; + public string psz; + public IntPtr lParam; + public int ptX; + public int ptY; + public int vkDirection; + } + + [StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct LVGROUP + { + public uint cbSize; + public uint mask; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszHeader; + public int cchHeader; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszFooter; + public int cchFooter; + public int iGroupId; + public uint stateMask; + public uint state; + public uint uAlign; + } + + [StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct LVGROUP2 + { + public uint cbSize; + public uint mask; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszHeader; + public uint cchHeader; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszFooter; + public int cchFooter; + public int iGroupId; + public uint stateMask; + public uint state; + public uint uAlign; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszSubtitle; + public uint cchSubtitle; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszTask; + public uint cchTask; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszDescriptionTop; + public uint cchDescriptionTop; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszDescriptionBottom; + public uint cchDescriptionBottom; + public int iTitleImage; + public int iExtendedImage; + public int iFirstItem; // Read only + public int cItems; // Read only + [MarshalAs(UnmanagedType.LPTStr)] + public string pszSubsetTitle; // NULL if group is not subset + public uint cchSubsetTitle; + } + + [StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct LVGROUPMETRICS + { + public uint cbSize; + public uint mask; + public uint Left; + public uint Top; + public uint Right; + public uint Bottom; + public int crLeft; + public int crTop; + public int crRight; + public int crBottom; + public int crHeader; + public int crFooter; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVHITTESTINFO + { + public int pt_x; + public int pt_y; + public int flags; + public int iItem; + public int iSubItem; + public int iGroup; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVITEM + { + public int mask; + public int iItem; + public int iSubItem; + public int state; + public int stateMask; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszText; + public int cchTextMax; + public int iImage; + public IntPtr lParam; + // These are available in Common Controls >= 0x0300 + public int iIndent; + // These are available in Common Controls >= 0x056 + public int iGroupId; + public int cColumns; + public IntPtr puColumns; + }; + + [StructLayout(LayoutKind.Sequential)] + public struct NMHDR + { + public IntPtr hwndFrom; + public IntPtr idFrom; + public int code; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMCUSTOMDRAW + { + public NativeMethods.NMHDR nmcd; + public int dwDrawStage; + public IntPtr hdc; + public NativeMethods.RECT rc; + public IntPtr dwItemSpec; + public int uItemState; + public IntPtr lItemlParam; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMHEADER + { + public NMHDR nhdr; + public int iItem; + public int iButton; + public IntPtr pHDITEM; + } + + const int MAX_LINKID_TEXT = 48; + const int L_MAX_URL_LENGTH = 2048 + 32 + 4; + //#define L_MAX_URL_LENGTH (2048 + 32 + sizeof("://")) + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct LITEM + { + public uint mask; + public int iLink; + public uint state; + public uint stateMask; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_LINKID_TEXT)] + public string szID; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = L_MAX_URL_LENGTH)] + public string szUrl; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLISTVIEW + { + public NativeMethods.NMHDR hdr; + public int iItem; + public int iSubItem; + public int uNewState; + public int uOldState; + public int uChanged; + public IntPtr lParam; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVCUSTOMDRAW + { + public NativeMethods.NMCUSTOMDRAW nmcd; + public int clrText; + public int clrTextBk; + public int iSubItem; + public int dwItemType; + public int clrFace; + public int iIconEffect; + public int iIconPhase; + public int iPartId; + public int iStateId; + public NativeMethods.RECT rcText; + public uint uAlign; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVFINDITEM + { + public NativeMethods.NMHDR hdr; + public int iStart; + public NativeMethods.LVFINDINFO lvfi; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVGETINFOTIP + { + public NativeMethods.NMHDR hdr; + public int dwFlags; + public string pszText; + public int cchTextMax; + public int iItem; + public int iSubItem; + public IntPtr lParam; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVGROUP + { + public NMHDR hdr; + public int iGroupId; // which group is changing + public uint uNewState; // LVGS_xxx flags + public uint uOldState; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVLINK + { + public NMHDR hdr; + public LITEM link; + public int iItem; + public int iSubItem; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVSCROLL + { + public NativeMethods.NMHDR hdr; + public int dx; + public int dy; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct NMTTDISPINFO + { + public NativeMethods.NMHDR hdr; + [MarshalAs(UnmanagedType.LPTStr)] + public string lpszText; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] + public string szText; + public IntPtr hinst; + public int uFlags; + public IntPtr lParam; + //public int hbmp; This is documented but doesn't work + } + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + [StructLayout(LayoutKind.Sequential)] + public class SCROLLINFO + { + public int cbSize = Marshal.SizeOf(typeof(NativeMethods.SCROLLINFO)); + public int fMask; + public int nMin; + public int nMax; + public int nPage; + public int nPos; + public int nTrackPos; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class TOOLINFO + { + public int cbSize = Marshal.SizeOf(typeof(NativeMethods.TOOLINFO)); + public int uFlags; + public IntPtr hwnd; + public IntPtr uId; + public NativeMethods.RECT rect; + public IntPtr hinst = IntPtr.Zero; + public IntPtr lpszText; + public IntPtr lParam = IntPtr.Zero; + } + + [StructLayout(LayoutKind.Sequential)] + public struct WINDOWPOS + { + public IntPtr hwnd; + public IntPtr hwndInsertAfter; + public int x; + public int y; + public int cx; + public int cy; + public int flags; + } + + #endregion + + #region Entry points + + // Various flavours of SendMessage: plain vanilla, and passing references to various structures + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, int lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageLVItem(IntPtr hWnd, int msg, int wParam, ref LVITEM lvi); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVHITTESTINFO ht); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageRECT(IntPtr hWnd, int msg, int wParam, ref RECT r); + //[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + //private static extern IntPtr SendMessageLVColumn(IntPtr hWnd, int m, int wParam, ref LVCOLUMN lvc); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + private static extern IntPtr SendMessageHDItem(IntPtr hWnd, int msg, int wParam, ref HDITEM hdi); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageHDHITTESTINFO(IntPtr hWnd, int Msg, IntPtr wParam, [In, Out] HDHITTESTINFO lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageTOOLINFO(IntPtr hWnd, int Msg, int wParam, NativeMethods.TOOLINFO lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageLVBKIMAGE(IntPtr hWnd, int Msg, int wParam, ref NativeMethods.LVBKIMAGE lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageString(IntPtr hWnd, int Msg, int wParam, string lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageIUnknown(IntPtr hWnd, int msg, [MarshalAs(UnmanagedType.IUnknown)] object wParam, int lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVGROUP lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVGROUP2 lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVGROUPMETRICS lParam); + + [DllImport("gdi32.dll")] + public static extern bool DeleteObject(IntPtr objectHandle); + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern bool GetClientRect(IntPtr hWnd, ref Rectangle r); + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern bool GetScrollInfo(IntPtr hWnd, int fnBar, SCROLLINFO scrollInfo); + + [DllImport("user32.dll", EntryPoint = "GetUpdateRect", CharSet = CharSet.Auto)] + private static extern bool GetUpdateRectInternal(IntPtr hWnd, ref Rectangle r, bool eraseBackground); + + [DllImport("comctl32.dll", CharSet = CharSet.Auto)] + private static extern bool ImageList_Draw(IntPtr himl, int i, IntPtr hdcDst, int x, int y, int fStyle); + + [DllImport("comctl32.dll", CharSet = CharSet.Auto)] + private static extern bool ImageList_DrawIndirect(ref IMAGELISTDRAWPARAMS parms); + + [DllImport("user32.dll")] + public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern bool GetWindowRect(IntPtr hWnd, ref Rectangle r); + + [DllImport("user32.dll", EntryPoint = "GetWindowLong", CharSet = CharSet.Auto)] + public static extern IntPtr GetWindowLong32(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr", CharSet = CharSet.Auto)] + public static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Auto)] + public static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Auto)] + public static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll", EntryPoint = "ValidateRect", CharSet = CharSet.Auto)] + private static extern IntPtr ValidatedRectInternal(IntPtr hWnd, ref Rectangle r); + + #endregion + + //[DllImport("user32.dll", EntryPoint = "LockWindowUpdate", CharSet = CharSet.Auto)] + //private static extern int LockWindowUpdateInternal(IntPtr hWnd); + + //public static void LockWindowUpdate(IWin32Window window) { + // if (window == null) + // NativeMethods.LockWindowUpdateInternal(IntPtr.Zero); + // else + // NativeMethods.LockWindowUpdateInternal(window.Handle); + //} + + /// + /// Put an image under the ListView. + /// + /// + /// + /// The ListView must have its handle created before calling this. + /// + /// + /// This doesn't work very well. Specifically, it doesn't play well with owner drawn, + /// and grid lines are drawn over it. + /// + /// + /// + /// The image to be used as the background. If this is null, any existing background image will be cleared. + /// If this is true, the image is pinned to the bottom right and does not scroll. The other parameters are ignored + /// If this is true, the image will be tiled to fill the whole control background. The offset parameters will be ignored. + /// If both watermark and tiled are false, this indicates the horizontal percentage where the image will be placed. 0 is absolute left, 100 is absolute right. + /// If both watermark and tiled are false, this indicates the vertical percentage where the image will be placed. + /// + public static bool SetBackgroundImage(ListView lv, Image image, bool isWatermark, bool isTiled, int xOffset, int yOffset) { + + LVBKIMAGE lvbkimage = new LVBKIMAGE(); + + // We have to clear any pre-existing background image, otherwise the attempt to set the image will fail. + // We don't know which type may already have been set, so we just clear both the watermark and the image. + lvbkimage.ulFlags = LVBKIF_TYPE_WATERMARK; + IntPtr result = NativeMethods.SendMessageLVBKIMAGE(lv.Handle, LVM_SETBKIMAGE, 0, ref lvbkimage); + lvbkimage.ulFlags = LVBKIF_SOURCE_HBITMAP; + result = NativeMethods.SendMessageLVBKIMAGE(lv.Handle, LVM_SETBKIMAGE, 0, ref lvbkimage); + + Bitmap bm = image as Bitmap; + if (bm != null) { + lvbkimage.hBmp = bm.GetHbitmap(); + lvbkimage.ulFlags = isWatermark ? LVBKIF_TYPE_WATERMARK : (isTiled ? LVBKIF_SOURCE_HBITMAP | LVBKIF_STYLE_TILE : LVBKIF_SOURCE_HBITMAP); + lvbkimage.xOffset = xOffset; + lvbkimage.yOffset = yOffset; + result = NativeMethods.SendMessageLVBKIMAGE(lv.Handle, LVM_SETBKIMAGE, 0, ref lvbkimage); + } + + return (result != IntPtr.Zero); + } + + public static bool DrawImageList(Graphics g, ImageList il, int index, int x, int y, bool isSelected, bool isDisabled) { + ImageListDrawItemConstants flags = (isSelected ? ImageListDrawItemConstants.ILD_SELECTED : ImageListDrawItemConstants.ILD_NORMAL) | ImageListDrawItemConstants.ILD_TRANSPARENT; + ImageListDrawStateConstants state = isDisabled ? ImageListDrawStateConstants.ILS_SATURATE : ImageListDrawStateConstants.ILS_NORMAL; + try { + IntPtr hdc = g.GetHdc(); + return DrawImage(il, hdc, index, x, y, flags, 0, 0, state); + } + finally { + g.ReleaseHdc(); + } + } + + /// + /// Flags controlling how the Image List item is + /// drawn + /// + [Flags] + public enum ImageListDrawItemConstants + { + /// + /// Draw item normally. + /// + ILD_NORMAL = 0x0, + /// + /// Draw item transparently. + /// + ILD_TRANSPARENT = 0x1, + /// + /// Draw item blended with 25% of the specified foreground colour + /// or the Highlight colour if no foreground colour specified. + /// + ILD_BLEND25 = 0x2, + /// + /// Draw item blended with 50% of the specified foreground colour + /// or the Highlight colour if no foreground colour specified. + /// + ILD_SELECTED = 0x4, + /// + /// Draw the icon's mask + /// + ILD_MASK = 0x10, + /// + /// Draw the icon image without using the mask + /// + ILD_IMAGE = 0x20, + /// + /// Draw the icon using the ROP specified. + /// + ILD_ROP = 0x40, + /// + /// Preserves the alpha channel in dest. XP only. + /// + ILD_PRESERVEALPHA = 0x1000, + /// + /// Scale the image to cx, cy instead of clipping it. XP only. + /// + ILD_SCALE = 0x2000, + /// + /// Scale the image to the current DPI of the display. XP only. + /// + ILD_DPISCALE = 0x4000 + } + + /// + /// Enumeration containing XP ImageList Draw State options + /// + [Flags] + public enum ImageListDrawStateConstants + { + /// + /// The image state is not modified. + /// + ILS_NORMAL = (0x00000000), + /// + /// Adds a glow effect to the icon, which causes the icon to appear to glow + /// with a given color around the edges. (Note: does not appear to be implemented) + /// + ILS_GLOW = (0x00000001), //The color for the glow effect is passed to the IImageList::Draw method in the crEffect member of IMAGELISTDRAWPARAMS. + /// + /// Adds a drop shadow effect to the icon. (Note: does not appear to be implemented) + /// + ILS_SHADOW = (0x00000002), //The color for the drop shadow effect is passed to the IImageList::Draw method in the crEffect member of IMAGELISTDRAWPARAMS. + /// + /// Saturates the icon by increasing each color component + /// of the RGB triplet for each pixel in the icon. (Note: only ever appears to result in a completely unsaturated icon) + /// + ILS_SATURATE = (0x00000004), // The amount to increase is indicated by the frame member in the IMAGELISTDRAWPARAMS method. + /// + /// Alpha blends the icon. Alpha blending controls the transparency + /// level of an icon, according to the value of its alpha channel. + /// (Note: does not appear to be implemented). + /// + ILS_ALPHA = (0x00000008) //The value of the alpha channel is indicated by the frame member in the IMAGELISTDRAWPARAMS method. The alpha channel can be from 0 to 255, with 0 being completely transparent, and 255 being completely opaque. + } + + private const uint CLR_DEFAULT = 0xFF000000; + + /// + /// Draws an image using the specified flags and state on XP systems. + /// + /// The image list from which an item will be drawn + /// Device context to draw to + /// Index of image to draw + /// X Position to draw at + /// Y Position to draw at + /// Drawing flags + /// Width to draw + /// Height to draw + /// State flags + public static bool DrawImage(ImageList il, IntPtr hdc, int index, int x, int y, ImageListDrawItemConstants flags, int cx, int cy, ImageListDrawStateConstants stateFlags) { + IMAGELISTDRAWPARAMS pimldp = new IMAGELISTDRAWPARAMS(); + pimldp.hdcDst = hdc; + pimldp.cbSize = Marshal.SizeOf(pimldp.GetType()); + pimldp.i = index; + pimldp.x = x; + pimldp.y = y; + pimldp.cx = cx; + pimldp.cy = cy; + pimldp.rgbFg = CLR_DEFAULT; + pimldp.fStyle = (uint) flags; + pimldp.fState = (uint) stateFlags; + pimldp.himl = il.Handle; + return ImageList_DrawIndirect(ref pimldp); + } + + /// + /// Make sure the ListView has the extended style that says to display subitem images. + /// + /// This method must be called after any .NET call that update the extended styles + /// since they seem to erase this setting. + /// The listview to send a m to + public static void ForceSubItemImagesExStyle(ListView list) { + SendMessage(list.Handle, LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_SUBITEMIMAGES, LVS_EX_SUBITEMIMAGES); + } + + /// + /// Change the virtual list size of the given ListView (which must be in virtual mode) + /// + /// This will not change the scroll position + /// The listview to send a message to + /// How many rows should the list have? + public static void SetItemCount(ListView list, int count) { + SendMessage(list.Handle, LVM_SETITEMCOUNT, count, LVSICF_NOSCROLL); + } + + /// + /// Make sure the ListView has the extended style that says to display subitem images. + /// + /// This method must be called after any .NET call that update the extended styles + /// since they seem to erase this setting. + /// The listview to send a m to + /// + /// + public static void SetExtendedStyle(ListView list, int style, int styleMask) { + SendMessage(list.Handle, LVM_SETEXTENDEDLISTVIEWSTYLE, styleMask, style); + } + + /// + /// Calculates the number of items that can fit vertically in the visible area of a list-view (which + /// must be in details or list view. + /// + /// The listView + /// Number of visible items per page + public static int GetCountPerPage(ListView list) { + return (int)SendMessage(list.Handle, LVM_GETCOUNTPERPAGE, 0, 0); + } + /// + /// For the given item and subitem, make it display the given image + /// + /// The listview to send a m to + /// row number (0 based) + /// subitem (0 is the item itself) + /// index into the image list + public static void SetSubItemImage(ListView list, int itemIndex, int subItemIndex, int imageIndex) { + LVITEM lvItem = new LVITEM(); + lvItem.mask = LVIF_IMAGE; + lvItem.iItem = itemIndex; + lvItem.iSubItem = subItemIndex; + lvItem.iImage = imageIndex; + SendMessageLVItem(list.Handle, LVM_SETITEM, 0, ref lvItem); + } + + /// + /// Setup the given column of the listview to show the given image to the right of the text. + /// If the image index is -1, any previous image is cleared + /// + /// The listview to send a m to + /// Index of the column to modify + /// + /// Index into the small image list + public static void SetColumnImage(ListView list, int columnIndex, SortOrder order, int imageIndex) { + IntPtr hdrCntl = NativeMethods.GetHeaderControl(list); + if (hdrCntl.ToInt32() == 0) + return; + + HDITEM item = new HDITEM(); + item.mask = HDI_FORMAT; + IntPtr result = SendMessageHDItem(hdrCntl, HDM_GETITEM, columnIndex, ref item); + + item.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN | HDF_IMAGE | HDF_BITMAP_ON_RIGHT); + + if (NativeMethods.HasBuiltinSortIndicators()) { + if (order == SortOrder.Ascending) + item.fmt |= HDF_SORTUP; + if (order == SortOrder.Descending) + item.fmt |= HDF_SORTDOWN; + } else { + item.mask |= HDI_IMAGE; + item.fmt |= (HDF_IMAGE | HDF_BITMAP_ON_RIGHT); + item.iImage = imageIndex; + } + + result = SendMessageHDItem(hdrCntl, HDM_SETITEM, columnIndex, ref item); + } + + /// + /// Does this version of the operating system have builtin sort indicators? + /// + /// Are there builtin sort indicators + /// XP and later have these + public static bool HasBuiltinSortIndicators() { + return OSFeature.Feature.GetVersionPresent(OSFeature.Themes) != null; + } + + /// + /// Return the bounds of the update region on the given control. + /// + /// The BeginPaint() system call validates the update region, effectively wiping out this information. + /// So this call has to be made before the BeginPaint() call. + /// The control whose update region is be calculated + /// A rectangle + public static Rectangle GetUpdateRect(Control cntl) { + Rectangle r = new Rectangle(); + GetUpdateRectInternal(cntl.Handle, ref r, false); + return r; + } + + /// + /// Validate an area of the given control. A validated area will not be repainted at the next redraw. + /// + /// The control to be validated + /// The area of the control to be validated + public static void ValidateRect(Control cntl, Rectangle r) { + ValidatedRectInternal(cntl.Handle, ref r); + } + + /// + /// Select all rows on the given listview + /// + /// The listview whose items are to be selected + public static void SelectAllItems(ListView list) { + NativeMethods.SetItemState(list, -1, LVIS_SELECTED, LVIS_SELECTED); + } + + /// + /// Deselect all rows on the given listview + /// + /// The listview whose items are to be deselected + public static void DeselectAllItems(ListView list) { + NativeMethods.SetItemState(list, -1, LVIS_SELECTED, 0); + } + + /// + /// Deselect a single row + /// + /// + /// + public static void DeselectOneItem(ListView list, int index) { + NativeMethods.SetItemState(list, index, LVIS_SELECTED, 0); + } + + /// + /// Set the item state on the given item + /// + /// The listview whose item's state is to be changed + /// The index of the item to be changed + /// Which bits of the value are to be set? + /// The value to be set + public static void SetItemState(ListView list, int itemIndex, int mask, int value) { + LVITEM lvItem = new LVITEM(); + lvItem.stateMask = mask; + lvItem.state = value; + SendMessageLVItem(list.Handle, LVM_SETITEMSTATE, itemIndex, ref lvItem); + } + + /// + /// Scroll the given listview by the given deltas + /// + /// + /// + /// + /// true if the scroll succeeded + public static bool Scroll(ListView list, int dx, int dy) { + return SendMessage(list.Handle, LVM_SCROLL, dx, dy) != IntPtr.Zero; + } + + /// + /// Return the handle to the header control on the given list + /// + /// The listview whose header control is to be returned + /// The handle to the header control + public static IntPtr GetHeaderControl(ListView list) { + return SendMessage(list.Handle, LVM_GETHEADER, 0, 0); + } + + /// + /// Return the edges of the given column. + /// + /// + /// + /// A Point holding the left and right co-ords of the column. + /// -1 means that the sides could not be retrieved. + public static Point GetColumnSides(ObjectListView lv, int columnIndex) { + Point sides = new Point(-1, -1); + IntPtr hdr = NativeMethods.GetHeaderControl(lv); + if (hdr == IntPtr.Zero) + return new Point(-1, -1); + + RECT r = new RECT(); + NativeMethods.SendMessageRECT(hdr, HDM_GETITEMRECT, columnIndex, ref r); + return new Point(r.left, r.right); + } + + /// + /// Return the edges of the given column. + /// + /// + /// + /// A Point holding the left and right co-ords of the column. + /// -1 means that the sides could not be retrieved. + public static Point GetScrolledColumnSides(ListView lv, int columnIndex) { + IntPtr hdr = NativeMethods.GetHeaderControl(lv); + if (hdr == IntPtr.Zero) + return new Point(-1, -1); + + RECT r = new RECT(); + IntPtr result = NativeMethods.SendMessageRECT(hdr, HDM_GETITEMRECT, columnIndex, ref r); + int scrollH = NativeMethods.GetScrollPosition(lv, true); + return new Point(r.left - scrollH, r.right - scrollH); + } + + /// + /// Return the index of the column of the header that is under the given point. + /// Return -1 if no column is under the pt + /// + /// The list we are interested in + /// The client co-ords + /// The index of the column under the point, or -1 if no column header is under that point + public static int GetColumnUnderPoint(IntPtr handle, Point pt) { + const int HHT_ONHEADER = 2; + const int HHT_ONDIVIDER = 4; + return NativeMethods.HeaderControlHitTest(handle, pt, HHT_ONHEADER | HHT_ONDIVIDER); + } + + private static int HeaderControlHitTest(IntPtr handle, Point pt, int flag) { + HDHITTESTINFO testInfo = new HDHITTESTINFO(); + testInfo.pt_x = pt.X; + testInfo.pt_y = pt.Y; + IntPtr result = NativeMethods.SendMessageHDHITTESTINFO(handle, HDM_HITTEST, IntPtr.Zero, testInfo); + if ((testInfo.flags & flag) != 0) + return testInfo.iItem; + else + return -1; + } + + /// + /// Return the index of the divider under the given point. Return -1 if no divider is under the pt + /// + /// The list we are interested in + /// The client co-ords + /// The index of the divider under the point, or -1 if no divider is under that point + public static int GetDividerUnderPoint(IntPtr handle, Point pt) { + const int HHT_ONDIVIDER = 4; + return NativeMethods.HeaderControlHitTest(handle, pt, HHT_ONDIVIDER); + } + + /// + /// Get the scroll position of the given scroll bar + /// + /// + /// + /// + public static int GetScrollPosition(ListView lv, bool horizontalBar) { + int fnBar = (horizontalBar ? SB_HORZ : SB_VERT); + + SCROLLINFO scrollInfo = new SCROLLINFO(); + scrollInfo.fMask = SIF_POS; + if (GetScrollInfo(lv.Handle, fnBar, scrollInfo)) + return scrollInfo.nPos; + else + return -1; + } + + /// + /// Change the z-order to the window 'toBeMoved' so it appear directly on top of 'reference' + /// + /// + /// + /// + public static bool ChangeZOrder(IWin32Window toBeMoved, IWin32Window reference) { + return NativeMethods.SetWindowPos(toBeMoved.Handle, reference.Handle, 0, 0, 0, 0, SWP_ZORDERONLY); + } + + /// + /// Make the given control/window a topmost window + /// + /// + /// + public static bool MakeTopMost(IWin32Window toBeMoved) { + IntPtr HWND_TOPMOST = (IntPtr)(-1); + return NativeMethods.SetWindowPos(toBeMoved.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_ZORDERONLY); + } + + /// + /// Change the size of the window without affecting any other attributes + /// + /// + /// + /// + /// + public static bool ChangeSize(IWin32Window toBeMoved, int width, int height) { + return NativeMethods.SetWindowPos(toBeMoved.Handle, IntPtr.Zero, 0, 0, width, height, SWP_SIZEONLY); + } + + /// + /// Show the given window without activating it + /// + /// The window to show + static public void ShowWithoutActivate(IWin32Window win) { + const int SW_SHOWNA = 8; + NativeMethods.ShowWindow(win.Handle, SW_SHOWNA); + } + + /// + /// Mark the given column as being selected. + /// + /// + /// The OLVColumn or null to clear + /// + /// This method works, but it prevents subitems in the given column from having + /// back colors. + /// + static public void SetSelectedColumn(ListView objectListView, ColumnHeader value) { + NativeMethods.SendMessage(objectListView.Handle, + LVM_SETSELECTEDCOLUMN, (value == null) ? -1 : value.Index, 0); + } + + static public int GetTopIndex(ListView lv) { + return (int)SendMessage(lv.Handle, LVM_GETTOPINDEX, 0, 0); + } + + static public IntPtr GetTooltipControl(ListView lv) { + return SendMessage(lv.Handle, LVM_GETTOOLTIPS, 0, 0); + } + + static public IntPtr SetTooltipControl(ListView lv, ToolTipControl tooltip) { + return SendMessage(lv.Handle, LVM_SETTOOLTIPS, 0, tooltip.Handle); + } + + static public bool HasHorizontalScrollBar(ListView lv) { + const int GWL_STYLE = -16; + const int WS_HSCROLL = 0x00100000; + + return (NativeMethods.GetWindowLong(lv.Handle, GWL_STYLE) & WS_HSCROLL) != 0; + } + + public static int GetWindowLong(IntPtr hWnd, int nIndex) { + if (IntPtr.Size == 4) + return (int)GetWindowLong32(hWnd, nIndex); + else + return (int)(long)GetWindowLongPtr64(hWnd, nIndex); + } + + public static int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong) { + if (IntPtr.Size == 4) + return (int)SetWindowLongPtr32(hWnd, nIndex, dwNewLong); + else + return (int)(long)SetWindowLongPtr64(hWnd, nIndex, dwNewLong); + } + + [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + public static extern IntPtr SetBkColor(IntPtr hDC, int clr); + + [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + public static extern IntPtr SetTextColor(IntPtr hDC, int crColor); + + [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + public static extern IntPtr SelectObject(IntPtr hdc, IntPtr obj); + + [DllImport("uxtheme.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + public static extern IntPtr SetWindowTheme(IntPtr hWnd, string subApp, string subIdList); + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern bool InvalidateRect(IntPtr hWnd, int ignored, bool erase); + + [StructLayout(LayoutKind.Sequential)] + public struct LVITEMINDEX + { + public int iItem; + public int iGroup; + } + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int x; + public int y; + } + + public static int GetGroupInfo(ObjectListView olv, int groupId, ref LVGROUP2 group) { + return (int)NativeMethods.SendMessage(olv.Handle, LVM_GETGROUPINFO, groupId, ref group); + } + + public static GroupState GetGroupState(ObjectListView olv, int groupId, GroupState mask) { + return (GroupState)NativeMethods.SendMessage(olv.Handle, LVM_GETGROUPSTATE, groupId, (int)mask); + } + + public static int InsertGroup(ObjectListView olv, LVGROUP2 group) { + return (int)NativeMethods.SendMessage(olv.Handle, LVM_INSERTGROUP, -1, ref group); + } + + public static int SetGroupInfo(ObjectListView olv, int groupId, LVGROUP2 group) { + return (int)NativeMethods.SendMessage(olv.Handle, LVM_SETGROUPINFO, groupId, ref group); + } + + public static int SetGroupMetrics(ObjectListView olv, LVGROUPMETRICS metrics) { + return (int)NativeMethods.SendMessage(olv.Handle, LVM_SETGROUPMETRICS, 0, ref metrics); + } + + public static void ClearGroups(VirtualObjectListView virtualObjectListView) { + NativeMethods.SendMessage(virtualObjectListView.Handle, LVM_REMOVEALLGROUPS, 0, 0); + } + + public static void SetGroupImageList(ObjectListView olv, ImageList il) { + const int LVSIL_GROUPHEADER = 3; + NativeMethods.SendMessage(olv.Handle, LVM_SETIMAGELIST, LVSIL_GROUPHEADER, il == null ? IntPtr.Zero : il.Handle); + } + + public static int HitTest(ObjectListView olv, ref LVHITTESTINFO hittest) + { + return (int)NativeMethods.SendMessage(olv.Handle, olv.View == View.Details ? LVM_SUBITEMHITTEST : LVM_HITTEST, -1, ref hittest); + } + } +} diff --git a/ObjectListView/Implementation/NullableDictionary.cs b/ObjectListView/Implementation/NullableDictionary.cs new file mode 100644 index 00000000..79b3c1ec --- /dev/null +++ b/ObjectListView/Implementation/NullableDictionary.cs @@ -0,0 +1,87 @@ +/* + * NullableDictionary - A simple Dictionary that can handle null as a key + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2017 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; + +namespace BrightIdeasSoftware { + + /// + /// A simple-minded implementation of a Dictionary that can handle null as a key. + /// + /// The type of the dictionary key + /// The type of the values to be stored + /// This is not a full implementation and is only meant to handle + /// collecting groups by their keys, since groups can have null as a key value. + internal class NullableDictionary : Dictionary { + private bool hasNullKey; + private TValue nullValue; + + new public TValue this[TKey key] { + get { + if (key != null) + return base[key]; + + if (this.hasNullKey) + return this.nullValue; + + throw new KeyNotFoundException(); + } + set { + if (key == null) { + this.hasNullKey = true; + this.nullValue = value; + } else + base[key] = value; + } + } + + new public bool ContainsKey(TKey key) { + return key == null ? this.hasNullKey : base.ContainsKey(key); + } + + new public IList Keys { + get { + ArrayList list = new ArrayList(base.Keys); + if (this.hasNullKey) + list.Add(null); + return list; + } + } + + new public IList Values { + get { + List list = new List(base.Values); + if (this.hasNullKey) + list.Add(this.nullValue); + return list; + } + } + } +} diff --git a/ObjectListView/Implementation/OLVListItem.cs b/ObjectListView/Implementation/OLVListItem.cs new file mode 100644 index 00000000..d4881232 --- /dev/null +++ b/ObjectListView/Implementation/OLVListItem.cs @@ -0,0 +1,325 @@ +/* + * OLVListItem - A row in an ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2018-09-01 JPP - Handle rare case of getting subitems when there are no columns + * v2.9 + * 2015-08-22 JPP - Added OLVListItem.SelectedBackColor and SelectedForeColor + * 2015-06-09 JPP - Added HasAnyHyperlinks property + * v2.8 + * 2014-09-27 JPP - Remove faulty caching of CheckState + * 2014-05-06 JPP - Added OLVListItem.Enabled flag + * vOld + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2018 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Windows.Forms; +using System.Drawing; + +namespace BrightIdeasSoftware { + + /// + /// OLVListItems are specialized ListViewItems that know which row object they came from, + /// and the row index at which they are displayed, even when in group view mode. They + /// also know the image they should draw against themselves + /// + public class OLVListItem : ListViewItem { + #region Constructors + + /// + /// Create a OLVListItem for the given row object + /// + public OLVListItem(object rowObject) { + this.rowObject = rowObject; + } + + /// + /// Create a OLVListItem for the given row object, represented by the given string and image + /// + public OLVListItem(object rowObject, string text, Object image) + : base(text, -1) { + this.rowObject = rowObject; + this.imageSelector = image; + } + + #endregion. + + #region Properties + + /// + /// Gets the bounding rectangle of the item, including all subitems + /// + new public Rectangle Bounds { + get { + try { + return base.Bounds; + } + catch (System.ArgumentException) { + // If the item is part of a collapsed group, Bounds will throw an exception + return Rectangle.Empty; + } + } + } + + /// + /// Gets or sets how many pixels will be left blank around each cell of this item + /// + /// This setting only takes effect when the control is owner drawn. + public Rectangle? CellPadding { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets or sets how the cells of this item will be vertically aligned + /// + /// This setting only takes effect when the control is owner drawn. + public StringAlignment? CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment? cellVerticalAlignment; + + /// + /// Gets or sets the checkedness of this item. + /// + /// + /// Virtual lists don't handle checkboxes well, so we have to intercept attempts to change them + /// through the items, and change them into something that will work. + /// Unfortunately, this won't work if this property is set through the base class, since + /// the property is not declared as virtual. + /// + new public bool Checked { + get { + return base.Checked; + } + set { + if (this.Checked != value) { + if (value) + ((ObjectListView)this.ListView).CheckObject(this.RowObject); + else + ((ObjectListView)this.ListView).UncheckObject(this.RowObject); + } + } + } + + /// + /// Enable tri-state checkbox. + /// + /// .NET's Checked property was not built to handle tri-state checkboxes, + /// and will return True for both Checked and Indeterminate states. + public CheckState CheckState { + get { + switch (this.StateImageIndex) { + case 0: + return System.Windows.Forms.CheckState.Unchecked; + case 1: + return System.Windows.Forms.CheckState.Checked; + case 2: + return System.Windows.Forms.CheckState.Indeterminate; + default: + return System.Windows.Forms.CheckState.Unchecked; + } + } + set { + switch (value) { + case System.Windows.Forms.CheckState.Unchecked: + this.StateImageIndex = 0; + break; + case System.Windows.Forms.CheckState.Checked: + this.StateImageIndex = 1; + break; + case System.Windows.Forms.CheckState.Indeterminate: + this.StateImageIndex = 2; + break; + } + } + } + + /// + /// Gets if this item has any decorations set for it. + /// + public bool HasDecoration { + get { + return this.decorations != null && this.decorations.Count > 0; + } + } + + /// + /// Gets or sets the decoration that will be drawn over this item + /// + /// Setting this replaces all other decorations + public IDecoration Decoration { + get { + if (this.HasDecoration) + return this.Decorations[0]; + else + return null; + } + set { + this.Decorations.Clear(); + if (value != null) + this.Decorations.Add(value); + } + } + + /// + /// Gets the collection of decorations that will be drawn over this item + /// + public IList Decorations { + get { + if (this.decorations == null) + this.decorations = new List(); + return this.decorations; + } + } + private IList decorations; + + /// + /// Gets whether or not this row can be selected and activated + /// + public bool Enabled + { + get { return this.enabled; } + internal set { this.enabled = value; } + } + private bool enabled; + + /// + /// Gets whether any cell on this item is showing a hyperlink + /// + public bool HasAnyHyperlinks { + get { + foreach (OLVListSubItem subItem in this.SubItems) { + if (!String.IsNullOrEmpty(subItem.Url)) + return true; + } + return false; + } + } + + /// + /// Get or set the image that should be shown against this item + /// + /// This can be an Image, a string or an int. A string or an int will + /// be used as an index into the small image list. + public Object ImageSelector { + get { return imageSelector; } + set { + imageSelector = value; + if (value is Int32) + this.ImageIndex = (Int32)value; + else if (value is String) + this.ImageKey = (String)value; + else + this.ImageIndex = -1; + } + } + private Object imageSelector; + + /// + /// Gets or sets the model object that is source of the data for this list item. + /// + public object RowObject { + get { return rowObject; } + set { rowObject = value; } + } + private object rowObject; + + /// + /// Gets or sets the color that will be used for this row's background when it is selected and + /// the control is focused. + /// + /// + /// To work reliably, this property must be set during a FormatRow event. + /// + /// If this is not set, the normal selection BackColor will be used. + /// + /// + public Color? SelectedBackColor { + get { return this.selectedBackColor; } + set { this.selectedBackColor = value; } + } + private Color? selectedBackColor; + + /// + /// Gets or sets the color that will be used for this row's foreground when it is selected and + /// the control is focused. + /// + /// + /// To work reliably, this property must be set during a FormatRow event. + /// + /// If this is not set, the normal selection ForeColor will be used. + /// + /// + public Color? SelectedForeColor + { + get { return this.selectedForeColor; } + set { this.selectedForeColor = value; } + } + private Color? selectedForeColor; + + #endregion + + #region Accessing + + /// + /// Return the sub item at the given index + /// + /// Index of the subitem to be returned + /// An OLVListSubItem + public virtual OLVListSubItem GetSubItem(int index) { + if (index >= 0 && index < this.SubItems.Count) + // If the control has 0 columns, ListViewItem.SubItems will auto create a + // SubItem of the wrong type. Casting using 'as' handles this rare case. + return this.SubItems[index] as OLVListSubItem; + + return null; + } + + + /// + /// Return bounds of the given subitem + /// + /// This correctly calculates the bounds even for column 0. + public virtual Rectangle GetSubItemBounds(int subItemIndex) { + if (subItemIndex == 0) { + Rectangle r = this.Bounds; + Point sides = NativeMethods.GetScrolledColumnSides(this.ListView, subItemIndex); + r.X = sides.X + 1; + r.Width = sides.Y - sides.X; + return r; + } + + OLVListSubItem subItem = this.GetSubItem(subItemIndex); + return subItem == null ? new Rectangle() : subItem.Bounds; + } + + #endregion + } +} diff --git a/ObjectListView/Implementation/OLVListSubItem.cs b/ObjectListView/Implementation/OLVListSubItem.cs new file mode 100644 index 00000000..e4f5bfe9 --- /dev/null +++ b/ObjectListView/Implementation/OLVListSubItem.cs @@ -0,0 +1,173 @@ +/* + * OLVListSubItem - A single cell in an ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Text; +using System.Windows.Forms; +using System.ComponentModel; + +namespace BrightIdeasSoftware { + + /// + /// A ListViewSubItem that knows which image should be drawn against it. + /// + [Browsable(false)] + public class OLVListSubItem : ListViewItem.ListViewSubItem { + #region Constructors + + /// + /// Create a OLVListSubItem + /// + public OLVListSubItem() { + } + + /// + /// Create a OLVListSubItem that shows the given string and image + /// + public OLVListSubItem(object modelValue, string text, Object image) { + this.ModelValue = modelValue; + this.Text = text; + this.ImageSelector = image; + } + + #endregion + + #region Properties + + /// + /// Gets or sets how many pixels will be left blank around this cell + /// + /// This setting only takes effect when the control is owner drawn. + public Rectangle? CellPadding { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets or sets how this cell will be vertically aligned + /// + /// This setting only takes effect when the control is owner drawn. + public StringAlignment? CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment? cellVerticalAlignment; + + /// + /// Gets or sets the model value is being displayed by this subitem. + /// + public object ModelValue + { + get { return modelValue; } + private set { modelValue = value; } + } + private object modelValue; + + /// + /// Gets if this subitem has any decorations set for it. + /// + public bool HasDecoration { + get { + return this.decorations != null && this.decorations.Count > 0; + } + } + + /// + /// Gets or sets the decoration that will be drawn over this item + /// + /// Setting this replaces all other decorations + public IDecoration Decoration { + get { + return this.HasDecoration ? this.Decorations[0] : null; + } + set { + this.Decorations.Clear(); + if (value != null) + this.Decorations.Add(value); + } + } + + /// + /// Gets the collection of decorations that will be drawn over this item + /// + public IList Decorations { + get { + if (this.decorations == null) + this.decorations = new List(); + return this.decorations; + } + } + private IList decorations; + + /// + /// Get or set the image that should be shown against this item + /// + /// This can be an Image, a string or an int. A string or an int will + /// be used as an index into the small image list. + public Object ImageSelector { + get { return imageSelector; } + set { imageSelector = value; } + } + private Object imageSelector; + + /// + /// Gets or sets the url that should be invoked when this subitem is clicked + /// + public string Url + { + get { return this.url; } + set { this.url = value; } + } + private string url; + + /// + /// Gets or sets whether this cell is selected + /// + public bool Selected + { + get { return this.selected; } + set { this.selected = value; } + } + private bool selected; + + #endregion + + #region Implementation Properties + + /// + /// Return the state of the animation of the image on this subitem. + /// Null means there is either no image, or it is not an animation + /// + internal ImageRenderer.AnimationState AnimationState; + + #endregion + } + +} diff --git a/ObjectListView/Implementation/OlvListViewHitTestInfo.cs b/ObjectListView/Implementation/OlvListViewHitTestInfo.cs new file mode 100644 index 00000000..7cc28eb8 --- /dev/null +++ b/ObjectListView/Implementation/OlvListViewHitTestInfo.cs @@ -0,0 +1,388 @@ +/* + * OlvListViewHitTestInfo - All information gathered during a OlvHitTest() operation + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; + +namespace BrightIdeasSoftware { + + /// + /// An indication of where a hit was within ObjectListView cell + /// + public enum HitTestLocation { + /// + /// Nowhere + /// + Nothing, + + /// + /// On the text + /// + Text, + + /// + /// On the image + /// + Image, + + /// + /// On the checkbox + /// + CheckBox, + + /// + /// On the expand button (TreeListView) + /// + ExpandButton, + + /// + /// in a button (cell must have ButtonRenderer) + /// + Button, + + /// + /// in the cell but not in any more specific location + /// + InCell, + + /// + /// UserDefined location1 (used for custom renderers) + /// + UserDefined, + + /// + /// On the expand/collapse widget of the group + /// + GroupExpander, + + /// + /// Somewhere on a group + /// + Group, + + /// + /// Somewhere in a column header + /// + Header, + + /// + /// Somewhere in a column header checkbox + /// + HeaderCheckBox, + + /// + /// Somewhere in a header divider + /// + HeaderDivider, + } + + /// + /// A collection of ListViewHitTest constants + /// + [Flags] + public enum HitTestLocationEx { + /// + /// + /// + LVHT_NOWHERE = 0x00000001, + /// + /// + /// + LVHT_ONITEMICON = 0x00000002, + /// + /// + /// + LVHT_ONITEMLABEL = 0x00000004, + /// + /// + /// + LVHT_ONITEMSTATEICON = 0x00000008, + /// + /// + /// + LVHT_ONITEM = (LVHT_ONITEMICON | LVHT_ONITEMLABEL | LVHT_ONITEMSTATEICON), + + /// + /// + /// + LVHT_ABOVE = 0x00000008, + /// + /// + /// + LVHT_BELOW = 0x00000010, + /// + /// + /// + LVHT_TORIGHT = 0x00000020, + /// + /// + /// + LVHT_TOLEFT = 0x00000040, + + /// + /// + /// + LVHT_EX_GROUP_HEADER = 0x10000000, + /// + /// + /// + LVHT_EX_GROUP_FOOTER = 0x20000000, + /// + /// + /// + LVHT_EX_GROUP_COLLAPSE = 0x40000000, + /// + /// + /// + LVHT_EX_GROUP_BACKGROUND = -2147483648, // 0x80000000 + /// + /// + /// + LVHT_EX_GROUP_STATEICON = 0x01000000, + /// + /// + /// + LVHT_EX_GROUP_SUBSETLINK = 0x02000000, + /// + /// + /// + LVHT_EX_GROUP = (LVHT_EX_GROUP_BACKGROUND | LVHT_EX_GROUP_COLLAPSE | LVHT_EX_GROUP_FOOTER | LVHT_EX_GROUP_HEADER | LVHT_EX_GROUP_STATEICON | LVHT_EX_GROUP_SUBSETLINK), + /// + /// + /// + LVHT_EX_GROUP_MINUS_FOOTER_AND_BKGRD = (LVHT_EX_GROUP_COLLAPSE | LVHT_EX_GROUP_HEADER | LVHT_EX_GROUP_STATEICON | LVHT_EX_GROUP_SUBSETLINK), + /// + /// + /// + LVHT_EX_ONCONTENTS = 0x04000000, // On item AND not on the background + /// + /// + /// + LVHT_EX_FOOTER = 0x08000000, + } + + /// + /// Instances of this class encapsulate the information gathered during a OlvHitTest() + /// operation. + /// + /// Custom renderers can use HitTestLocation.UserDefined and the UserData + /// object to store more specific locations for use during event handlers. + public class OlvListViewHitTestInfo { + + /// + /// Create a OlvListViewHitTestInfo + /// + public OlvListViewHitTestInfo(OLVListItem olvListItem, OLVListSubItem subItem, int flags, OLVGroup group, int iColumn) + { + this.item = olvListItem; + this.subItem = subItem; + this.location = ConvertNativeFlagsToDotNetLocation(olvListItem, flags); + this.HitTestLocationEx = (HitTestLocationEx)flags; + this.Group = group; + this.ColumnIndex = iColumn; + this.ListView = olvListItem == null ? null : (ObjectListView)olvListItem.ListView; + + switch (location) { + case ListViewHitTestLocations.StateImage: + this.HitTestLocation = HitTestLocation.CheckBox; + break; + case ListViewHitTestLocations.Image: + this.HitTestLocation = HitTestLocation.Image; + break; + case ListViewHitTestLocations.Label: + this.HitTestLocation = HitTestLocation.Text; + break; + default: + if ((this.HitTestLocationEx & HitTestLocationEx.LVHT_EX_GROUP_COLLAPSE) == HitTestLocationEx.LVHT_EX_GROUP_COLLAPSE) + this.HitTestLocation = HitTestLocation.GroupExpander; + else if ((this.HitTestLocationEx & HitTestLocationEx.LVHT_EX_GROUP_MINUS_FOOTER_AND_BKGRD) != 0) + this.HitTestLocation = HitTestLocation.Group; + else + this.HitTestLocation = HitTestLocation.Nothing; + break; + } + } + + /// + /// Create a OlvListViewHitTestInfo when the header was hit + /// + public OlvListViewHitTestInfo(ObjectListView olv, int iColumn, bool isOverCheckBox, int iDivider) { + this.ListView = olv; + this.ColumnIndex = iColumn; + this.HeaderDividerIndex = iDivider; + this.HitTestLocation = isOverCheckBox ? HitTestLocation.HeaderCheckBox : (iDivider < 0 ? HitTestLocation.Header : HitTestLocation.HeaderDivider); + } + + private static ListViewHitTestLocations ConvertNativeFlagsToDotNetLocation(OLVListItem hitItem, int flags) + { + // Untangle base .NET behaviour. + + // In Windows SDK, the value 8 can have two meanings here: LVHT_ONITEMSTATEICON or LVHT_ABOVE. + // .NET changes these to be: + // - LVHT_ABOVE becomes ListViewHitTestLocations.AboveClientArea (which is 0x100). + // - LVHT_ONITEMSTATEICON becomes ListViewHitTestLocations.StateImage (which is 0x200). + // So, if we see the 8 bit set in flags, we change that to either a state image hit + // (if we hit an item) or to AboveClientAream if nothing was hit. + + if ((8 & flags) == 8) + return (ListViewHitTestLocations)(0xf7 & flags | (hitItem == null ? 0x100 : 0x200)); + + // Mask off the LVHT_EX_XXXX values since ListViewHitTestLocations doesn't have them + return (ListViewHitTestLocations)(flags & 0xffff); + } + + #region Public fields + + /// + /// Where is the hit location? + /// + public HitTestLocation HitTestLocation; + + /// + /// Where is the hit location? + /// + public HitTestLocationEx HitTestLocationEx; + + /// + /// Which group was hit? + /// + public OLVGroup Group; + + /// + /// Custom renderers can use this information to supply more details about the hit location + /// + public Object UserData; + + #endregion + + #region Public read-only properties + + /// + /// Gets the item that was hit + /// + public OLVListItem Item { + get { return item; } + internal set { item = value; } + } + private OLVListItem item; + + /// + /// Gets the subitem that was hit + /// + public OLVListSubItem SubItem { + get { return subItem; } + internal set { subItem = value; } + } + private OLVListSubItem subItem; + + /// + /// Gets the part of the subitem that was hit + /// + public ListViewHitTestLocations Location { + get { return location; } + internal set { location = value; } + } + private ListViewHitTestLocations location; + + /// + /// Gets the ObjectListView that was tested + /// + public ObjectListView ListView { + get { return listView; } + internal set { listView = value; } + } + private ObjectListView listView; + + /// + /// Gets the model object that was hit + /// + public Object RowObject { + get { + return this.Item == null ? null : this.Item.RowObject; + } + } + + /// + /// Gets the index of the row under the hit point or -1 + /// + public int RowIndex { + get { return this.Item == null ? -1 : this.Item.Index; } + } + + /// + /// Gets the index of the column under the hit point + /// + public int ColumnIndex { + get { return columnIndex; } + internal set { columnIndex = value; } + } + private int columnIndex; + + /// + /// Gets the index of the header divider + /// + public int HeaderDividerIndex { + get { return headerDividerIndex; } + internal set { headerDividerIndex = value; } + } + private int headerDividerIndex = -1; + + /// + /// Gets the column that was hit + /// + public OLVColumn Column { + get { + int index = this.ColumnIndex; + return index < 0 || this.ListView == null ? null : this.ListView.GetColumn(index); + } + } + + #endregion + + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object. + /// + /// 2 + public override string ToString() + { + return string.Format("HitTestLocation: {0}, HitTestLocationEx: {1}, Item: {2}, SubItem: {3}, Location: {4}, Group: {5}, ColumnIndex: {6}", + this.HitTestLocation, this.HitTestLocationEx, this.item, this.subItem, this.location, this.Group, this.ColumnIndex); + } + + internal class HeaderHitTestInfo + { + public int ColumnIndex; + public bool IsOverCheckBox; + public int OverDividerIndex; + } + } +} diff --git a/ObjectListView/Implementation/TreeDataSourceAdapter.cs b/ObjectListView/Implementation/TreeDataSourceAdapter.cs new file mode 100644 index 00000000..e54cf10b --- /dev/null +++ b/ObjectListView/Implementation/TreeDataSourceAdapter.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; + +namespace BrightIdeasSoftware +{ + /// + /// A TreeDataSourceAdapter knows how to build a tree structure from a binding list. + /// + /// To build a tree + public class TreeDataSourceAdapter : DataSourceAdapter + { + #region Life and death + + /// + /// Create a data source adaptor that knows how to build a tree structure + /// + /// + public TreeDataSourceAdapter(DataTreeListView tlv) + : base(tlv) { + this.treeListView = tlv; + this.treeListView.CanExpandGetter = delegate(object model) { return this.CalculateHasChildren(model); }; + this.treeListView.ChildrenGetter = delegate(object model) { return this.CalculateChildren(model); }; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the name of the property/column that uniquely identifies each row. + /// + /// + /// + /// The value contained by this column must be unique across all rows + /// in the data source. Odd and unpredictable things will happen if two + /// rows have the same id. + /// + /// Null cannot be a valid key value. + /// + public virtual string KeyAspectName { + get { return keyAspectName; } + set { + if (keyAspectName == value) + return; + keyAspectName = value; + this.keyMunger = new Munger(this.KeyAspectName); + this.InitializeDataSource(); + } + } + private string keyAspectName; + + /// + /// Gets or sets the name of the property/column that contains the key of + /// the parent of a row. + /// + /// + /// + /// The test condition for deciding if one row is the parent of another is functionally + /// equivalent to this: + /// + /// Object.Equals(candidateParentRow[this.KeyAspectName], row[this.ParentKeyAspectName]) + /// + /// + /// Unlike key value, parent keys can be null but a null parent key can only be used + /// to identify root objects. + /// + public virtual string ParentKeyAspectName { + get { return parentKeyAspectName; } + set { + if (parentKeyAspectName == value) + return; + parentKeyAspectName = value; + this.parentKeyMunger = new Munger(this.ParentKeyAspectName); + this.InitializeDataSource(); + } + } + private string parentKeyAspectName; + + /// + /// Gets or sets the value that identifies a row as a root object. + /// When the ParentKey of a row equals the RootKeyValue, that row will + /// be treated as root of the TreeListView. + /// + /// + /// + /// The test condition for deciding a root object is functionally + /// equivalent to this: + /// + /// Object.Equals(candidateRow[this.ParentKeyAspectName], this.RootKeyValue) + /// + /// + /// The RootKeyValue can be null. + /// + public virtual object RootKeyValue { + get { return rootKeyValue; } + set { + if (Equals(rootKeyValue, value)) + return; + rootKeyValue = value; + this.InitializeDataSource(); + } + } + private object rootKeyValue; + + /// + /// Gets or sets whether or not the key columns (id and parent id) should + /// be shown to the user. + /// + /// This must be set before the DataSource is set. It has no effect + /// afterwards. + public virtual bool ShowKeyColumns { + get { return showKeyColumns; } + set { showKeyColumns = value; } + } + private bool showKeyColumns = true; + + + #endregion + + #region Implementation properties + + /// + /// Gets the DataTreeListView that is being managed + /// + protected DataTreeListView TreeListView { + get { return treeListView; } + } + private readonly DataTreeListView treeListView; + + #endregion + + #region Implementation + + /// + /// + /// + protected override void InitializeDataSource() { + base.InitializeDataSource(); + this.TreeListView.RebuildAll(true); + } + + /// + /// + /// + protected override void SetListContents() { + this.TreeListView.Roots = this.CalculateRoots(); + } + + /// + /// + /// + /// + /// + protected override bool ShouldCreateColumn(PropertyDescriptor property) { + // If the property is a key column, and we aren't supposed to show keys, don't show it + if (!this.ShowKeyColumns && (property.Name == this.KeyAspectName || property.Name == this.ParentKeyAspectName)) + return false; + + return base.ShouldCreateColumn(property); + } + + /// + /// + /// + /// + protected override void HandleListChangedItemChanged(System.ComponentModel.ListChangedEventArgs e) { + // If the id or the parent id of a row changes, we just rebuild everything. + // We can't do anything more specific. We don't know what the previous values, so we can't + // tell the previous parent to refresh itself. If the id itself has changed, things that used + // to be children will no longer be children. Just rebuild everything. + // It seems PropertyDescriptor is only filled in .NET 4 :( + if (e.PropertyDescriptor != null && + (e.PropertyDescriptor.Name == this.KeyAspectName || + e.PropertyDescriptor.Name == this.ParentKeyAspectName)) + this.InitializeDataSource(); + else + base.HandleListChangedItemChanged(e); + } + + /// + /// + /// + /// + protected override void ChangePosition(int index) { + // We can't use our base method directly, since the normal position management + // doesn't know about our tree structure. They treat our dataset as a flat list + // but we have a collapsible structure. This means that the 5'th row to them + // may not even be visible to us + + // To display the n'th row, we have to make sure that all its ancestors + // are expanded. Then we will be able to select it. + object model = this.CurrencyManager.List[index]; + object parent = this.CalculateParent(model); + while (parent != null && !this.TreeListView.IsExpanded(parent)) { + this.TreeListView.Expand(parent); + parent = this.CalculateParent(parent); + } + + base.ChangePosition(index); + } + + private IEnumerable CalculateRoots() { + foreach (object x in this.CurrencyManager.List) { + object parentKey = this.GetParentValue(x); + if (Object.Equals(this.RootKeyValue, parentKey)) + yield return x; + } + } + + private bool CalculateHasChildren(object model) { + object keyValue = this.GetKeyValue(model); + if (keyValue == null) + return false; + + foreach (object x in this.CurrencyManager.List) { + object parentKey = this.GetParentValue(x); + if (Object.Equals(keyValue, parentKey)) + return true; + } + return false; + } + + private IEnumerable CalculateChildren(object model) { + object keyValue = this.GetKeyValue(model); + if (keyValue != null) { + foreach (object x in this.CurrencyManager.List) { + object parentKey = this.GetParentValue(x); + if (Object.Equals(keyValue, parentKey)) + yield return x; + } + } + } + + private object CalculateParent(object model) { + object parentValue = this.GetParentValue(model); + if (parentValue == null) + return null; + + foreach (object x in this.CurrencyManager.List) { + object key = this.GetKeyValue(x); + if (Object.Equals(parentValue, key)) + return x; + } + return null; + } + + private object GetKeyValue(object model) { + return this.keyMunger == null ? null : this.keyMunger.GetValue(model); + } + + private object GetParentValue(object model) { + return this.parentKeyMunger == null ? null : this.parentKeyMunger.GetValue(model); + } + + #endregion + + private Munger keyMunger; + private Munger parentKeyMunger; + } +} \ No newline at end of file diff --git a/ObjectListView/Implementation/VirtualGroups.cs b/ObjectListView/Implementation/VirtualGroups.cs new file mode 100644 index 00000000..1466ebbd --- /dev/null +++ b/ObjectListView/Implementation/VirtualGroups.cs @@ -0,0 +1,341 @@ +/* + * Virtual groups - Classes and interfaces needed to implement virtual groups + * + * Author: Phillip Piper + * Date: 28/08/2009 11:10am + * + * Change log: + * 2011-02-21 JPP - Correctly honor group comparer and collapsible groups settings + * v2.3 + * 2009-08-28 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using System.Runtime.InteropServices; + +namespace BrightIdeasSoftware +{ + /// + /// A IVirtualGroups is the interface that a virtual list must implement to support virtual groups + /// + public interface IVirtualGroups + { + /// + /// Return the list of groups that should be shown according to the given parameters + /// + /// + /// + IList GetGroups(GroupingParameters parameters); + + /// + /// Return the index of the item that appears at the given position within the given group. + /// + /// + /// + /// + int GetGroupMember(OLVGroup group, int indexWithinGroup); + + /// + /// Return the index of the group to which the given item belongs + /// + /// + /// + int GetGroup(int itemIndex); + + /// + /// Return the index at which the given item is shown in the given group + /// + /// + /// + /// + int GetIndexWithinGroup(OLVGroup group, int itemIndex); + + /// + /// A hint that the given range of items are going to be required + /// + /// + /// + /// + /// + void CacheHint(int fromGroupIndex, int fromIndex, int toGroupIndex, int toIndex); + } + + /// + /// This is a safe, do nothing implementation of a grouping strategy + /// + public class AbstractVirtualGroups : IVirtualGroups + { + /// + /// Return the list of groups that should be shown according to the given parameters + /// + /// + /// + public virtual IList GetGroups(GroupingParameters parameters) { + return new List(); + } + + /// + /// Return the index of the item that appears at the given position within the given group. + /// + /// + /// + /// + public virtual int GetGroupMember(OLVGroup group, int indexWithinGroup) { + return -1; + } + + /// + /// Return the index of the group to which the given item belongs + /// + /// + /// + public virtual int GetGroup(int itemIndex) { + return -1; + } + + /// + /// Return the index at which the given item is shown in the given group + /// + /// + /// + /// + public virtual int GetIndexWithinGroup(OLVGroup group, int itemIndex) { + return -1; + } + + /// + /// A hint that the given range of items are going to be required + /// + /// + /// + /// + /// + public virtual void CacheHint(int fromGroupIndex, int fromIndex, int toGroupIndex, int toIndex) { + } + } + + + /// + /// Provides grouping functionality to a FastObjectListView + /// + public class FastListGroupingStrategy : AbstractVirtualGroups + { + /// + /// Create groups for FastListView + /// + /// + /// + public override IList GetGroups(GroupingParameters parameters) { + + // There is a lot of overlap between this method and ObjectListView.MakeGroups() + // Any changes made here may need to be reflected there + + // This strategy can only be used on FastObjectListViews + FastObjectListView folv = (FastObjectListView)parameters.ListView; + + // Separate the list view items into groups, using the group key as the descrimanent + int objectCount = 0; + NullableDictionary> map = new NullableDictionary>(); + foreach (object model in folv.FilteredObjects) { + object key = parameters.GroupByColumn.GetGroupKey(model); + if (!map.ContainsKey(key)) + map[key] = new List(); + map[key].Add(model); + objectCount++; + } + + // Sort the items within each group + OLVColumn primarySortColumn = parameters.SortItemsByPrimaryColumn ? parameters.ListView.GetColumn(0) : parameters.PrimarySort; + ModelObjectComparer sorter = new ModelObjectComparer(primarySortColumn, parameters.PrimarySortOrder, + parameters.SecondarySort, parameters.SecondarySortOrder); + foreach (object key in map.Keys) { + map[key].Sort(sorter); + } + + // Make a list of the required groups + List groups = new List(); + foreach (object key in map.Keys) { + OLVGroup lvg = parameters.CreateGroup(key, map[key].Count, folv.HasCollapsibleGroups); + lvg.Contents = map[key].ConvertAll(delegate(object x) { return folv.IndexOf(x); }); + lvg.VirtualItemCount = map[key].Count; + if (parameters.GroupByColumn.GroupFormatter != null) + parameters.GroupByColumn.GroupFormatter(lvg, parameters); + groups.Add(lvg); + } + + // Sort the groups + if (parameters.GroupByOrder != SortOrder.None) + groups.Sort(parameters.GroupComparer ?? new OLVGroupComparer(parameters.GroupByOrder)); + + // Build an array that remembers which group each item belongs to. + this.indexToGroupMap = new List(objectCount); + this.indexToGroupMap.AddRange(new int[objectCount]); + + for (int i = 0; i < groups.Count; i++) { + OLVGroup group = groups[i]; + List members = (List)group.Contents; + foreach (int j in members) + this.indexToGroupMap[j] = i; + } + + return groups; + } + private List indexToGroupMap; + + /// + /// + /// + /// + /// + /// + public override int GetGroupMember(OLVGroup group, int indexWithinGroup) { + return (int)group.Contents[indexWithinGroup]; + } + + /// + /// + /// + /// + /// + public override int GetGroup(int itemIndex) { + return this.indexToGroupMap[itemIndex]; + } + + /// + /// + /// + /// + /// + /// + public override int GetIndexWithinGroup(OLVGroup group, int itemIndex) { + return group.Contents.IndexOf(itemIndex); + } + } + + + /// + /// This is the COM interface that a ListView must be given in order for groups in virtual lists to work. + /// + /// + /// This interface is NOT documented by MS. It was found on Greg Chapell's site. This means that there is + /// no guarantee that it will work on future versions of Windows, nor continue to work on current ones. + /// + [ComImport(), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown), + Guid("44C09D56-8D3B-419D-A462-7B956B105B47")] + internal interface IOwnerDataCallback + { + /// + /// Not sure what this does + /// + /// + /// + void GetItemPosition(int i, out NativeMethods.POINT pt); + + /// + /// Not sure what this does + /// + /// + /// + void SetItemPosition(int t, NativeMethods.POINT pt); + + /// + /// Get the index of the item that occurs at the n'th position of the indicated group. + /// + /// Index of the group + /// Index within the group + /// Index of the item within the whole list + void GetItemInGroup(int groupIndex, int n, out int itemIndex); + + /// + /// Get the index of the group to which the given item belongs + /// + /// Index of the item within the whole list + /// Which occurrences of the item is wanted + /// Index of the group + void GetItemGroup(int itemIndex, int occurrenceCount, out int groupIndex); + + /// + /// Get the number of groups that contain the given item + /// + /// Index of the item within the whole list + /// How many groups does it occur within + void GetItemGroupCount(int itemIndex, out int occurrenceCount); + + /// + /// A hint to prepare any cache for the given range of requests + /// + /// + /// + void OnCacheHint(NativeMethods.LVITEMINDEX i, NativeMethods.LVITEMINDEX j); + } + + /// + /// A default implementation of the IOwnerDataCallback interface + /// + [Guid("6FC61F50-80E8-49b4-B200-3F38D3865ABD")] + internal class OwnerDataCallbackImpl : IOwnerDataCallback + { + public OwnerDataCallbackImpl(VirtualObjectListView olv) { + this.olv = olv; + } + VirtualObjectListView olv; + + #region IOwnerDataCallback Members + + public void GetItemPosition(int i, out NativeMethods.POINT pt) { + //System.Diagnostics.Debug.WriteLine("GetItemPosition"); + throw new NotSupportedException(); + } + + public void SetItemPosition(int t, NativeMethods.POINT pt) { + //System.Diagnostics.Debug.WriteLine("SetItemPosition"); + throw new NotSupportedException(); + } + + public void GetItemInGroup(int groupIndex, int n, out int itemIndex) { + //System.Diagnostics.Debug.WriteLine(String.Format("-> GetItemInGroup({0}, {1})", groupIndex, n)); + itemIndex = this.olv.GroupingStrategy.GetGroupMember(this.olv.OLVGroups[groupIndex], n); + //System.Diagnostics.Debug.WriteLine(String.Format("<- {0}", itemIndex)); + } + + public void GetItemGroup(int itemIndex, int occurrenceCount, out int groupIndex) { + //System.Diagnostics.Debug.WriteLine(String.Format("GetItemGroup({0}, {1})", itemIndex, occurrenceCount)); + groupIndex = this.olv.GroupingStrategy.GetGroup(itemIndex); + //System.Diagnostics.Debug.WriteLine(String.Format("<- {0}", groupIndex)); + } + + public void GetItemGroupCount(int itemIndex, out int occurrenceCount) { + //System.Diagnostics.Debug.WriteLine(String.Format("GetItemGroupCount({0})", itemIndex)); + occurrenceCount = 1; + } + + public void OnCacheHint(NativeMethods.LVITEMINDEX from, NativeMethods.LVITEMINDEX to) { + //System.Diagnostics.Debug.WriteLine(String.Format("OnCacheHint({0}, {1}, {2}, {3})", from.iGroup, from.iItem, to.iGroup, to.iItem)); + this.olv.GroupingStrategy.CacheHint(from.iGroup, from.iItem, to.iGroup, to.iItem); + } + + #endregion + } +} diff --git a/ObjectListView/Implementation/VirtualListDataSource.cs b/ObjectListView/Implementation/VirtualListDataSource.cs new file mode 100644 index 00000000..7bc378d3 --- /dev/null +++ b/ObjectListView/Implementation/VirtualListDataSource.cs @@ -0,0 +1,349 @@ +/* + * VirtualListDataSource - Encapsulate how data is provided to a virtual list + * + * Author: Phillip Piper + * Date: 28/08/2009 11:10am + * + * Change log: + * v2.4 + * 2010-04-01 JPP - Added IFilterableDataSource + * v2.3 + * 2009-08-28 JPP - Initial version (Separated from VirtualObjectListView.cs) + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A VirtualListDataSource is a complete manner to provide functionality to a virtual list. + /// An object that implements this interface provides a VirtualObjectListView with all the + /// information it needs to be fully functional. + /// + /// Implementors must provide functioning implementations of at least GetObjectCount() + /// and GetNthObject(), otherwise nothing will appear in the list. + public interface IVirtualListDataSource + { + /// + /// Return the object that should be displayed at the n'th row. + /// + /// The index of the row whose object is to be returned. + /// The model object at the n'th row, or null if the fetching was unsuccessful. + Object GetNthObject(int n); + + /// + /// Return the number of rows that should be visible in the virtual list + /// + /// The number of rows the list view should have. + int GetObjectCount(); + + /// + /// Get the index of the row that is showing the given model object + /// + /// The model object sought + /// The index of the row showing the model, or -1 if the object could not be found. + int GetObjectIndex(Object model); + + /// + /// The ListView is about to request the given range of items. Do + /// whatever caching seems appropriate. + /// + /// + /// + void PrepareCache(int first, int last); + + /// + /// Find the first row that "matches" the given text in the given range. + /// + /// The text typed by the user + /// Start searching from this index. This may be greater than the 'to' parameter, + /// in which case the search should descend + /// Do not search beyond this index. This may be less than the 'from' parameter. + /// The column that should be considered when looking for a match. + /// Return the index of row that was matched, or -1 if no match was found + int SearchText(string value, int first, int last, OLVColumn column); + + /// + /// Sort the model objects in the data source. + /// + /// + /// + void Sort(OLVColumn column, SortOrder order); + + //----------------------------------------------------------------------------------- + // Modification commands + // THINK: Should we split these four into a separate interface? + + /// + /// Add the given collection of model objects to this control. + /// + /// A collection of model objects + void AddObjects(ICollection modelObjects); + + /// + /// Insert the given collection of model objects to this control at the position + /// + /// Index where the collection will be added + /// A collection of model objects + void InsertObjects(int index, ICollection modelObjects); + + /// + /// Remove all of the given objects from the control + /// + /// Collection of objects to be removed + void RemoveObjects(ICollection modelObjects); + + /// + /// Set the collection of objects that this control will show. + /// + /// + void SetObjects(IEnumerable collection); + + /// + /// Update/replace the nth object with the given object + /// + /// + /// + void UpdateObject(int index, object modelObject); + } + + /// + /// This extension allow virtual lists to filter their contents + /// + public interface IFilterableDataSource + { + /// + /// All subsequent retrievals on this data source should be filtered + /// through the given filters. null means no filtering of that kind. + /// + /// + /// + void ApplyFilters(IModelFilter modelFilter, IListFilter listFilter); + } + + /// + /// A do-nothing implementation of the VirtualListDataSource interface. + /// + public class AbstractVirtualListDataSource : IVirtualListDataSource, IFilterableDataSource + { + /// + /// Creates an AbstractVirtualListDataSource + /// + /// + public AbstractVirtualListDataSource(VirtualObjectListView listView) { + this.listView = listView; + } + + /// + /// The list view that this data source is giving information to. + /// + protected VirtualObjectListView listView; + + /// + /// + /// + /// + /// + public virtual object GetNthObject(int n) { + return null; + } + + /// + /// + /// + /// + public virtual int GetObjectCount() { + return -1; + } + + /// + /// + /// + /// + /// + public virtual int GetObjectIndex(object model) { + return -1; + } + + /// + /// + /// + /// + /// + public virtual void PrepareCache(int from, int to) { + } + + /// + /// + /// + /// + /// + /// + /// + /// + public virtual int SearchText(string value, int first, int last, OLVColumn column) { + return -1; + } + + /// + /// + /// + /// + /// + public virtual void Sort(OLVColumn column, SortOrder order) { + } + + /// + /// + /// + /// + public virtual void AddObjects(ICollection modelObjects) { + } + + /// + /// + /// + /// + /// + public virtual void InsertObjects(int index, ICollection modelObjects) { + } + + /// + /// + /// + /// + public virtual void RemoveObjects(ICollection modelObjects) { + } + + /// + /// + /// + /// + public virtual void SetObjects(IEnumerable collection) { + } + + /// + /// Update/replace the nth object with the given object + /// + /// + /// + public virtual void UpdateObject(int index, object modelObject) { + } + + /// + /// This is a useful default implementation of SearchText method, intended to be called + /// by implementors of IVirtualListDataSource. + /// + /// + /// + /// + /// + /// + /// + static public int DefaultSearchText(string value, int first, int last, OLVColumn column, IVirtualListDataSource source) { + if (first <= last) { + for (int i = first; i <= last; i++) { + string data = column.GetStringValue(source.GetNthObject(i)); + if (data.StartsWith(value, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } else { + for (int i = first; i >= last; i--) { + string data = column.GetStringValue(source.GetNthObject(i)); + if (data.StartsWith(value, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } + + return -1; + } + + #region IFilterableDataSource Members + + /// + /// + /// + /// + /// + virtual public void ApplyFilters(IModelFilter modelFilter, IListFilter listFilter) { + } + + #endregion + } + + /// + /// This class mimics the behavior of VirtualObjectListView v1.x. + /// + public class VirtualListVersion1DataSource : AbstractVirtualListDataSource + { + /// + /// Creates a VirtualListVersion1DataSource + /// + /// + public VirtualListVersion1DataSource(VirtualObjectListView listView) + : base(listView) { + } + + #region Public properties + + /// + /// How will the n'th object of the data source be fetched? + /// + public RowGetterDelegate RowGetter { + get { return rowGetter; } + set { rowGetter = value; } + } + private RowGetterDelegate rowGetter; + + #endregion + + #region IVirtualListDataSource implementation + + /// + /// + /// + /// + /// + public override object GetNthObject(int n) { + if (this.RowGetter == null) + return null; + else + return this.RowGetter(n); + } + + /// + /// + /// + /// + /// + /// + /// + /// + public override int SearchText(string value, int first, int last, OLVColumn column) { + return DefaultSearchText(value, first, last, column, this); + } + + #endregion + } +} diff --git a/ObjectListView/OLVColumn.cs b/ObjectListView/OLVColumn.cs new file mode 100644 index 00000000..21ce4f90 --- /dev/null +++ b/ObjectListView/OLVColumn.cs @@ -0,0 +1,1909 @@ +/* + * OLVColumn - A column in an ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2018-05-05 JPP - Added EditorCreator to OLVColumn + * 2015-06-12 JPP - HeaderTextAlign became nullable so that it can be "not set" (this was always the intent) + * 2014-09-07 JPP - Added ability to have checkboxes in headers + * + * 2011-05-27 JPP - Added Sortable, Hideable, Groupable, Searchable, ShowTextInHeader properties + * 2011-04-12 JPP - Added HasFilterIndicator + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.ComponentModel; +using System.Windows.Forms; +using System.Drawing; +using System.Collections; +using System.Diagnostics; +using System.Drawing.Design; + +namespace BrightIdeasSoftware { + + // TODO + //[TypeConverter(typeof(ExpandableObjectConverter))] + //public class CheckBoxSettings + //{ + // private bool useSettings; + // private Image checkedImage; + + // public bool UseSettings { + // get { return useSettings; } + // set { useSettings = value; } + // } + + // public Image CheckedImage { + // get { return checkedImage; } + // set { checkedImage = value; } + // } + + // public Image UncheckedImage { + // get { return checkedImage; } + // set { checkedImage = value; } + // } + + // public Image IndeterminateImage { + // get { return checkedImage; } + // set { checkedImage = value; } + // } + //} + + /// + /// An OLVColumn knows which aspect of an object it should present. + /// + /// + /// The column knows how to: + /// + /// extract its aspect from the row object + /// convert an aspect to a string + /// calculate the image for the row object + /// extract a group "key" from the row object + /// convert a group "key" into a title for the group + /// + /// For sorting to work correctly, aspects from the same column + /// must be of the same type, that is, the same aspect cannot sometimes + /// return strings and other times integers. + /// + [Browsable(false)] + public partial class OLVColumn : ColumnHeader { + + /// + /// How should the button be sized? + /// + public enum ButtonSizingMode + { + /// + /// Every cell will have the same sized button, as indicated by ButtonSize property + /// + FixedBounds, + + /// + /// Every cell will draw a button that fills the cell, inset by ButtonPadding + /// + CellBounds, + + /// + /// Each button will be resized to contain the text of the Aspect + /// + TextBounds + } + + #region Life and death + + /// + /// Create an OLVColumn + /// + public OLVColumn() { + } + + /// + /// Initialize a column to have the given title, and show the given aspect + /// + /// The title of the column + /// The aspect to be shown in the column + public OLVColumn(string title, string aspect) + : this() { + this.Text = title; + this.AspectName = aspect; + } + + #endregion + + #region Public Properties + + /// + /// This delegate will be used to extract a value to be displayed in this column. + /// + /// + /// If this is set, AspectName is ignored. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AspectGetterDelegate AspectGetter { + get { return aspectGetter; } + set { aspectGetter = value; } + } + private AspectGetterDelegate aspectGetter; + + /// + /// Remember if this aspect getter for this column was generated internally, and can therefore + /// be regenerated at will + /// + [Obsolete("This property is no longer maintained", true), + Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool AspectGetterAutoGenerated { + get { return aspectGetterAutoGenerated; } + set { aspectGetterAutoGenerated = value; } + } + private bool aspectGetterAutoGenerated; + + /// + /// The name of the property or method that should be called to get the value to display in this column. + /// This is only used if a ValueGetterDelegate has not been given. + /// + /// This name can be dotted to chain references to properties or parameter-less methods. + /// "DateOfBirth" + /// "Owner.HomeAddress.Postcode" + [Category("ObjectListView"), + Description("The name of the property or method that should be called to get the aspect to display in this column"), + DefaultValue(null)] + public string AspectName { + get { return aspectName; } + set { + aspectName = value; + this.aspectMunger = null; + } + } + private string aspectName; + + /// + /// This delegate will be used to put an edited value back into the model object. + /// + /// + /// This does nothing if IsEditable == false. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AspectPutterDelegate AspectPutter { + get { return aspectPutter; } + set { aspectPutter = value; } + } + private AspectPutterDelegate aspectPutter; + + /// + /// The delegate that will be used to translate the aspect to display in this column into a string. + /// + /// If this value is set, AspectToStringFormat will be ignored. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AspectToStringConverterDelegate AspectToStringConverter { + get { return aspectToStringConverter; } + set { aspectToStringConverter = value; } + } + private AspectToStringConverterDelegate aspectToStringConverter; + + /// + /// This format string will be used to convert an aspect to its string representation. + /// + /// + /// This string is passed as the first parameter to the String.Format() method. + /// This is only used if AspectToStringConverter has not been set. + /// "{0:C}" to convert a number to currency + [Category("ObjectListView"), + Description("The format string that will be used to convert an aspect to its string representation"), + DefaultValue(null)] + public string AspectToStringFormat { + get { return aspectToStringFormat; } + set { aspectToStringFormat = value; } + } + private string aspectToStringFormat; + + /// + /// Gets or sets whether the cell editor should use AutoComplete + /// + [Category("ObjectListView"), + Description("Should the editor for cells of this column use AutoComplete"), + DefaultValue(true)] + public bool AutoCompleteEditor { + get { return this.AutoCompleteEditorMode != AutoCompleteMode.None; } + set { + if (value) { + if (this.AutoCompleteEditorMode == AutoCompleteMode.None) + this.AutoCompleteEditorMode = AutoCompleteMode.Append; + } else + this.AutoCompleteEditorMode = AutoCompleteMode.None; + } + } + + /// + /// Gets or sets whether the cell editor should use AutoComplete + /// + [Category("ObjectListView"), + Description("Should the editor for cells of this column use AutoComplete"), + DefaultValue(AutoCompleteMode.Append)] + public AutoCompleteMode AutoCompleteEditorMode { + get { return autoCompleteEditorMode; } + set { autoCompleteEditorMode = value; } + } + private AutoCompleteMode autoCompleteEditorMode = AutoCompleteMode.Append; + + /// + /// Gets whether this column can be hidden by user actions + /// + /// This take into account both the Hideable property and whether this column + /// is the primary column of the listview (column 0). + [Browsable(false)] + public bool CanBeHidden { + get { + return this.Hideable && (this.Index != 0); + } + } + + /// + /// When a cell is edited, should the whole cell be used (minus any space used by checkbox or image)? + /// + /// + /// This is always treated as true when the control is NOT owner drawn. + /// + /// When this is false (the default) and the control is owner drawn, + /// ObjectListView will try to calculate the width of the cell's + /// actual contents, and then size the editing control to be just the right width. If this is true, + /// the whole width of the cell will be used, regardless of the cell's contents. + /// + /// If this property is not set on the column, the value from the control will be used + /// + /// This value is only used when the control is in Details view. + /// Regardless of this setting, developers can specify the exact size of the editing control + /// by listening for the CellEditStarting event. + /// + [Category("ObjectListView"), + Description("When a cell is edited, should the whole cell be used?"), + DefaultValue(null)] + public virtual bool? CellEditUseWholeCell + { + get { return cellEditUseWholeCell; } + set { cellEditUseWholeCell = value; } + } + private bool? cellEditUseWholeCell; + + /// + /// Get whether the whole cell should be used when editing a cell in this column + /// + /// This calculates the current effective value, which may be different to CellEditUseWholeCell + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual bool CellEditUseWholeCellEffective { + get { + bool? columnSpecificValue = this.ListView.View == View.Details ? this.CellEditUseWholeCell : (bool?) null; + return (columnSpecificValue ?? ((ObjectListView) this.ListView).CellEditUseWholeCell); + } + } + + /// + /// Gets or sets how many pixels will be left blank around this cells in this column + /// + /// This setting only takes effect when the control is owner drawn. + [Category("ObjectListView"), + Description("How many pixels will be left blank around the cells in this column?"), + DefaultValue(null)] + public Rectangle? CellPadding + { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets or sets how cells in this column will be vertically aligned. + /// + /// + /// + /// This setting only takes effect when the control is owner drawn. + /// + /// + /// If this is not set, the value from the control itself will be used. + /// + /// + [Category("ObjectListView"), + Description("How will cell values be vertically aligned?"), + DefaultValue(null)] + public virtual StringAlignment? CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment? cellVerticalAlignment; + + /// + /// Gets or sets whether this column will show a checkbox. + /// + /// + /// Setting this on column 0 has no effect. Column 0 check box is controlled + /// by the CheckBoxes property on the ObjectListView itself. + /// + [Category("ObjectListView"), + Description("Should values in this column be treated as a checkbox, rather than a string?"), + DefaultValue(false)] + public virtual bool CheckBoxes { + get { return checkBoxes; } + set { + if (this.checkBoxes == value) + return; + + this.checkBoxes = value; + if (this.checkBoxes) { + if (this.Renderer == null) + this.Renderer = new CheckStateRenderer(); + } else { + if (this.Renderer is CheckStateRenderer) + this.Renderer = null; + } + } + } + private bool checkBoxes; + + /// + /// Gets or sets the clustering strategy used for this column. + /// + /// + /// + /// The clustering strategy is used to build a Filtering menu for this item. + /// If this is null, a useful default will be chosen. + /// + /// + /// To disable filtering on this column, set UseFiltering to false. + /// + /// + /// Cluster strategies belong to a particular column. The same instance + /// cannot be shared between multiple columns. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IClusteringStrategy ClusteringStrategy { + get { + if (this.clusteringStrategy == null) + this.ClusteringStrategy = this.DecideDefaultClusteringStrategy(); + return clusteringStrategy; + } + set { + this.clusteringStrategy = value; + if (this.clusteringStrategy != null) + this.clusteringStrategy.Column = this; + } + } + private IClusteringStrategy clusteringStrategy; + + /// + /// Gets or sets a delegate that will create an editor for a cell in this column. + /// + /// + /// If you need different editors for different cells in the same column, this + /// delegate is your solution. Return null to use the default editor for the cell. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public EditorCreatorDelegate EditorCreator { + get { return editorCreator; } + set { editorCreator = value; } + } + private EditorCreatorDelegate editorCreator; + + /// + /// Gets or sets whether the button in this column (if this column is drawing buttons) will be enabled + /// even if the row itself is disabled + /// + [Category("ObjectListView"), + Description("If this column contains a button, should the button be enabled even if the row is disabled?"), + DefaultValue(false)] + public bool EnableButtonWhenItemIsDisabled + { + get { return this.enableButtonWhenItemIsDisabled; } + set { this.enableButtonWhenItemIsDisabled = value; } + } + private bool enableButtonWhenItemIsDisabled; + + /// + /// Should this column resize to fill the free space in the listview? + /// + /// + /// + /// If you want two (or more) columns to equally share the available free space, set this property to True. + /// If you want this column to have a larger or smaller share of the free space, you must + /// set the FreeSpaceProportion property explicitly. + /// + /// + /// Space filling columns are still governed by the MinimumWidth and MaximumWidth properties. + /// + /// /// + [Category("ObjectListView"), + Description("Will this column resize to fill unoccupied horizontal space in the listview?"), + DefaultValue(false)] + public bool FillsFreeSpace { + get { return this.FreeSpaceProportion > 0; } + set { this.FreeSpaceProportion = value ? 1 : 0; } + } + + /// + /// What proportion of the unoccupied horizontal space in the control should be given to this column? + /// + /// + /// + /// There are situations where it would be nice if a column (normally the rightmost one) would expand as + /// the list view expands, so that as much of the column was visible as possible without having to scroll + /// horizontally (you should never, ever make your users have to scroll anything horizontally!). + /// + /// + /// A space filling column is resized to occupy a proportion of the unoccupied width of the listview (the + /// unoccupied width is the width left over once all the non-filling columns have been given their space). + /// This property indicates the relative proportion of that unoccupied space that will be given to this column. + /// The actual value of this property is not important -- only its value relative to the value in other columns. + /// For example: + /// + /// + /// If there is only one space filling column, it will be given all the free space, regardless of the value in FreeSpaceProportion. + /// + /// + /// If there are two or more space filling columns and they all have the same value for FreeSpaceProportion, + /// they will share the free space equally. + /// + /// + /// If there are three space filling columns with values of 3, 2, and 1 + /// for FreeSpaceProportion, then the first column with occupy half the free space, the second will + /// occupy one-third of the free space, and the third column one-sixth of the free space. + /// + /// + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int FreeSpaceProportion { + get { return freeSpaceProportion; } + set { freeSpaceProportion = Math.Max(0, value); } + } + private int freeSpaceProportion; + + /// + /// Gets or sets whether groups will be rebuild on this columns values when this column's header is clicked. + /// + /// + /// This setting is only used when ShowGroups is true. + /// + /// If this is false, clicking the header will not rebuild groups. It will not provide + /// any feedback as to why the list is not being regrouped. It is the programmers responsibility to + /// provide appropriate feedback. + /// + /// When this is false, BeforeCreatingGroups events are still fired, which can be used to allow grouping + /// or give feedback, on a case by case basis. + /// + [Category("ObjectListView"), + Description("Will the list create groups when this header is clicked?"), + DefaultValue(true)] + public bool Groupable { + get { return groupable; } + set { groupable = value; } + } + private bool groupable = true; + + /// + /// This delegate is called when a group has been created but not yet made + /// into a real ListViewGroup. The user can take this opportunity to fill + /// in lots of other details about the group. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public GroupFormatterDelegate GroupFormatter { + get { return groupFormatter; } + set { groupFormatter = value; } + } + private GroupFormatterDelegate groupFormatter; + + /// + /// This delegate is called to get the object that is the key for the group + /// to which the given row belongs. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public GroupKeyGetterDelegate GroupKeyGetter { + get { return groupKeyGetter; } + set { groupKeyGetter = value; } + } + private GroupKeyGetterDelegate groupKeyGetter; + + /// + /// This delegate is called to convert a group key into a title for that group. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public GroupKeyToTitleConverterDelegate GroupKeyToTitleConverter { + get { return groupKeyToTitleConverter; } + set { groupKeyToTitleConverter = value; } + } + private GroupKeyToTitleConverterDelegate groupKeyToTitleConverter; + + /// + /// When the listview is grouped by this column and group title has an item count, + /// how should the label be formatted? + /// + /// + /// The given format string can/should have two placeholders: + /// + /// {0} - the original group title + /// {1} - the number of items in the group + /// + /// + /// "{0} [{1} items]" + [Category("ObjectListView"), + Description("The format to use when suffixing item counts to group titles"), + DefaultValue(null), + Localizable(true)] + public string GroupWithItemCountFormat { + get { return groupWithItemCountFormat; } + set { groupWithItemCountFormat = value; } + } + private string groupWithItemCountFormat; + + /// + /// Gets this.GroupWithItemCountFormat or a reasonable default + /// + /// + /// If GroupWithItemCountFormat is not set, its value will be taken from the ObjectListView if possible. + /// + [Browsable(false)] + public string GroupWithItemCountFormatOrDefault { + get { + if (!String.IsNullOrEmpty(this.GroupWithItemCountFormat)) + return this.GroupWithItemCountFormat; + + if (this.ListView != null) { + cachedGroupWithItemCountFormat = ((ObjectListView)this.ListView).GroupWithItemCountFormatOrDefault; + return cachedGroupWithItemCountFormat; + } + + // There is one rare but pathologically possible case where the ListView can + // be null (if the column is grouping a ListView, but is not one of the columns + // for that ListView) so we have to provide a workable default for that rare case. + return cachedGroupWithItemCountFormat ?? "{0} [{1} items]"; + } + } + private string cachedGroupWithItemCountFormat; + + /// + /// When the listview is grouped by this column and a group title has an item count, + /// how should the label be formatted if there is only one item in the group? + /// + /// + /// The given format string can/should have two placeholders: + /// + /// {0} - the original group title + /// {1} - the number of items in the group (always 1) + /// + /// + /// "{0} [{1} item]" + [Category("ObjectListView"), + Description("The format to use when suffixing item counts to group titles"), + DefaultValue(null), + Localizable(true)] + public string GroupWithItemCountSingularFormat { + get { return groupWithItemCountSingularFormat; } + set { groupWithItemCountSingularFormat = value; } + } + private string groupWithItemCountSingularFormat; + + /// + /// Get this.GroupWithItemCountSingularFormat or a reasonable default + /// + /// + /// If this value is not set, the values from the list view will be used + /// + [Browsable(false)] + public string GroupWithItemCountSingularFormatOrDefault { + get { + if (!String.IsNullOrEmpty(this.GroupWithItemCountSingularFormat)) + return this.GroupWithItemCountSingularFormat; + + if (this.ListView != null) { + cachedGroupWithItemCountSingularFormat = ((ObjectListView)this.ListView).GroupWithItemCountSingularFormatOrDefault; + return cachedGroupWithItemCountSingularFormat; + } + + // There is one rare but pathologically possible case where the ListView can + // be null (if the column is grouping a ListView, but is not one of the columns + // for that ListView) so we have to provide a workable default for that rare case. + return cachedGroupWithItemCountSingularFormat ?? "{0} [{1} item]"; + } + } + private string cachedGroupWithItemCountSingularFormat; + + /// + /// Gets whether this column should be drawn with a filter indicator in the column header. + /// + [Browsable(false)] + public bool HasFilterIndicator { + get { + return this.UseFiltering && this.ValuesChosenForFiltering != null && this.ValuesChosenForFiltering.Count > 0; + } + } + + /// + /// Gets or sets a delegate that will be used to own draw header column. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public HeaderDrawingDelegate HeaderDrawing { + get { return headerDrawing; } + set { headerDrawing = value; } + } + private HeaderDrawingDelegate headerDrawing; + + /// + /// Gets or sets the style that will be used to draw the header for this column + /// + /// This is only uses when the owning ObjectListView has HeaderUsesThemes set to false. + [Category("ObjectListView"), + Description("What style will be used to draw the header of this column"), + DefaultValue(null)] + public HeaderFormatStyle HeaderFormatStyle { + get { return this.headerFormatStyle; } + set { this.headerFormatStyle = value; } + } + private HeaderFormatStyle headerFormatStyle; + + /// + /// Gets or sets the font in which the header for this column will be drawn + /// + /// You should probably use a HeaderFormatStyle instead of this property + /// This is only uses when HeaderUsesThemes is false. + [Category("ObjectListView"), + Description("Which font will be used to draw the header?"), + DefaultValue(null)] + public Font HeaderFont { + get { return this.HeaderFormatStyle == null ? null : this.HeaderFormatStyle.Normal.Font; } + set { + if (value == null && this.HeaderFormatStyle == null) + return; + + if (this.HeaderFormatStyle == null) + this.HeaderFormatStyle = new HeaderFormatStyle(); + + this.HeaderFormatStyle.SetFont(value); + } + } + + /// + /// Gets or sets the color in which the text of the header for this column will be drawn + /// + /// You should probably use a HeaderFormatStyle instead of this property + /// This is only uses when HeaderUsesThemes is false. + [Category("ObjectListView"), + Description("In what color will the header text be drawn?"), + DefaultValue(typeof(Color), "")] + public Color HeaderForeColor { + get { return this.HeaderFormatStyle == null ? Color.Empty : this.HeaderFormatStyle.Normal.ForeColor; } + set { + if (value.IsEmpty && this.HeaderFormatStyle == null) + return; + + if (this.HeaderFormatStyle == null) + this.HeaderFormatStyle = new HeaderFormatStyle(); + + this.HeaderFormatStyle.SetForeColor(value); + } + } + + /// + /// Gets or sets the ImageList key of the image that will be drawn in the header of this column. + /// + /// This is only taken into account when HeaderUsesThemes is false. + [Category("ObjectListView"), + Description("Name of the image that will be shown in the column header."), + DefaultValue(null), + TypeConverter(typeof(ImageKeyConverter)), + Editor("System.Windows.Forms.Design.ImageIndexEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor)), + RefreshProperties(RefreshProperties.Repaint)] + public string HeaderImageKey { + get { return headerImageKey; } + set { headerImageKey = value; } + } + private string headerImageKey; + + + /// + /// Gets or sets how the text of the header will be drawn? + /// + [Category("ObjectListView"), + Description("How will the header text be aligned? If this is not set, the alignment of the header will follow the alignment of the column"), + DefaultValue(null)] + public HorizontalAlignment? HeaderTextAlign { + get { return headerTextAlign; } + set { headerTextAlign = value; } + } + private HorizontalAlignment? headerTextAlign; + + /// + /// Return the text alignment of the header. This will either have been set explicitly, + /// or will follow the alignment of the text in the column + /// + [Browsable(false)] + public HorizontalAlignment HeaderTextAlignOrDefault + { + get { return headerTextAlign.HasValue ? headerTextAlign.Value : this.TextAlign; } + } + + /// + /// Gets the header alignment converted to a StringAlignment + /// + [Browsable(false)] + public StringAlignment HeaderTextAlignAsStringAlignment { + get { + switch (this.HeaderTextAlignOrDefault) { + case HorizontalAlignment.Left: return StringAlignment.Near; + case HorizontalAlignment.Center: return StringAlignment.Center; + case HorizontalAlignment.Right: return StringAlignment.Far; + default: return StringAlignment.Near; + } + } + } + + /// + /// Gets whether or not this column has an image in the header + /// + [Browsable(false)] + public bool HasHeaderImage { + get { + return (this.ListView != null && + this.ListView.SmallImageList != null && + this.ListView.SmallImageList.Images.ContainsKey(this.HeaderImageKey)); + } + } + + /// + /// Gets or sets whether this header will place a checkbox in the header + /// + [Category("ObjectListView"), + Description("Draw a checkbox in the header of this column"), + DefaultValue(false)] + public bool HeaderCheckBox + { + get { return headerCheckBox; } + set { headerCheckBox = value; } + } + private bool headerCheckBox; + + /// + /// Gets or sets whether this header will place a tri-state checkbox in the header + /// + [Category("ObjectListView"), + Description("Draw a tri-state checkbox in the header of this column"), + DefaultValue(false)] + public bool HeaderTriStateCheckBox + { + get { return headerTriStateCheckBox; } + set { headerTriStateCheckBox = value; } + } + private bool headerTriStateCheckBox; + + /// + /// Gets or sets the checkedness of the checkbox in the header of this column + /// + [Category("ObjectListView"), + Description("Checkedness of the header checkbox"), + DefaultValue(CheckState.Unchecked)] + public CheckState HeaderCheckState + { + get { return headerCheckState; } + set { headerCheckState = value; } + } + private CheckState headerCheckState = CheckState.Unchecked; + + /// + /// Gets or sets whether the + /// checking/unchecking the value of the header's checkbox will result in the + /// checkboxes for all cells in this column being set to the same checked/unchecked. + /// Defaults to true. + /// + /// + /// + /// There is no reverse of this function that automatically updates the header when the + /// checkedness of a cell changes. + /// + /// + /// This property's behaviour on a TreeListView is probably best describes as undefined + /// and should be avoided. + /// + /// + /// The performance of this action (checking/unchecking all rows) is O(n) where n is the + /// number of rows. It will work on large virtual lists, but it may take some time. + /// + /// + [Category("ObjectListView"), + Description("Update row checkboxes when the header checkbox is clicked by the user"), + DefaultValue(true)] + public bool HeaderCheckBoxUpdatesRowCheckBoxes { + get { return headerCheckBoxUpdatesRowCheckBoxes; } + set { headerCheckBoxUpdatesRowCheckBoxes = value; } + } + private bool headerCheckBoxUpdatesRowCheckBoxes = true; + + /// + /// Gets or sets whether the checkbox in the header is disabled + /// + /// + /// Clicking on a disabled checkbox does not change its value, though it does raise + /// a HeaderCheckBoxChanging event, which allows the programmer the opportunity to do + /// something appropriate. + [Category("ObjectListView"), + Description("Is the checkbox in the header of this column disabled"), + DefaultValue(false)] + public bool HeaderCheckBoxDisabled + { + get { return headerCheckBoxDisabled; } + set { headerCheckBoxDisabled = value; } + } + private bool headerCheckBoxDisabled; + + /// + /// Gets or sets whether this column can be hidden by the user. + /// + /// + /// Column 0 can never be hidden, regardless of this setting. + /// + [Category("ObjectListView"), + Description("Will the user be able to choose to hide this column?"), + DefaultValue(true)] + public bool Hideable { + get { return hideable; } + set { hideable = value; } + } + private bool hideable = true; + + /// + /// Gets or sets whether the text values in this column will act like hyperlinks + /// + [Category("ObjectListView"), + Description("Will the text values in the cells of this column act like hyperlinks?"), + DefaultValue(false)] + public bool Hyperlink { + get { return hyperlink; } + set { hyperlink = value; } + } + private bool hyperlink; + + /// + /// This is the name of property that will be invoked to get the image selector of the + /// image that should be shown in this column. + /// It can return an int, string, Image or null. + /// + /// + /// This is ignored if ImageGetter is not null. + /// The property can use these return value to identify the image: + /// + /// null or -1 -- indicates no image + /// an int -- the int value will be used as an index into the image list + /// a String -- the string value will be used as a key into the image list + /// an Image -- the Image will be drawn directly (only in OwnerDrawn mode) + /// + /// + [Category("ObjectListView"), + Description("The name of the property that holds the image selector"), + DefaultValue(null)] + public string ImageAspectName { + get { return imageAspectName; } + set { imageAspectName = value; } + } + private string imageAspectName; + + /// + /// This delegate is called to get the image selector of the image that should be shown in this column. + /// It can return an int, string, Image or null. + /// + /// This delegate can use these return value to identify the image: + /// + /// null or -1 -- indicates no image + /// an int -- the int value will be used as an index into the image list + /// a String -- the string value will be used as a key into the image list + /// an Image -- the Image will be drawn directly (only in OwnerDrawn mode) + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ImageGetterDelegate ImageGetter { + get { return imageGetter; } + set { imageGetter = value; } + } + private ImageGetterDelegate imageGetter; + + /// + /// Gets or sets whether this column will draw buttons in its cells + /// + /// + /// + /// When this is set to true, the renderer for the column is become a ColumnButtonRenderer + /// if it isn't already. If this is set to false, any previous button renderer will be discarded + /// + /// If the cell's aspect is null or empty, nothing will be drawn in the cell. + [Category("ObjectListView"), + Description("Does this column draw its cells as buttons?"), + DefaultValue(false)] + public bool IsButton { + get { return isButton; } + set { + isButton = value; + if (value) { + ColumnButtonRenderer buttonRenderer = this.Renderer as ColumnButtonRenderer; + if (buttonRenderer == null) { + this.Renderer = this.CreateColumnButtonRenderer(); + this.FillInColumnButtonRenderer(); + } + } else { + if (this.Renderer is ColumnButtonRenderer) + this.Renderer = null; + } + } + } + private bool isButton; + + /// + /// Create a ColumnButtonRenderer to draw buttons in this column + /// + /// + protected virtual ColumnButtonRenderer CreateColumnButtonRenderer() { + return new ColumnButtonRenderer(); + } + + /// + /// Fill in details to our ColumnButtonRenderer based on the properties set on the column + /// + protected virtual void FillInColumnButtonRenderer() { + ColumnButtonRenderer buttonRenderer = this.Renderer as ColumnButtonRenderer; + if (buttonRenderer == null) + return; + + buttonRenderer.SizingMode = this.ButtonSizing; + buttonRenderer.ButtonSize = this.ButtonSize; + buttonRenderer.ButtonPadding = this.ButtonPadding; + buttonRenderer.MaxButtonWidth = this.ButtonMaxWidth; + } + + /// + /// Gets or sets the maximum width that a button can occupy. + /// -1 means there is no maximum width. + /// + /// This is only considered when the SizingMode is TextBounds + [Category("ObjectListView"), + Description("The maximum width that a button can occupy when the SizingMode is TextBounds"), + DefaultValue(-1)] + public int ButtonMaxWidth { + get { return this.buttonMaxWidth; } + set { + this.buttonMaxWidth = value; + FillInColumnButtonRenderer(); + } + } + private int buttonMaxWidth = -1; + + /// + /// Gets or sets the extra space that surrounds the cell when the SizingMode is TextBounds + /// + [Category("ObjectListView"), + Description("The extra space that surrounds the cell when the SizingMode is TextBounds"), + DefaultValue(null)] + public Size? ButtonPadding { + get { return this.buttonPadding; } + set { + this.buttonPadding = value; + this.FillInColumnButtonRenderer(); + } + } + private Size? buttonPadding; + + /// + /// Gets or sets the size of the button when the SizingMode is FixedBounds + /// + /// If this is not set, the bounds of the cell will be used + [Category("ObjectListView"), + Description("The size of the button when the SizingMode is FixedBounds"), + DefaultValue(null)] + public Size? ButtonSize { + get { return this.buttonSize; } + set { + this.buttonSize = value; + this.FillInColumnButtonRenderer(); + } + } + private Size? buttonSize; + + /// + /// Gets or sets how each button will be sized if this column is displaying buttons + /// + [Category("ObjectListView"), + Description("If this column is showing buttons, how each button will be sized"), + DefaultValue(ButtonSizingMode.TextBounds)] + public ButtonSizingMode ButtonSizing { + get { return this.buttonSizing; } + set { + this.buttonSizing = value; + this.FillInColumnButtonRenderer(); + } + } + private ButtonSizingMode buttonSizing = ButtonSizingMode.TextBounds; + + /// + /// Can the values shown in this column be edited? + /// + /// This defaults to true, since the primary means to control the editability of a listview + /// is on the listview itself. Once a listview is editable, all the columns are too, unless the + /// programmer explicitly marks them as not editable + [Category("ObjectListView"), + Description("Can the value in this column be edited?"), + DefaultValue(true)] + public bool IsEditable + { + get { return isEditable; } + set { isEditable = value; } + } + private bool isEditable = true; + + /// + /// Is this column a fixed width column? + /// + [Browsable(false)] + public bool IsFixedWidth { + get { + return (this.MinimumWidth != -1 && this.MaximumWidth != -1 && this.MinimumWidth >= this.MaximumWidth); + } + } + + /// + /// Get/set whether this column should be used when the view is switched to tile view. + /// + /// Column 0 is always included in tileview regardless of this setting. + /// Tile views do not work well with many "columns" of information. + /// Two or three works best. + [Category("ObjectListView"), + Description("Will this column be used when the view is switched to tile view"), + DefaultValue(false)] + public bool IsTileViewColumn { + get { return isTileViewColumn; } + set { isTileViewColumn = value; } + } + private bool isTileViewColumn; + + /// + /// Gets or sets whether the text of this header should be rendered vertically. + /// + /// + /// If this is true, it is a good idea to set ToolTipText to the name of the column so it's easy to read. + /// Vertical headers are text only. They do not draw their image. + /// + [Category("ObjectListView"), + Description("Will the header for this column be drawn vertically?"), + DefaultValue(false)] + public bool IsHeaderVertical { + get { return isHeaderVertical; } + set { isHeaderVertical = value; } + } + private bool isHeaderVertical; + + /// + /// Can this column be seen by the user? + /// + /// After changing this value, you must call RebuildColumns() before the changes will take effect. + [Category("ObjectListView"), + Description("Can this column be seen by the user?"), + DefaultValue(true)] + public bool IsVisible { + get { return isVisible; } + set + { + if (isVisible == value) + return; + + isVisible = value; + OnVisibilityChanged(EventArgs.Empty); + } + } + private bool isVisible = true; + + /// + /// Where was this column last positioned within the Detail view columns + /// + /// DisplayIndex is volatile. Once a column is removed from the control, + /// there is no way to discover where it was in the display order. This property + /// guards that information even when the column is not in the listview's active columns. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int LastDisplayIndex { + get { return this.lastDisplayIndex; } + set { this.lastDisplayIndex = value; } + } + private int lastDisplayIndex = -1; + + /// + /// What is the maximum width that the user can give to this column? + /// + /// -1 means there is no maximum width. Give this the same value as MinimumWidth to make a fixed width column. + [Category("ObjectListView"), + Description("What is the maximum width to which the user can resize this column? -1 means no limit"), + DefaultValue(-1)] + public int MaximumWidth { + get { return maxWidth; } + set { + maxWidth = value; + if (maxWidth != -1 && this.Width > maxWidth) + this.Width = maxWidth; + } + } + private int maxWidth = -1; + + /// + /// What is the minimum width that the user can give to this column? + /// + /// -1 means there is no minimum width. Give this the same value as MaximumWidth to make a fixed width column. + [Category("ObjectListView"), + Description("What is the minimum width to which the user can resize this column? -1 means no limit"), + DefaultValue(-1)] + public int MinimumWidth { + get { return minWidth; } + set { + minWidth = value; + if (this.Width < minWidth) + this.Width = minWidth; + } + } + private int minWidth = -1; + + /// + /// Get/set the renderer that will be invoked when a cell needs to be redrawn + /// + [Category("ObjectListView"), + Description("The renderer will draw this column when the ListView is owner drawn"), + DefaultValue(null)] + public IRenderer Renderer { + get { return renderer; } + set { renderer = value; } + } + private IRenderer renderer; + + /// + /// This delegate is called when a cell needs to be drawn in OwnerDrawn mode. + /// + /// This method is kept primarily for backwards compatibility. + /// New code should implement an IRenderer, though this property will be maintained. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public RenderDelegate RendererDelegate { + get { + Version1Renderer version1Renderer = this.Renderer as Version1Renderer; + return version1Renderer != null ? version1Renderer.RenderDelegate : null; + } + set { + this.Renderer = value == null ? null : new Version1Renderer(value); + } + } + + /// + /// Gets or sets whether the text in this column's cell will be used when doing text searching. + /// + /// + /// + /// If this is false, text filters will not trying searching this columns cells when looking for matches. + /// + /// + [Category("ObjectListView"), + Description("Will the text of the cells in this column be considered when searching?"), + DefaultValue(true)] + public bool Searchable { + get { return searchable; } + set { searchable = value; } + } + private bool searchable = true; + + /// + /// Gets or sets a delegate which will return the array of text values that should be + /// considered for text matching when using a text based filter. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public SearchValueGetterDelegate SearchValueGetter { + get { return searchValueGetter; } + set { searchValueGetter = value; } + } + private SearchValueGetterDelegate searchValueGetter; + + /// + /// Gets or sets whether the header for this column will include the column's Text. + /// + /// + /// + /// If this is false, the only thing rendered in the column header will be the image from . + /// + /// This setting is only considered when is false on the owning ObjectListView. + /// + [Category("ObjectListView"), + Description("Will the header for this column include text?"), + DefaultValue(true)] + public bool ShowTextInHeader { + get { return showTextInHeader; } + set { showTextInHeader = value; } + } + private bool showTextInHeader = true; + + /// + /// Gets or sets whether the contents of the list will be resorted when the user clicks the + /// header of this column. + /// + /// + /// + /// If this is false, clicking the header will not sort the list, but will not provide + /// any feedback as to why the list is not being sorted. It is the programmers responsibility to + /// provide appropriate feedback. + /// + /// When this is false, BeforeSorting events are still fired, which can be used to allow sorting + /// or give feedback, on a case by case basis. + /// + [Category("ObjectListView"), + Description("Will clicking this columns header resort the list?"), + DefaultValue(true)] + public bool Sortable { + get { return sortable; } + set { sortable = value; } + } + private bool sortable = true; + + /// + /// Gets or sets the horizontal alignment of the contents of the column. + /// + /// .NET will not allow column 0 to have any alignment except + /// to the left. We can't change the basic behaviour of the listview, + /// but when owner drawn, column 0 can now have other alignments. + new public HorizontalAlignment TextAlign { + get { + return this.textAlign.HasValue ? this.textAlign.Value : base.TextAlign; + } + set { + this.textAlign = value; + base.TextAlign = value; + } + } + private HorizontalAlignment? textAlign; + + /// + /// Gets the StringAlignment equivalent of the column text alignment + /// + [Browsable(false)] + public StringAlignment TextStringAlign { + get { + switch (this.TextAlign) { + case HorizontalAlignment.Center: + return StringAlignment.Center; + case HorizontalAlignment.Left: + return StringAlignment.Near; + case HorizontalAlignment.Right: + return StringAlignment.Far; + default: + return StringAlignment.Near; + } + } + } + + /// + /// What string should be displayed when the mouse is hovered over the header of this column? + /// + /// If a HeaderToolTipGetter is installed on the owning ObjectListView, this + /// value will be ignored. + [Category("ObjectListView"), + Description("The tooltip to show when the mouse is hovered over the header of this column"), + DefaultValue((String)null), + Localizable(true)] + public String ToolTipText { + get { return toolTipText; } + set { toolTipText = value; } + } + private String toolTipText; + + /// + /// Should this column have a tri-state checkbox? + /// + /// + /// If this is true, the user can choose the third state (normally Indeterminate). + /// + [Category("ObjectListView"), + Description("Should values in this column be treated as a tri-state checkbox?"), + DefaultValue(false)] + public virtual bool TriStateCheckBoxes { + get { return triStateCheckBoxes; } + set { + triStateCheckBoxes = value; + if (value && !this.CheckBoxes) + this.CheckBoxes = true; + } + } + private bool triStateCheckBoxes; + + /// + /// Group objects by the initial letter of the aspect of the column + /// + /// + /// One common pattern is to group column by the initial letter of the value for that group. + /// The aspect must be a string (obviously). + /// + [Category("ObjectListView"), + Description("The name of the property or method that should be called to get the aspect to display in this column"), + DefaultValue(false)] + public bool UseInitialLetterForGroup { + get { return useInitialLetterForGroup; } + set { useInitialLetterForGroup = value; } + } + private bool useInitialLetterForGroup; + + /// + /// Gets or sets whether or not this column should be user filterable + /// + [Category("ObjectListView"), + Description("Does this column want to show a Filter menu item when its header is right clicked"), + DefaultValue(true)] + public bool UseFiltering { + get { return useFiltering; } + set { useFiltering = value; } + } + private bool useFiltering = true; + + /// + /// Gets or sets a filter that will only include models where the model's value + /// for this column is one of the values in ValuesChosenForFiltering + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IModelFilter ValueBasedFilter { + get { + if (!this.UseFiltering) + return null; + + if (valueBasedFilter != null) + return valueBasedFilter; + + if (this.ClusteringStrategy == null) + return null; + + if (this.ValuesChosenForFiltering == null || this.ValuesChosenForFiltering.Count == 0) + return null; + + return this.ClusteringStrategy.CreateFilter(this.ValuesChosenForFiltering); + } + set { valueBasedFilter = value; } + } + private IModelFilter valueBasedFilter; + + /// + /// Gets or sets the values that will be used to generate a filter for this + /// column. For a model to be included by the generated filter, its value for this column + /// must be in this list. If the list is null or empty, this column will + /// not be used for filtering. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IList ValuesChosenForFiltering { + get { return this.valuesChosenForFiltering; } + set { this.valuesChosenForFiltering = value; } + } + private IList valuesChosenForFiltering = new ArrayList(); + + /// + /// What is the width of this column? + /// + [Category("ObjectListView"), + Description("The width in pixels of this column"), + DefaultValue(60)] + public new int Width { + get { return base.Width; } + set { + if (this.MaximumWidth != -1 && value > this.MaximumWidth) + base.Width = this.MaximumWidth; + else + base.Width = Math.Max(this.MinimumWidth, value); + } + } + + /// + /// Gets or set whether the contents of this column's cells should be word wrapped + /// + /// If this column uses a custom IRenderer (that is, one that is not descended + /// from BaseRenderer), then that renderer is responsible for implementing word wrapping. + [Category("ObjectListView"), + Description("Draw this column cell's word wrapped"), + DefaultValue(false)] + public bool WordWrap { + get { return wordWrap; } + set { wordWrap = value; } + } + + private bool wordWrap; + + #endregion + + #region Object commands + + /// + /// For a given group value, return the string that should be used as the groups title. + /// + /// The group key that is being converted to a title + /// string + public string ConvertGroupKeyToTitle(object value) { + if (this.groupKeyToTitleConverter != null) + return this.groupKeyToTitleConverter(value); + + return value == null ? ObjectListView.GroupTitleDefault : this.ValueToString(value); + } + + /// + /// Get the checkedness of the given object for this column + /// + /// The row object that is being displayed + /// The checkedness of the object + public CheckState GetCheckState(object rowObject) { + if (!this.CheckBoxes) + return CheckState.Unchecked; + + bool? aspectAsBool = this.GetValue(rowObject) as bool?; + if (aspectAsBool.HasValue) { + if (aspectAsBool.Value) + return CheckState.Checked; + else + return CheckState.Unchecked; + } else + return CheckState.Indeterminate; + } + + /// + /// Put the checkedness of the given object for this column + /// + /// The row object that is being displayed + /// + /// The checkedness of the object + public void PutCheckState(object rowObject, CheckState newState) { + if (newState == CheckState.Checked) + this.PutValue(rowObject, true); + else + if (newState == CheckState.Unchecked) + this.PutValue(rowObject, false); + else + this.PutValue(rowObject, null); + } + + /// + /// For a given row object, extract the value indicated by the AspectName property of this column. + /// + /// The row object that is being displayed + /// An object, which is the aspect named by AspectName + public object GetAspectByName(object rowObject) { + if (this.aspectMunger == null) + this.aspectMunger = new Munger(this.AspectName); + + return this.aspectMunger.GetValue(rowObject); + } + private Munger aspectMunger; + + /// + /// For a given row object, return the object that is the key of the group that this row belongs to. + /// + /// The row object that is being displayed + /// Group key object + public object GetGroupKey(object rowObject) { + if (this.groupKeyGetter != null) + return this.groupKeyGetter(rowObject); + + object key = this.GetValue(rowObject); + + if (this.UseInitialLetterForGroup) { + String keyAsString = key as String; + if (!String.IsNullOrEmpty(keyAsString)) + return keyAsString.Substring(0, 1).ToUpper(); + } + + return key; + } + + /// + /// For a given row object, return the image selector of the image that should displayed in this column. + /// + /// The row object that is being displayed + /// int or string or Image. int or string will be used as index into image list. null or -1 means no image + public Object GetImage(object rowObject) { + if (this.CheckBoxes) + return this.GetCheckStateImage(rowObject); + + if (this.ImageGetter != null) + return this.ImageGetter(rowObject); + + if (!String.IsNullOrEmpty(this.ImageAspectName)) { + if (this.imageAspectMunger == null) + this.imageAspectMunger = new Munger(this.ImageAspectName); + + return this.imageAspectMunger.GetValue(rowObject); + } + + // I think this is wrong. ImageKey is meant for the image in the header, not in the rows + if (!String.IsNullOrEmpty(this.ImageKey)) + return this.ImageKey; + + return this.ImageIndex; + } + private Munger imageAspectMunger; + + /// + /// Return the image that represents the check box for the given model + /// + /// + /// + public string GetCheckStateImage(Object rowObject) { + CheckState checkState = this.GetCheckState(rowObject); + + if (checkState == CheckState.Checked) + return ObjectListView.CHECKED_KEY; + + if (checkState == CheckState.Unchecked) + return ObjectListView.UNCHECKED_KEY; + + return ObjectListView.INDETERMINATE_KEY; + } + + /// + /// For a given row object, return the strings that will be searched when trying to filter by string. + /// + /// + /// This will normally be the simple GetStringValue result, but if this column is non-textual (e.g. image) + /// you might want to install a SearchValueGetter delegate which can return something that could be used + /// for text filtering. + /// + /// + /// The array of texts to be searched. If this returns null, search will not match that object. + public string[] GetSearchValues(object rowObject) { + if (this.SearchValueGetter != null) + return this.SearchValueGetter(rowObject); + + var stringValue = this.GetStringValue(rowObject); + + DescribedTaskRenderer dtr = this.Renderer as DescribedTaskRenderer; + if (dtr != null) { + return new string[] { stringValue, dtr.GetDescription(rowObject) }; + } + + return new string[] { stringValue }; + } + + /// + /// For a given row object, return the string representation of the value shown in this column. + /// + /// + /// For aspects that are string (e.g. aPerson.Name), the aspect and its string representation are the same. + /// For non-strings (e.g. aPerson.DateOfBirth), the string representation is very different. + /// + /// + /// + public string GetStringValue(object rowObject) + { + return this.ValueToString(this.GetValue(rowObject)); + } + + /// + /// For a given row object, return the object that is to be displayed in this column. + /// + /// The row object that is being displayed + /// An object, which is the aspect to be displayed + public object GetValue(object rowObject) { + if (this.AspectGetter == null) + return this.GetAspectByName(rowObject); + else + return this.AspectGetter(rowObject); + } + + /// + /// Update the given model object with the given value using the column's + /// AspectName. + /// + /// The model object to be updated + /// The value to be put into the model + public void PutAspectByName(Object rowObject, Object newValue) { + if (this.aspectMunger == null) + this.aspectMunger = new Munger(this.AspectName); + + this.aspectMunger.PutValue(rowObject, newValue); + } + + /// + /// Update the given model object with the given value + /// + /// The model object to be updated + /// The value to be put into the model + public void PutValue(Object rowObject, Object newValue) { + if (this.aspectPutter == null) + this.PutAspectByName(rowObject, newValue); + else + this.aspectPutter(rowObject, newValue); + } + + /// + /// Convert the aspect object to its string representation. + /// + /// + /// If the column has been given a AspectToStringConverter, that will be used to do + /// the conversion, otherwise just use ToString(). + /// The returned value will not be null. Nulls are always converted + /// to empty strings. + /// + /// The value of the aspect that should be displayed + /// A string representation of the aspect + public string ValueToString(object value) { + // Give the installed converter a chance to work (even if the value is null) + if (this.AspectToStringConverter != null) + return this.AspectToStringConverter(value) ?? String.Empty; + + // Without a converter, nulls become simple empty strings + if (value == null) + return String.Empty; + + string fmt = this.AspectToStringFormat; + if (String.IsNullOrEmpty(fmt)) + return value.ToString(); + else + return String.Format(fmt, value); + } + + #endregion + + #region Utilities + + /// + /// Decide the clustering strategy that will be used for this column + /// + /// + private IClusteringStrategy DecideDefaultClusteringStrategy() { + if (!this.UseFiltering) + return null; + + if (this.DataType == typeof(DateTime)) + return new DateTimeClusteringStrategy(); + + return new ClustersFromGroupsStrategy(); + } + + /// + /// Gets or sets the type of data shown in this column. + /// + /// If this is not set, it will try to get the type + /// by looking through the rows of the listview. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Type DataType { + get { + if (this.dataType == null) { + ObjectListView olv = this.ListView as ObjectListView; + if (olv != null) { + object value = olv.GetFirstNonNullValue(this); + if (value != null) + return value.GetType(); // THINK: Should we cache this? + } + } + return this.dataType; + } + set { + this.dataType = value; + } + } + private Type dataType; + + #region Events + + /// + /// This event is triggered when the visibility of this column changes. + /// + [Category("ObjectListView"), + Description("This event is triggered when the visibility of the column changes.")] + public event EventHandler VisibilityChanged; + + /// + /// Tell the world when visibility of a column changes. + /// + public virtual void OnVisibilityChanged(EventArgs e) + { + if (this.VisibilityChanged != null) + this.VisibilityChanged(this, e); + } + + #endregion + + /// + /// Create groupies + /// This is an untyped version to help with Generator and OLVColumn attributes + /// + /// + /// + public void MakeGroupies(object[] values, string[] descriptions) { + this.MakeGroupies(values, descriptions, null, null, null); + } + + /// + /// Create groupies + /// + /// + /// + /// + public void MakeGroupies(T[] values, string[] descriptions) { + this.MakeGroupies(values, descriptions, null, null, null); + } + + /// + /// Create groupies + /// + /// + /// + /// + /// + public void MakeGroupies(T[] values, string[] descriptions, object[] images) { + this.MakeGroupies(values, descriptions, images, null, null); + } + + /// + /// Create groupies + /// + /// + /// + /// + /// + /// + public void MakeGroupies(T[] values, string[] descriptions, object[] images, string[] subtitles) { + this.MakeGroupies(values, descriptions, images, subtitles, null); + } + + /// + /// Create groupies. + /// Install delegates that will group the columns aspects into progressive partitions. + /// If an aspect is less than value[n], it will be grouped with description[n]. + /// If an aspect has a value greater than the last element in "values", it will be grouped + /// with the last element in "descriptions". + /// + /// Array of values. Values must be able to be + /// compared to the aspect (using IComparable) + /// The description for the matching value. The last element is the default description. + /// If there are n values, there must be n+1 descriptions. + /// + /// this.salaryColumn.MakeGroupies( + /// new UInt32[] { 20000, 100000 }, + /// new string[] { "Lowly worker", "Middle management", "Rarified elevation"}); + /// + /// + /// + /// + /// + public void MakeGroupies(T[] values, string[] descriptions, object[] images, string[] subtitles, string[] tasks) { + // Sanity checks + if (values == null) + throw new ArgumentNullException("values"); + if (descriptions == null) + throw new ArgumentNullException("descriptions"); + if (values.Length + 1 != descriptions.Length) + throw new ArgumentException("descriptions must have one more element than values."); + + // Install a delegate that returns the index of the description to be shown + this.GroupKeyGetter = delegate(object row) { + Object aspect = this.GetValue(row); + if (aspect == null || aspect == DBNull.Value) + return -1; + IComparable comparable = (IComparable)aspect; + for (int i = 0; i < values.Length; i++) { + if (comparable.CompareTo(values[i]) < 0) + return i; + } + + // Display the last element in the array + return descriptions.Length - 1; + }; + + // Install a delegate that simply looks up the given index in the descriptions. + this.GroupKeyToTitleConverter = delegate(object key) { + if ((int)key < 0) + return ""; + + return descriptions[(int)key]; + }; + + // Install one delegate that does all the other formatting + this.GroupFormatter = delegate(OLVGroup group, GroupingParameters parms) { + int key = (int)group.Key; // we know this is an int since we created it in GroupKeyGetter + + if (key >= 0) { + if (images != null && key < images.Length) + group.TitleImage = images[key]; + + if (subtitles != null && key < subtitles.Length) + group.Subtitle = subtitles[key]; + + if (tasks != null && key < tasks.Length) + group.Task = tasks[key]; + } + }; + } + /// + /// Create groupies based on exact value matches. + /// + /// + /// Install delegates that will group rows into partitions based on equality of this columns aspects. + /// If an aspect is equal to value[n], it will be grouped with description[n]. + /// If an aspect is not equal to any value, it will be grouped with "[other]". + /// + /// Array of values. Values must be able to be + /// equated to the aspect + /// The description for the matching value. + /// + /// this.marriedColumn.MakeEqualGroupies( + /// new MaritalStatus[] { MaritalStatus.Single, MaritalStatus.Married, MaritalStatus.Divorced, MaritalStatus.Partnered }, + /// new string[] { "Looking", "Content", "Looking again", "Mostly content" }); + /// + /// + /// + /// + /// + public void MakeEqualGroupies(T[] values, string[] descriptions, object[] images, string[] subtitles, string[] tasks) { + // Sanity checks + if (values == null) + throw new ArgumentNullException("values"); + if (descriptions == null) + throw new ArgumentNullException("descriptions"); + if (values.Length != descriptions.Length) + throw new ArgumentException("descriptions must have the same number of elements as values."); + + ArrayList valuesArray = new ArrayList(values); + + // Install a delegate that returns the index of the description to be shown + this.GroupKeyGetter = delegate(object row) { + return valuesArray.IndexOf(this.GetValue(row)); + }; + + // Install a delegate that simply looks up the given index in the descriptions. + this.GroupKeyToTitleConverter = delegate(object key) { + int intKey = (int)key; // we know this is an int since we created it in GroupKeyGetter + return (intKey < 0) ? "[other]" : descriptions[intKey]; + }; + + // Install one delegate that does all the other formatting + this.GroupFormatter = delegate(OLVGroup group, GroupingParameters parms) { + int key = (int)group.Key; // we know this is an int since we created it in GroupKeyGetter + + if (key >= 0) { + if (images != null && key < images.Length) + group.TitleImage = images[key]; + + if (subtitles != null && key < subtitles.Length) + group.Subtitle = subtitles[key]; + + if (tasks != null && key < tasks.Length) + group.Task = tasks[key]; + } + }; + } + + #endregion + } +} diff --git a/ObjectListView/ObjectListView.DesignTime.cs b/ObjectListView/ObjectListView.DesignTime.cs new file mode 100644 index 00000000..2fac113e --- /dev/null +++ b/ObjectListView/ObjectListView.DesignTime.cs @@ -0,0 +1,550 @@ +/* + * DesignSupport - Design time support for the various classes within ObjectListView + * + * Author: Phillip Piper + * Date: 12/08/2009 8:36 PM + * + * Change log: + * 2012-08-27 JPP - Fall back to more specific type name for the ListViewDesigner if + * the first GetType() fails. + * v2.5.1 + * 2012-04-26 JPP - Filter group events from TreeListView since it can't have groups + * 2011-06-06 JPP - Vastly improved ObjectListViewDesigner, based off information in + * "'Inheriting' from an Internal WinForms Designer" on CodeProject. + * v2.3 + * 2009-08-12 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Design; +using System.Diagnostics; +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; +using System.Windows.Forms.Design; + +namespace BrightIdeasSoftware.Design +{ + + /// + /// Designer for and its subclasses. + /// + /// + /// + /// This designer removes properties and events that are available on ListView but that are not + /// useful on ObjectListView. + /// + /// + /// We can't inherit from System.Windows.Forms.Design.ListViewDesigner, since it is marked internal. + /// So, this class uses reflection to create a ListViewDesigner and then forwards messages to that designer. + /// + /// + public class ObjectListViewDesigner : ControlDesigner + { + + #region Initialize & Dispose + + /// + /// Initialises the designer with the specified component. + /// + /// The to associate the designer with. This component must always be an instance of, or derive from, . + public override void Initialize(IComponent component) { + // Debug.WriteLine("ObjectListViewDesigner.Initialize"); + + // Use reflection to bypass the "internal" marker on ListViewDesigner + // If we can't get the unversioned designer, look specifically for .NET 4.0 version of it. + Type tListViewDesigner = Type.GetType("System.Windows.Forms.Design.ListViewDesigner, System.Design") ?? + Type.GetType("System.Windows.Forms.Design.ListViewDesigner, System.Design, " + + "Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + if (tListViewDesigner == null) throw new ArgumentException("Could not load ListViewDesigner"); + + this.listViewDesigner = (ControlDesigner)Activator.CreateInstance(tListViewDesigner, BindingFlags.Instance | BindingFlags.Public, null, null, null); + this.designerFilter = this.listViewDesigner; + + // Fetch the methods from the ListViewDesigner that we know we want to use + this.listViewDesignGetHitTest = tListViewDesigner.GetMethod("GetHitTest", BindingFlags.Instance | BindingFlags.NonPublic); + this.listViewDesignWndProc = tListViewDesigner.GetMethod("WndProc", BindingFlags.Instance | BindingFlags.NonPublic); + + Debug.Assert(this.listViewDesignGetHitTest != null, "Required method (GetHitTest) not found on ListViewDesigner"); + Debug.Assert(this.listViewDesignWndProc != null, "Required method (WndProc) not found on ListViewDesigner"); + + // Tell the Designer to use properties of default designer as well as the properties of this class (do before base.Initialize) + TypeDescriptor.CreateAssociation(component, this.listViewDesigner); + + IServiceContainer site = (IServiceContainer)component.Site; + if (site != null && GetService(typeof(DesignerCommandSet)) == null) { + site.AddService(typeof(DesignerCommandSet), new CDDesignerCommandSet(this)); + } else { + Debug.Fail("site != null && GetService(typeof (DesignerCommandSet)) == null"); + } + + this.listViewDesigner.Initialize(component); + base.Initialize(component); + + RemoveDuplicateDockingActionList(); + } + + /// + /// Initialises a newly created component. + /// + /// A name/value dictionary of default values to apply to properties. May be null if no default values are specified. + public override void InitializeNewComponent(IDictionary defaultValues) { + // Debug.WriteLine("ObjectListViewDesigner.InitializeNewComponent"); + base.InitializeNewComponent(defaultValues); + this.listViewDesigner.InitializeNewComponent(defaultValues); + } + + /// + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) { + // Debug.WriteLine("ObjectListViewDesigner.Dispose"); + if (disposing) { + if (this.listViewDesigner != null) { + this.listViewDesigner.Dispose(); + // Normally we would now null out the designer, but this designer + // still has methods called AFTER it is disposed. + } + } + + base.Dispose(disposing); + } + + /// + /// Removes the duplicate DockingActionList added by this designer to the . + /// + /// + /// adds an internal DockingActionList : 'Dock/Undock in Parent Container'. + /// But the default designer has already added that action list. So we need to remove one. + /// + private void RemoveDuplicateDockingActionList() { + // This is a true hack -- in a class that is basically a huge hack itself. + // Reach into the bowel of our base class, get a private field, and use that fields value to + // remove an action from the designer. + // In ControlDesigner, there is "private DockingActionList dockingAction;" + // Don't you just love Reflector?! + FieldInfo fi = typeof(ControlDesigner).GetField("dockingAction", BindingFlags.Instance | BindingFlags.NonPublic); + if (fi != null) { + DesignerActionList dockingAction = (DesignerActionList)fi.GetValue(this); + if (dockingAction != null) { + DesignerActionService service = (DesignerActionService)GetService(typeof(DesignerActionService)); + if (service != null) { + service.Remove(this.Control, dockingAction); + } + } + } + } + + #endregion + + #region IDesignerFilter overrides + + /// + /// Adjusts the set of properties the component exposes through a . + /// + /// An containing the properties for the class of the component. + protected override void PreFilterProperties(IDictionary properties) { + // Debug.WriteLine("ObjectListViewDesigner.PreFilterProperties"); + + // Always call the base PreFilterProperties implementation + // before you modify the properties collection. + base.PreFilterProperties(properties); + + // Give the listviewdesigner a chance to filter the properties + // (though we already know it's not going to do anything) + this.designerFilter.PreFilterProperties(properties); + + // I'd like to just remove the redundant properties, but that would + // break backward compatibility. The deserialiser that handles the XXX.Designer.cs file + // works off the designer, so even if the property exists in the class, the deserialiser will + // throw an error if the associated designer actually removes that property. + // So we shadow the unwanted properties, and give the replacement properties + // non-browsable attributes so that they are hidden from the user + + List unwantedProperties = new List(new string[] { + "BackgroundImage", "BackgroundImageTiled", "HotTracking", "HoverSelection", + "LabelEdit", "VirtualListSize", "VirtualMode" }); + + // Also hid Tooltip properties, since giving a tooltip to the control through the IDE + // messes up the tooltip handling + foreach (string propertyName in properties.Keys) { + if (propertyName.StartsWith("ToolTip")) { + unwantedProperties.Add(propertyName); + } + } + + // If we are looking at a TreeListView, remove group related properties + // since TreeListViews can't show groups + if (this.Control is TreeListView) { + unwantedProperties.AddRange(new string[] { + "GroupImageList", "GroupWithItemCountFormat", "GroupWithItemCountSingularFormat", "HasCollapsibleGroups", + "SpaceBetweenGroups", "ShowGroups", "SortGroupItemsByPrimaryColumn", "ShowItemCountOnGroups" + }); + } + + // Shadow the unwanted properties, and give the replacement properties + // non-browsable attributes so that they are hidden from the user + foreach (string unwantedProperty in unwantedProperties) { + PropertyDescriptor propertyDesc = TypeDescriptor.CreateProperty( + typeof(ObjectListView), + (PropertyDescriptor)properties[unwantedProperty], + new BrowsableAttribute(false)); + properties[unwantedProperty] = propertyDesc; + } + } + + /// + /// Allows a designer to add to the set of events that it exposes through a . + /// + /// The events for the class of the component. + protected override void PreFilterEvents(IDictionary events) { + // Debug.WriteLine("ObjectListViewDesigner.PreFilterEvents"); + base.PreFilterEvents(events); + this.designerFilter.PreFilterEvents(events); + + // Remove the events that don't make sense for an ObjectListView. + // See PreFilterProperties() for why we do this dance rather than just remove the event. + List unwanted = new List(new string[] { + "AfterLabelEdit", + "BeforeLabelEdit", + "DrawColumnHeader", + "DrawItem", + "DrawSubItem", + "RetrieveVirtualItem", + "SearchForVirtualItem", + "VirtualItemsSelectionRangeChanged" + }); + + // If we are looking at a TreeListView, remove group related events + // since TreeListViews can't show groups + if (this.Control is TreeListView) { + unwanted.AddRange(new string[] { + "AboutToCreateGroups", + "AfterCreatingGroups", + "BeforeCreatingGroups", + "GroupTaskClicked", + "GroupExpandingCollapsing", + "GroupStateChanged" + }); + } + + foreach (string unwantedEvent in unwanted) { + EventDescriptor eventDesc = TypeDescriptor.CreateEvent( + typeof(ObjectListView), + (EventDescriptor)events[unwantedEvent], + new BrowsableAttribute(false)); + events[unwantedEvent] = eventDesc; + } + } + + /// + /// Allows a designer to change or remove items from the set of attributes that it exposes through a . + /// + /// The attributes for the class of the component. + protected override void PostFilterAttributes(IDictionary attributes) { + // Debug.WriteLine("ObjectListViewDesigner.PostFilterAttributes"); + this.designerFilter.PostFilterAttributes(attributes); + base.PostFilterAttributes(attributes); + } + + /// + /// Allows a designer to change or remove items from the set of events that it exposes through a . + /// + /// The events for the class of the component. + protected override void PostFilterEvents(IDictionary events) { + // Debug.WriteLine("ObjectListViewDesigner.PostFilterEvents"); + this.designerFilter.PostFilterEvents(events); + base.PostFilterEvents(events); + } + + #endregion + + #region Overrides + + /// + /// Gets the design-time action lists supported by the component associated with the designer. + /// + /// + /// The design-time action lists supported by the component associated with the designer. + /// + public override DesignerActionListCollection ActionLists { + get { + // We want to change the first action list so it only has the commands we want + DesignerActionListCollection actionLists = this.listViewDesigner.ActionLists; + if (actionLists.Count > 0 && !(actionLists[0] is ListViewActionListAdapter)) { + actionLists[0] = new ListViewActionListAdapter(this, actionLists[0]); + } + return actionLists; + } + } + + /// + /// Gets the collection of components associated with the component managed by the designer. + /// + /// + /// The components that are associated with the component managed by the designer. + /// + public override ICollection AssociatedComponents { + get { + ArrayList components = new ArrayList(base.AssociatedComponents); + components.AddRange(this.listViewDesigner.AssociatedComponents); + return components; + } + } + + /// + /// Indicates whether a mouse click at the specified point should be handled by the control. + /// + /// + /// true if a click at the specified point is to be handled by the control; otherwise, false. + /// + /// A indicating the position at which the mouse was clicked, in screen coordinates. + protected override bool GetHitTest(Point point) { + // The ListViewDesigner wants to allow column dividers to be resized + return (bool)this.listViewDesignGetHitTest.Invoke(listViewDesigner, new object[] { point }); + } + + /// + /// Processes Windows messages and optionally routes them to the control. + /// + /// The to process. + protected override void WndProc(ref Message m) { + switch (m.Msg) { + case 0x4e: + case 0x204e: + // The listview designer is interested in HDN_ENDTRACK notifications + this.listViewDesignWndProc.Invoke(listViewDesigner, new object[] { m }); + break; + default: + base.WndProc(ref m); + break; + } + } + + #endregion + + #region Implementation variables + + private ControlDesigner listViewDesigner; + private IDesignerFilter designerFilter; + private MethodInfo listViewDesignGetHitTest; + private MethodInfo listViewDesignWndProc; + + #endregion + + #region Custom action list + + /// + /// This class modifies a ListViewActionList, by removing the "Edit Items" and "Edit Groups" actions. + /// + /// + /// + /// That class is internal, so we cannot simply subclass it, which would be simpler. + /// + /// + /// Action lists use reflection to determine if that action can be executed, so we not + /// only have to modify the returned collection of actions, but we have to implement + /// the properties and commands that the returned actions use. + /// + private class ListViewActionListAdapter : DesignerActionList + { + public ListViewActionListAdapter(ObjectListViewDesigner designer, DesignerActionList wrappedList) + : base(wrappedList.Component) { + this.designer = designer; + this.wrappedList = wrappedList; + } + + public override DesignerActionItemCollection GetSortedActionItems() { + DesignerActionItemCollection items = wrappedList.GetSortedActionItems(); + items.RemoveAt(2); // remove Edit Groups + items.RemoveAt(0); // remove Edit Items + return items; + } + + private void EditValue(ComponentDesigner componentDesigner, IComponent iComponent, string propertyName) { + // One more complication. The ListViewActionList classes uses an internal class, EditorServiceContext, to + // edit the items/columns/groups collections. So, we use reflection to bypass the data hiding. + Type tEditorServiceContext = Type.GetType("System.Windows.Forms.Design.EditorServiceContext, System.Design"); + tEditorServiceContext.InvokeMember("EditValue", BindingFlags.InvokeMethod | BindingFlags.Static, null, null, new object[] { componentDesigner, iComponent, propertyName }); + } + + private void SetValue(object target, string propertyName, object value) { + TypeDescriptor.GetProperties(target)[propertyName].SetValue(target, value); + } + + public void InvokeColumnsDialog() { + EditValue(this.designer, base.Component, "Columns"); + } + + // Don't need these since we removed their corresponding actions from the list. + // Keep the methods just in case. + + //public void InvokeGroupsDialog() { + // EditValue(this.designer, base.Component, "Groups"); + //} + + //public void InvokeItemsDialog() { + // EditValue(this.designer, base.Component, "Items"); + //} + + public ImageList LargeImageList { + get { return ((ListView)base.Component).LargeImageList; } + set { SetValue(base.Component, "LargeImageList", value); } + } + + public ImageList SmallImageList { + get { return ((ListView)base.Component).SmallImageList; } + set { SetValue(base.Component, "SmallImageList", value); } + } + + public View View { + get { return ((ListView)base.Component).View; } + set { SetValue(base.Component, "View", value); } + } + + ObjectListViewDesigner designer; + DesignerActionList wrappedList; + } + + #endregion + + #region DesignerCommandSet + + private class CDDesignerCommandSet : DesignerCommandSet + { + + public CDDesignerCommandSet(ComponentDesigner componentDesigner) { + this.componentDesigner = componentDesigner; + } + + public override ICollection GetCommands(string name) { + // Debug.WriteLine("CDDesignerCommandSet.GetCommands:" + name); + if (componentDesigner != null) { + if (name.Equals("Verbs")) { + return componentDesigner.Verbs; + } + if (name.Equals("ActionLists")) { + return componentDesigner.ActionLists; + } + } + return base.GetCommands(name); + } + + private readonly ComponentDesigner componentDesigner; + } + + #endregion + } + + /// + /// This class works in conjunction with the OLVColumns property to allow OLVColumns + /// to be added to the ObjectListView. + /// + public class OLVColumnCollectionEditor : System.ComponentModel.Design.CollectionEditor + { + /// + /// Create a OLVColumnCollectionEditor + /// + /// + public OLVColumnCollectionEditor(Type t) + : base(t) { + } + + /// + /// What type of object does this editor create? + /// + /// + protected override Type CreateCollectionItemType() { + return typeof(OLVColumn); + } + + /// + /// Edit a given value + /// + /// + /// + /// + /// + public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { + if (context == null) + throw new ArgumentNullException("context"); + if (provider == null) + throw new ArgumentNullException("provider"); + + // Figure out which ObjectListView we are working on. This should be the Instance of the context. + ObjectListView olv = context.Instance as ObjectListView; + Debug.Assert(olv != null, "Instance must be an ObjectListView"); + + // Edit all the columns, not just the ones that are visible + base.EditValue(context, provider, olv.AllColumns); + + // Set the columns on the ListView to just the visible columns + List newColumns = olv.GetFilteredColumns(View.Details); + olv.Columns.Clear(); + olv.Columns.AddRange(newColumns.ToArray()); + + return olv.Columns; + } + + /// + /// What text should be shown in the list for the given object? + /// + /// + /// + protected override string GetDisplayText(object value) { + OLVColumn col = value as OLVColumn; + if (col == null || String.IsNullOrEmpty(col.AspectName)) + return base.GetDisplayText(value); + + return String.Format("{0} ({1})", base.GetDisplayText(value), col.AspectName); + } + } + + /// + /// Control how the overlay is presented in the IDE + /// + internal class OverlayConverter : ExpandableObjectConverter + { + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { + return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); + } + + public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { + if (destinationType == typeof(string)) { + ImageOverlay imageOverlay = value as ImageOverlay; + if (imageOverlay != null) { + return imageOverlay.Image == null ? "(none)" : "(set)"; + } + TextOverlay textOverlay = value as TextOverlay; + if (textOverlay != null) { + return String.IsNullOrEmpty(textOverlay.Text) ? "(none)" : "(set)"; + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} diff --git a/ObjectListView/ObjectListView.FxCop b/ObjectListView/ObjectListView.FxCop new file mode 100644 index 00000000..b5653ddc --- /dev/null +++ b/ObjectListView/ObjectListView.FxCop @@ -0,0 +1,3521 @@ + + + + True + c:\program files\microsoft fxcop 1.36\Xml\FxCopReport.xsl + + + + + + True + True + True + 10 + 1 + + False + + False + 120 + True + 2.0 + + + + $(ProjectDir)/trunk/ObjectListView/bin/Debug/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'ObjectListView.dll' + + + + + + + + + + + 'BarRenderer' + 'Pen' + + + + + + + + + + + + + + 'BarRenderer.BarRenderer(Pen, Brush)' + 'BarRenderer.UseStandardBar' + 'bool' + false + + + + + + + + + + + + + + 'BarRenderer.BarRenderer(int, int, Pen, Brush)' + 'BarRenderer.UseStandardBar' + 'bool' + false + + + + + + + + + + + + + + 'BarRenderer.BackgroundColor' + 'BaseRenderer.GetBackgroundColor()' + + + + + + + + + + + + + + + + + + 'BaseRenderer.GetBackgroundColor()' + + + + + + + + + + + + + + 'BaseRenderer.GetForegroundColor()' + + + + + + + + + + + + + + 'BaseRenderer.GetImageSelector()' + + + + + + + + + 'BaseRenderer.GetText()' + + + + + + + + + + + + + + 'BaseRenderer.GetTextBackgroundColor()' + + + + + + + + + + + + + + + + 'BorderDecoration' + 'SolidBrush' + + + + + + + + + 'CellEditEventHandler' + + + + + + + + + + + + + + + + 'g' + 'CheckStateRenderer.CalculateCheckBoxBounds(Graphics, Rectangle)' + + + + + + + + + + + 'ColumnRightClickEventHandler' + + + + + + + + + + + + + + + + + + 'ComboBoxItem.Key.get()' + + + + + + + + + + + + + + + 'DataListView.currencyManager_ListChanged(object, ListChangedEventArgs)' + + + + + + + + + 'DataListView.currencyManager_MetaDataChanged(object, EventArgs)' + + + + + + + + + 'DataListView.currencyManager_PositionChanged(object, EventArgs)' + + + + + + + + + + + + + 'DescribedTaskRenderer.GetDescription()' + + + + + + + + + + + 'DropTargetLocation' + + + + + + + + + + + 'collection' + 'ICollection' + 'FastObjectListDataSource.EnumerableToArray(IEnumerable)' + castclass + + + + + + + + + + + 'FastObjectListDataSource.FilteredObjectList.get()' + + + + + + + + + + + + + Flag + 'FlagRenderer' + + + + + + + + + + + + + 'FloatCellEditor.Value.get()' + + + + + + + + + + + + + + 'FloatCellEditor.Value.set(double)' + + + + + + + + + + + + + + + + + + + + + + 'GlassPanelForm.CreateParams.get()' + 'Form.CreateParams.get()' + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + + + + + + + + + + + 'GlassPanelForm.WndProc(ref Message)' + 'Form.WndProc(ref Message)' + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + + + + + + + + + + + 'GroupMetricsMask' + 'GroupMetricsMask.LVGMF_NONE' + + + + + + + + + + 'GroupMetricsMask' + + + + + + + + + + + + + + 'GroupState' + 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000 + + + + + 'GroupState' + 'GroupState.LVGS_NORMAL' + + + + + 'GroupState' + + + + + + + + + + + 'HeaderControl.HeaderControl(ObjectListView)' + 'NativeWindow.AssignHandle(IntPtr)' + ->'HeaderControl.HeaderControl(ObjectListView)' ->'HeaderControl.HeaderControl(ObjectListView)' + + + 'HeaderControl.HeaderControl(ObjectListView)' + 'NativeWindow.NativeWindow()' + ->'HeaderControl.HeaderControl(ObjectListView)' ->'HeaderControl.HeaderControl(ObjectListView)' + + + + + + + + + + + + + + 'g' + 'HeaderControl.CalculateHeight(Graphics)' + 'Graphics' + 'IDeviceContext' + + + + + + + + + 'g' + 'HeaderControl.DrawHeaderText(Graphics, Rectangle, OLVColumn, HeaderStateStyle)' + 'Graphics' + 'IDeviceContext' + + + + + + + + + 'g' + 'HeaderControl.DrawThemedBackground(Graphics, Rectangle, int, bool)' + 'Graphics' + 'IDeviceContext' + + + + + + + + + 'g' + 'HeaderControl.DrawThemedSortIndicator(Graphics, Rectangle)' + 'Graphics' + 'IDeviceContext' + + + + + + + + + Unthemed + 'HeaderControl.DrawUnthemedBackground(Graphics, Rectangle, int, bool, HeaderStateStyle)' + + + + + + + + + Unthemed + 'HeaderControl.DrawUnthemedSortIndicator(Graphics, Rectangle)' + + + + + + + + + 'm' + 'HeaderControl.HandleDestroy(ref Message)' + + + + + + + + + 'm' + 'HeaderControl.HandleMouseMove(ref Message)' + + + + + 'm' + + + + + + + + + + + + + + 'HeaderControl.HandleNotify(ref Message)' + 'Message.GetLParam(Type)' + ->'HeaderControl.HandleNotify(ref Message)' ->'HeaderControl.HandleNotify(ref Message)' + + + 'HeaderControl.HandleNotify(ref Message)' + 'Message.LParam.get()' + ->'HeaderControl.HandleNotify(ref Message)' ->'HeaderControl.HandleNotify(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + + + 'value' + 'HeaderControl.HotFontStyle.set(FontStyle)' + + + + + + + + + + + Flags + 'HeaderControl.TextFormatFlags' + + + + + + + + + 'HeaderControl.WndProc(ref Message)' + 'NativeWindow.WndProc(ref Message)' + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + + + + + 'HeaderControl.WndProc(ref Message)' + 'Message.Msg.get()' + ->'HeaderControl.WndProc(ref Message)' ->'HeaderControl.WndProc(ref Message)' + + + 'HeaderControl.WndProc(ref Message)' + 'NativeWindow.WndProc(ref Message)' + ->'HeaderControl.WndProc(ref Message)' ->'HeaderControl.WndProc(ref Message)' + + + + + + + + + + + + + + + + + + 'text' + 'HighlightTextRenderer.HighlightTextRenderer(string)' + + + + + + + + + + + 'value' + 'HighlightTextRenderer.StringComparison.set(StringComparison)' + + + + + + + + + + + + + 'value' + 'HighlightTextRenderer.TextToHighlight.set(string)' + + + + + + + + + + + + + + + + + 'HyperlinkEventArgs.Column.set(OLVColumn)' + + + + + + + + + + + + + 'HyperlinkEventArgs.ColumnIndex.set(int)' + + + + + + + + + + + + + 'HyperlinkEventArgs.Item.set(OLVListItem)' + + + + + + + + + + + + + 'HyperlinkEventArgs.ListView.set(ObjectListView)' + + + + + + + + + + + + + 'HyperlinkEventArgs.Model.set(object)' + + + + + + + + + + + + + 'HyperlinkEventArgs.RowIndex.set(int)' + + + + + + + + + + + + + 'HyperlinkEventArgs.SubItem.set(OLVListSubItem)' + + + + + + + + + + + 'HyperlinkEventArgs.Url' + + + + + + + + + 'HyperlinkEventArgs.Url.set(string)' + + + + + + + + + + + + + + + 'ImageRenderer.GetImageFromAspect()' + + + + + + + + + + + + + + + + + + + + 'IntUpDown.Value.get()' + + + + + + + + + + + + + + 'IntUpDown.Value.set(int)' + + + + + + + + + + + + + + + + + + + + 'IVirtualListDataSource.GetObjectCount()' + + + + + + + + + + + + + + + + Multi + 'MultiImageRenderer' + + + + + + + + + + + + + + 'MultiImageRenderer.ImageSelector' + 'BaseRenderer.GetImageSelector()' + + + + + + + + + + + + + 'Munger.GetValue(object)' + 'object' + + + + + + + + + + + + + + 'Munger.PutValue(object, object)' + 'object' + + + 'Munger.PutValue(object, object)' + 'object' + + + + + + + + + + + + + + + + + + 'NativeMethods.ChangeSize(IWin32Window, int, int)' + + + + + + + + + 'NativeMethods.ChangeZOrder(IWin32Window, IWin32Window)' + + + + + + + + + 'NativeMethods.DeleteObject(IntPtr)' + + + + + + + + + 'NativeMethods.DrawImageList(Graphics, ImageList, int, int, int, bool)' + + + + + + + + + 'NativeMethods.GetClientRect(IntPtr, ref Rectangle)' + + + + + + + + + 'NativeMethods.GetColumnSides(ObjectListView, int)' + + + + + 'NativeMethods.GetColumnSides(ObjectListView, int)' + 'Point' + + + + + + + + + 'NativeMethods.GetGroupInfo(ObjectListView, int, ref NativeMethods.LVGROUP2)' + + + + + + + + + 'NativeMethods.GetScrollInfo(IntPtr, int, NativeMethods.SCROLLINFO)' + + + + + + + + + 'NativeMethods.GetUpdateRect(Control)' + 'NativeMethods.GetUpdateRectInternal(IntPtr, ref Rectangle, bool)' + + + + + + + + + 'eraseBackground' + 'NativeMethods.GetUpdateRectInternal(IntPtr, ref Rectangle, bool)' + + + + + + + + + 'NativeMethods.GetWindowLong32(IntPtr, int)' + 8 + 64-bit + 4 + 'IntPtr' + + + + + + + + + 'NativeMethods.GetWindowLongPtr64(IntPtr, int)' + 'user32.dll' + GetWindowLongPtr + + + + + + + + + 'NativeMethods.ImageList_Draw(IntPtr, int, IntPtr, int, int, int)' + + + + + 'NativeMethods.ImageList_Draw(IntPtr, int, IntPtr, int, int, int)' + + + + + + + + + 'erase' + 'NativeMethods.InvalidateRect(IntPtr, int, bool)' + + + 'NativeMethods.InvalidateRect(IntPtr, int, bool)' + + + + + + + + + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVGROUP)' + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVGROUP)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVGROUP2)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVGROUPMETRICS)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVHITTESTINFO)' + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVHITTESTINFO)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, int)' + 4 + 64-bit + 8 + 'int' + + + + + 'lParam' + 'NativeMethods.SendMessage(IntPtr, int, int, int)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, IntPtr)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'lParam' + 'NativeMethods.SendMessage(IntPtr, int, IntPtr, int)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageHDItem(IntPtr, int, int, ref NativeMethods.HDITEM)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'NativeMethods.SendMessageIUnknown(IntPtr, int, object, int)' + + + + + 'lParam' + 'NativeMethods.SendMessageIUnknown(IntPtr, int, object, int)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'NativeMethods.SendMessageLVBKIMAGE(IntPtr, int, int, ref NativeMethods.LVBKIMAGE)' + + + + + 'wParam' + 'NativeMethods.SendMessageLVBKIMAGE(IntPtr, int, int, ref NativeMethods.LVBKIMAGE)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageLVItem(IntPtr, int, int, ref NativeMethods.LVITEM)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageRECT(IntPtr, int, int, ref NativeMethods.RECT)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageString(IntPtr, int, int, string)' + 4 + 64-bit + 8 + 'int' + + + + + 'lParam' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageTOOLINFO(IntPtr, int, int, NativeMethods.TOOLINFO)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'NativeMethods.SetBackgroundImage(ListView, Image)' + + + + + + + + + 'NativeMethods.SetBkColor(IntPtr, int)' + + + + + + + + + 'NativeMethods.SetSelectedColumn(ListView, ColumnHeader)' + + + + + + + + + 'NativeMethods.SetTextColor(IntPtr, int)' + + + + + + + + + 'NativeMethods.SetTooltipControl(ListView, ToolTipControl)' + + + + + + + + + 'NativeMethods.SetWindowLongPtr32(IntPtr, int, int)' + 8 + 64-bit + 4 + 'IntPtr' + + + + + + + + + 'dwNewLong' + 'NativeMethods.SetWindowLongPtr64(IntPtr, int, int)' + 4 + 64-bit + 8 + 'int' + + + + + 'NativeMethods.SetWindowLongPtr64(IntPtr, int, int)' + 'user32.dll' + SetWindowLongPtr + + + + + + + + + 'NativeMethods.SetWindowPos(IntPtr, IntPtr, int, int, int, int, uint)' + + + + + + + + + 'NativeMethods.SetWindowTheme(IntPtr, string, string)' + 8 + 64-bit + 4 + 'IntPtr' + + + + + 'subApp' + + + + + 'subIdList' + + + + + + + + + 'NativeMethods.ShowWindow(IntPtr, int)' + + + + + + + + + 'NativeMethods.ValidatedRectInternal(IntPtr, ref Rectangle)' + + + + + + + + + 'NativeMethods.ValidateRect(Control, Rectangle)' + + + + + + + + + + + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'HeaderControl.ColumnIndexUnderCursor.get()' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'HeaderControl.IsCursorOverLockedDivider.get()' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'OLVListItem.GetSubItemBounds(int)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'ObjectListView.CalculateCellBounds(OLVListItem, int, ItemBoundsPortion)' ->'ObjectListView.CalculateCellBounds(OLVListItem, int)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'ObjectListView.CalculateCellBounds(OLVListItem, int, ItemBoundsPortion)' ->'ObjectListView.CalculateCellTextBounds(OLVListItem, int)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'ObjectListView.OlvHitTest(int, int)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'TintedColumnDecoration.Draw(ObjectListView, Graphics, Rectangle)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'ObjectListView.EnsureGroupVisible(ListViewGroup)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'ObjectListView.HandleBeginScroll(ref Message)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'ObjectListView.HandleKeyDown(ref Message)' + + + + + + + + + + + + + + + + + + 'NativeMethods.TOOLINFO.TOOLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.TOOLINFO.TOOLINFO()' ->'NativeMethods.TOOLINFO.TOOLINFO()' ->'ToolTipControl.MakeToolInfoStruct(IWin32Window)' ->'ToolTipControl.AddTool(IWin32Window)' + + + 'NativeMethods.TOOLINFO.TOOLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.TOOLINFO.TOOLINFO()' ->'NativeMethods.TOOLINFO.TOOLINFO()' ->'ToolTipControl.MakeToolInfoStruct(IWin32Window)' ->'ToolTipControl.RemoveToolTip(IWin32Window)' + + + + + + + + + + + + + + + + + + 'ObjectListView.AllColumns' + + + + + + + + + + 'List<OLVColumn>' + 'ObjectListView.AllColumns' + + + + + + + + + + + + + + 'rowIndex' + 'ObjectListView.ApplyHyperlinkStyle(int, OLVListItem)' + + + + + + + + + 'ObjectListView.BooleanCheckStateGetter' + + + + + + + + + 'ObjectListView.BooleanCheckStatePutter' + + + + + + + + + 'item' + 'ObjectListView.CalculateCellBounds(OLVListItem, int)' + 'OLVListItem' + 'ListViewItem' + + + + + + + + + + + + + + 'ObjectListView.CellEditor' + 'ObjectListView.GetCellEditor(OLVListItem, int)' + + + + + + + + + 'ObjectListView.CellEditor_Validating(object, CancelEventArgs)' + + + + + + + + + 'ObjectListView.CellToolTip' + 'ObjectListView.GetCellToolTip(int, int)' + + + + + + + + + 'ObjectListView.CheckedObject' + 'ObjectListView.GetCheckedObject()' + + + + + + + + + 'ObjectListView.CheckedObjects' + 'ObjectListView.GetCheckedObjects()' + + + + + 'ObjectListView.CheckedObjects' + + + + + + + + + + + + + + 'List<OLVColumn>' + 'ObjectListView.ColumnsInDisplayOrder' + + + + + + + + + + + + + + 'ObjectListView.ConfigureAutoComplete(TextBox, OLVColumn)' + tb + 'tb' + + + + + + + + + 'ObjectListView.ConfigureAutoComplete(TextBox, OLVColumn, int)' + tb + 'tb' + + + + + + + + + 'List<OLVListItem>' + 'ObjectListView.DrawAllDecorations(Graphics, List<OLVListItem>)' + + + + + + + + + 'ObjectListView.EditorRegistry' + + + + + + + + + 'ObjectListView.EnsureGroupVisible(ListViewGroup)' + lvg + 'lvg' + + + + + + + + + 'ObjectListView.FilterObjects(IEnumerable, IModelFilter, IListFilter)' + a + 'aListFilter' + + + 'ObjectListView.FilterObjects(IEnumerable, IModelFilter, IListFilter)' + a + 'aModelFilter' + + + + + + + + + 'control' + 'CheckBox' + 'ObjectListView.GetControlValue(Control)' + castclass + + + 'control' + 'ComboBox' + 'ObjectListView.GetControlValue(Control)' + castclass + + + 'control' + 'TextBox' + 'ObjectListView.GetControlValue(Control)' + castclass + + + + + + + + + 'List<OLVColumn>' + 'ObjectListView.GetFilteredColumns(View)' + + + + + + + + + + + + + + 'selectedColumn' + + + + + + + + + + + + + + 'ObjectListView.GetItemCount()' + + + + + + + + + + + + + + 'ObjectListView.GetLastItemInDisplayOrder()' + + + + + + + + + 'ObjectListView.GetSelectedObject()' + + + + + + + + + + + + + + 'ObjectListView.GetSelectedObjects()' + + + + + + + + + + + + + + 'ObjectListView.HandleApplicationIdle(object, EventArgs)' + + + + + + + + + 'ObjectListView.HandleApplicationIdle_ResizeColumns(object, EventArgs)' + + + + + + + + + 'ObjectListView.HandleCellToolTipShowing(object, ToolTipShowingEventArgs)' + + + + + + + + + 'ObjectListView.HandleChar(ref Message)' + 'Control.ProcessKeyEventArgs(ref Message)' + ->'ObjectListView.HandleChar(ref Message)' ->'ObjectListView.HandleChar(ref Message)' + + + 'ObjectListView.HandleChar(ref Message)' + 'Message.WParam.get()' + ->'ObjectListView.HandleChar(ref Message)' ->'ObjectListView.HandleChar(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HandleColumnClick(object, ColumnClickEventArgs)' + + + + + + + + + 'ObjectListView.HandleColumnWidthChanged(object, ColumnWidthChangedEventArgs)' + + + + + + + + + 'ObjectListView.HandleColumnWidthChanging(object, ColumnWidthChangingEventArgs)' + + + + + + + + + 'ObjectListView.HandleContextMenu(ref Message)' + 'Message.LParam.get()' + ->'ObjectListView.HandleContextMenu(ref Message)' ->'ObjectListView.HandleContextMenu(ref Message)' + + + 'ObjectListView.HandleContextMenu(ref Message)' + 'Message.WParam.get()' + ->'ObjectListView.HandleContextMenu(ref Message)' ->'ObjectListView.HandleContextMenu(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HandleFindItem(ref Message)' + 'Message.GetLParam(Type)' + ->'ObjectListView.HandleFindItem(ref Message)' ->'ObjectListView.HandleFindItem(ref Message)' + + + 'ObjectListView.HandleFindItem(ref Message)' + 'Message.Result.set(IntPtr)' + ->'ObjectListView.HandleFindItem(ref Message)' ->'ObjectListView.HandleFindItem(ref Message)' + + + 'ObjectListView.HandleFindItem(ref Message)' + 'Message.WParam.get()' + ->'ObjectListView.HandleFindItem(ref Message)' ->'ObjectListView.HandleFindItem(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HandleHeaderToolTipShowing(object, ToolTipShowingEventArgs)' + + + + + + + + + 'ObjectListView.HandleLayout(object, LayoutEventArgs)' + + + + + + + + + 'ObjectListView.HandleNotify(ref Message)' + 'Marshal.PtrToStructure(IntPtr, Type)' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + 'ObjectListView.HandleNotify(ref Message)' + 'Marshal.StructureToPtr(object, IntPtr, bool)' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + 'ObjectListView.HandleNotify(ref Message)' + 'Message.GetLParam(Type)' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + 'ObjectListView.HandleNotify(ref Message)' + 'Message.Result.set(IntPtr)' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + 'ObjectListView.HandleNotify(ref Message)' + 'NativeWindow.Handle.get()' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HandleReflectNotify(ref Message)' + 'Marshal.StructureToPtr(object, IntPtr, bool)' + ->'ObjectListView.HandleReflectNotify(ref Message)' ->'ObjectListView.HandleReflectNotify(ref Message)' + + + 'ObjectListView.HandleReflectNotify(ref Message)' + 'Message.GetLParam(Type)' + ->'ObjectListView.HandleReflectNotify(ref Message)' ->'ObjectListView.HandleReflectNotify(ref Message)' + + + 'ObjectListView.HandleReflectNotify(ref Message)' + 'Message.LParam.get()' + ->'ObjectListView.HandleReflectNotify(ref Message)' ->'ObjectListView.HandleReflectNotify(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HeaderToolTip' + 'ObjectListView.GetHeaderToolTip(int)' + + + + + + + + + 'url' + 'ObjectListView.IsUrlVisited(string)' + + + + + + + + + 'url' + 'ObjectListView.MarkUrlVisited(string)' + + + + + + + + + Unsort + 'ObjectListView.MenuLabelUnsort' + + + + + + + + + 'ObjectListView.ProcessDialogKey(Keys)' + 'Control.ProcessDialogKey(Keys)' + [UIPermission(SecurityAction.LinkDemand, Window = UIPermissionWindow.AllWindows)] + + + + + 'ObjectListView.ProcessDialogKey(Keys)' + 'Control.ProcessDialogKey(Keys)' + ->'ObjectListView.ProcessDialogKey(Keys)' ->'ObjectListView.ProcessDialogKey(Keys)' + + + + + + + + + + + + + + 'ObjectListView.SelectedObject' + 'ObjectListView.GetSelectedObject()' + + + + + + + + + 'ObjectListView.SelectedObjects' + 'ObjectListView.GetSelectedObjects()' + + + + + 'ObjectListView.SelectedObjects' + + + + + + + + + + + + + + 'control' + 'ComboBox' + 'ObjectListView.SetControlValue(Control, object, string)' + castclass + + + + + + + + + Checkedness + 'ObjectListView.SetObjectCheckedness(object, CheckState)' + + + + + + + + + + + + + + 'item' + 'ObjectListView.SetSubItemImages(int, OLVListItem, bool)' + 'OLVListItem' + 'ListViewItem' + + + + + + + + + + + + + + 'columnToSort' + 'ObjectListView.ShowSortIndicator(OLVColumn, SortOrder)' + 'OLVColumn' + 'ColumnHeader' + + + + + + + + + + + + + + 'ObjectListView.SORT_INDICATOR_DOWN_KEY' + + + + + + + + + + + + + + 'ObjectListView.SORT_INDICATOR_UP_KEY' + + + + + + + + + + + + + + 'ObjectListView' + 'ISupportInitialize.BeginInit()' + + + + + + + + + 'ObjectListView' + 'ISupportInitialize.EndInit()' + + + + + + + + + Renderering + 'ObjectListView.TextRendereringHint' + + + + + + + + + Unsort + 'ObjectListView.Unsort()' + + + + + + + + + 'ObjectListView.WndProc(ref Message)' + 'ListView.WndProc(ref Message)' + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + + + + + 'ObjectListView.WndProc(ref Message)' + 'Control.DefWndProc(ref Message)' + ->'ObjectListView.WndProc(ref Message)' ->'ObjectListView.WndProc(ref Message)' + + + 'ObjectListView.WndProc(ref Message)' + 'ListView.WndProc(ref Message)' + ->'ObjectListView.WndProc(ref Message)' ->'ObjectListView.WndProc(ref Message)' + + + 'ObjectListView.WndProc(ref Message)' + 'Message.Msg.get()' + ->'ObjectListView.WndProc(ref Message)' ->'ObjectListView.WndProc(ref Message)' + + + + + + + + + + + + + + + + + + 'ObjectListView.ObjectListViewState.VersionNumber' + + + + + + + + + + + + + + + + 'OLVColumnAttribute' + + + + + 'OLVColumnAttribute.Title' + 'title' + + + + + 'OLVColumnAttribute' + + + + + + + + + Cutoffs + 'OLVColumnAttribute.GroupCutoffs' + + + + + 'OLVColumnAttribute.GroupCutoffs' + + + + + + + + + 'OLVColumnAttribute.GroupDescriptions' + + + + + + + + + + + + + 'OLVDataObject.ConvertToHtmlFragment(string)' + 'string.IndexOf(string)' + 'string.IndexOf(string, StringComparison)' + + + + + + + + + + + + + 'OLVGroup.GetState()' + + + + + + + + + 'OLVGroup.State' + 'OLVGroup.GetState()' + + + + + + + + + Subseted + 'OLVGroup.Subseted' + + + + + + + + + + + 'OLVListItem' + + + + + + + + + 'OLVListItem.Bounds' + 'ListViewItem.GetBounds(ItemBoundsPortion)' + + + + + + + + + + + 'value' + 'string' + 'OLVListItem.ImageSelector.set(object)' + castclass + + + + + + + + + + + + + + + 'OLVListSubItem.Url' + + + + + + + + + + + 'SimpleDropSink' + 'Timer' + + + + + + + + + 'Timer.Interval.set(int)' + 'SimpleDropSink.SimpleDropSink()' + + + + + + + + + + + 'TextAdornment' + 'StringFormat' + + + + + + + + + + + 'TextMatchFilter.TextMatchFilter(ObjectListView, string, OLVColumn[])' + StringComparison.InvariantCultureIgnoreCase + 'TextMatchFilter.TextMatchFilter(ObjectListView, string, OLVColumn[], TextMatchFilter.MatchKind, StringComparison)' + + + + + + + + + 'TextMatchFilter.TextMatchFilter(ObjectListView, string, TextMatchFilter.MatchKind)' + StringComparison.InvariantCultureIgnoreCase + 'TextMatchFilter.TextMatchFilter(ObjectListView, string, OLVColumn[], TextMatchFilter.MatchKind, StringComparison)' + + + + + + + + + 'TextMatchFilter.Columns' + + + + + + + + + 'TextMatchFilter.IsIncluded(OLVColumn)' + + + + + + + + + + + 'TextMatchFilter.MatchKind' + + + + + + + + + + + + + + 'TintedColumnDecoration' + 'SolidBrush' + + + + + + + + + + + Disp + 'ToolTipControl.HandleGetDispInfo(ref Message)' + + + + + + + + + + + + + + 'window' + 'ToolTipControl.PopToolTip(IWin32Window)' + + + + + + + + + + + 'ToolTipControl.StandardIcons' + + + + + + + + + + + 'List<TreeListView.Branch>' + 'TreeListView.Branch.ChildBranches' + + + + + + + + + 'List<TreeListView.Branch>' + 'TreeListView.Branch.FilteredChildBranches' + + + + + + + + + Flag + 'TreeListView.Branch.ManageLastChildFlag(MethodInvoker)' + + + + + + + + + + + Flags + 'TreeListView.Branch.BranchFlags' + + + + + + + + + + + 'TreeListView.Tree.GetBranchComparer()' + + + + + + + + + + + + + + + + + + 'TreeListView.TreeRenderer.PIXELS_PER_LEVEL' + + + + + + + + + + + + + 'TypedObjectListView<T>.BooleanCheckStateGetter' + + + + + + + + + 'TypedObjectListView<T>.BooleanCheckStatePutter' + + + + + + + + + 'TypedObjectListView<T>.CellToolTipGetter' + + + + + + + + + 'TypedObjectListView<T>.CheckedObjects' + + + + + + + + + + + + + + 'TypedObjectListView<T>.SelectedObjects' + + + + + + + + + + + + + + + + + + + + 'UintUpDown.Value.get()' + + + + + + + + + + + + + + 'UintUpDown.Value.set(uint)' + + + + + + + + + + + + + + + + + + + + 'VirtualObjectListView.CheckedObjects' + + + + + + + + + + + + + + 'VirtualObjectListView.HandleCacheVirtualItems(object, CacheVirtualItemsEventArgs)' + + + + + + + + + 'VirtualObjectListView.HandleRetrieveVirtualItem(object, RetrieveVirtualItemEventArgs)' + + + + + + + + + 'VirtualObjectListView.HandleSearchForVirtualItem(object, SearchForVirtualItemEventArgs)' + + + + + + + + + 'VirtualObjectListView.SetVirtualListSize(int)' + 'Exception' + + + + + + + + + + + + + + + + + + + + + These should be methods rather than properties + The out parameter is necessary since this method returns two pieces of information: the item under the point and the subitem item too + This is used to ensure we understand the newly load state. + All these properties should be assignable. + Our project is build with unsafe code enabled, so it automatically has the SecurityProperty set + These initializations are not unnecessary + We have to pass the windows message by reference + Instances of this class do not need to be disposable + These are utility methods that could well be used at runtime + Old style constants. Can't change now + These are OK like this. We need List<>, not IList<> since only List has a ToArray() method + Legacy cases that have to be kept like this + These are acceptable as methods rather than properties + windows messages should be passed by reference + This is not a security risk + There are several problems that can occur here and we want to ignore them all + These spellings are acceptable + These will only be used by OL types + + + Not appropriate here + Can't change now + we want to catch everthing + MS! + not flags + MS! + MS + + + + + Sign {0} with a strong name key. + + + Consider a design that does not require that {0} be an out parameter. + + + {0} appears to have no upstream public or protected callers. + + + Seal {0}, if possible. + + + It appears that field {0} is never used or is only ever assigned to. Use this field or remove it. + + + Change {0} to be read-only by removing the property setter. + + + Consider changing the type of parameter {0} in {1} from {2} to its base type {3}. This method appears to only require base class members in its implementation. Suppress this violation if there is a compelling reason to require the more derived type in the method signature. + + + Remove the property setter from {0} or reduce its accessibility because it corresponds to positional argument {1}. + + + {0}, a parameter, is cast to type {1} multiple times in method {2}. Cache the result of the 'as' operator or direct cast in order to eliminate the redundant {3} instruction. + + + Modify {0} to catch a more specific exception than {1} or rethrow the exception. + + + Change {0} in {1} to use Collection<T>, ReadOnlyCollection<T> or KeyedCollection<K,V> + + + {0} calls {1} but does not use the HRESULT or error code that the method returns. This could lead to unexpected behavior in error conditions or low-resource situations. Use the result in a conditional statement, assign the result to a variable, or pass it as an argument to another method. + {0} creates a new instance of {1} which is never used. Pass the instance as an argument to another method, assign the instance to a variable, or remove the object creation if it is unnecessary. + + + + {0} initializes field {1} of type {2} to {3}. Remove this initialization because it will be done automatically by the runtime. + + + {0} is marked with FlagsAttribute but a discrete member cannot be found for every settable bit that is used across the range of enum values. Remove FlagsAttribute from the type or define new members for the following (currently missing) values: {1} + + + + Modify the call to {0} in method {1} to set the timer interval to a value that's greater than or equal to one second. + + + In enum {0}, change the name of {1} to 'None'. + + + If enumeration name {0} is singular, change it to a plural form. + + + Correct the spelling of '{0}' in member name {1} or remove it entirely if it represents any sort of Hungarian notation. + In method {0}, correct the spelling of '{1}' in parameter name {2} or remove it entirely if it represents any sort of Hungarian notation. + Correct the spelling of '{0}' in type name {1}. + + + + Make {0} sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of {1} and is visible to derived classes. + + + Specify AttributeUsage on {0}. + + + Add the MarshalAsAttribute to parameter {0} of P/Invoke {1}. If the corresponding unmanaged parameter is a 4-byte Win32 'BOOL', use [MarshalAs(UnmanagedType.Bool)]. For a 1-byte C++ 'bool', use MarshalAs(UnmanagedType.U1). + Add the MarshalAsAttribute to the return type of P/Invoke {0}. If the corresponding unmanaged return type is a 4-byte Win32 'BOOL', use MarshalAs(UnmanagedType.Bool). For a 1-byte C++ 'bool', use MarshalAs(UnmanagedType.U1). + + + The constituent members of {0} appear to represent flags that can be combined rather than discrete values. If this is correct, mark the enumeration with FlagsAttribute. + + + Add [Serializable] to {0} as this type implements ISerializable. + + + Consider making {0} non-public or a constant. + + + If the name {0} is plural, change it to its singular form. + + + Add the following security attribute to {0} in order to match a LinkDemand on base method {1}: {2}. + + + As it is declared in your code, parameter {0} of P/Invoke {1} will be {2} bytes wide on {3} platforms. This is not correct, as the actual native declaration of this API indicates it should be {4} bytes wide on {3} platforms. Consult the MSDN Platform SDK documentation for help determining what data type should be used instead of {5}. + As it is declared in your code, the return type of P/Invoke {0} will be {1} bytes wide on {2} platforms. This is not correct, as the actual native declaration of this API indicates it should be {3} bytes wide on {2} platforms. Consult the MSDN Platform SDK documentation for help determining what data type should be used instead of {4}. + + + Correct the declaration of {0} so that it correctly points to an existing entry point in {1}. The unmanaged entry point name currently linked to is {2}. + + + Because property {0} is write-only, either add a property getter with an accessibility that is greater than or equal to its setter or convert this property into a method. + + + Change {0} to return a collection or make it a method. + + + The property name {0} is confusing given the existence of inherited method {1}. Rename or remove this property. + The property name {0} is confusing given the existence of method {1}. Rename or remove one of these members. + + + Parameter {0} of {1} is never used. Remove the parameter or use it in the method body. + + + Consider making {0} not externally visible. + + + To reduce security risk, marshal parameter {0} as Unicode, by setting DllImport.CharSet to CharSet.Unicode, or by explicitly marshaling the parameter as UnmanagedType.LPWStr. If you need to marshal this string as ANSI or system-dependent, set BestFitMapping=false; for added security, also set ThrowOnUnmappableChar=true. + + + {0} makes a call to {1} that does not explicitly provide a StringComparison. This should be replaced with a call to {2}. + + + Implement IDisposable on {0} because it creates members of the following IDisposable types: {1}. If {0} has previously shipped, adding new members that implement IDisposable to this type is considered a breaking change to existing consumers. + + + Change the type of parameter {0} of method {1} from string to System.Uri, or provide an overload of {1}, that allows {0} to be passed as a System.Uri object. + + + Change the type of property {0} from string to System.Uri. + + + Remove {0} and replace its usage with EventHandler<T> + + + {0} passes {1} as an argument to {2}. Replace this usage with StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase if appropriate. + + + Replace the term '{0}' in member name {1} with an appropriate alternate or remove it entirely. + Replace the term '{0}' in type name {1} with an appropriate alternate or remove it entirely. + + + Change {0} to a property if appropriate. + + + + diff --git a/ObjectListView/ObjectListView.cs b/ObjectListView/ObjectListView.cs new file mode 100644 index 00000000..7b4bbe59 --- /dev/null +++ b/ObjectListView/ObjectListView.cs @@ -0,0 +1,10924 @@ +/* + * ObjectListView - A listview to show various aspects of a collection of objects + * + * Author: Phillip Piper + * Date: 9/10/2006 11:15 AM + * + * Change log + * 2018-10-06 JPP - InsertObjects() when in a non-Detail View now correctly positions the items (SF bug #154) + * 2018-09-01 JPP - Hardened code against the rare case of the control having no columns (SF bug #174) + * The underlying ListView does not like having rows when there are no columns and throws exceptions.j + * 2018-05-05 JPP - Added OLVColumn.EditorCreator to allow fine control over what control is used to edit + * a particular cell. + * - Added IOlvEditor to allow custom editor to easily integrate with our editing scheme + * - ComboBoxes resize drop downs to show the widest entry via ControlUtilities.AutoResizeDropDown() + * 2018-05-03 JPP - Extend OnColumnRightClick so the event handler can tweak the menu to be shown + * 2018-04-27 JPP - Sorting now works when grouping is locked on a column AND SortGroupItemsByPrimaryColumn is true + * - Correctly report right clicks on group headers via CellRightClick events. + * v2.9.2 + * 2016-06-02 JPP - Cell editors now respond to mouse wheel events. Set AllowCellEditorsToProcessMouseWheel + * to false revert to previous behaviour. + * - Fixed issue in PauseAnimations() that prevented it from working until + * after the control had been rendered at least once. + * - CellEditUseWholeCell now has correct default value (true). + * - Dropping on a subitem when CellEditActivation is set to SingleClick no longer + * initiates a cell edit + * v2.9.1 + * 2015-12-30 JPP - Added CellRendererGetter to allow each cell to have a different renderer. + * - Obsolete properties are no longer code-gen'ed. + * + * v2.9.0 + * 2015-08-22 JPP - Allow selected row back/fore colours to be specified for each row + * - Renamed properties related to selection colours: + * - HighlightBackgroundColor -> SelectedBackColor + * - HighlightForegroundColor -> SelectedForeColor + * - UnfocusedHighlightBackgroundColor -> UnfocusedSelectedBackColor + * - UnfocusedHighlightForegroundColor -> UnfocusedSelectedForeColor + * - UseCustomSelectionColors is no longer used + * 2015-08-03 JPP - Added ObjectListView.CellEditFinished event + * - Added EditorRegistry.Unregister() + * 2015-07-08 JPP - All ObjectListViews are now OwnerDrawn by default. This allows all the great features + * of ObjectListView to work correctly at the slight cost of more processing at render time. + * It also avoids the annoying "hot item background ignored in column 0" behaviour that native + * ListView has. Programmers can still turn it back off if they wish. + * 2015-06-27 JPP - Yet another attempt to disable ListView's "shift click toggles checkboxes" behaviour. + * The last strategy (fake right click) worked, but had nasty side effects. This one works + * by intercepting a HITTEST message so that it fails. It no longer creates fake right mouse events. + * - Trigger SelectionChanged when filter is changed + * 2015-06-23 JPP - [BIG] Added support for Buttons + * 2015-06-22 JPP - Added OLVColumn.SearchValueGetter to allow the text used when text filtering to be customised + * - The default DefaultRenderer is now a HighlightTextRenderer, since that seems more generally useful + * 2015-06-17 JPP - Added FocusedObject property + * - Hot item is now always applied to the row even if FullRowSelect is false + * 2015-06-11 JPP - Added DefaultHotItemStyle property + * 2015-06-07 JPP - Added HeaderMinimumHeight property + * - Added ObjectListView.CellEditUsesWholeCell and OLVColumn.CellEditUsesWholeCell properties. + * 2015-05-15 JPP - Allow ImageGetter to return an Image (which I can't believe didn't work from the beginning!) + * 2015-04-27 JPP - Fix bug where setting View to LargeIcon in the designer was not persisted + * 2015-04-07 JPP - Ensure changes to row.Font in FormatRow are not wiped out by FormatCell (SF #141) + * + * v2.8.1 + * 2014-10-15 JPP - Added CellEditActivateMode.SingleClickAlways mode + * - Fire Filter event even if ModelFilter and ListFilter are null (SF #126) + * - Fixed issue where single-click editing didn't work (SF #128) + * v2.8.0 + * 2014-10-11 JPP - Fixed some XP-only flicker issues + * 2014-09-26 JPP - Fixed intricate bug involving checkboxes on non-owner-drawn virtual lists. + * - Fixed long standing (but previously unreported) error on non-details virtual lists where + * users could not click on checkboxes. + * 2014-09-07 JPP - (Major) Added ability to have checkboxes in headers + * - CellOver events are raised when the mouse moves over the header. Set TriggerCellOverEventsWhenOverHeader + * to false to disable this behaviour. + * - Freeze/Unfreeze now use BeginUpdate/EndUpdate to disable Window level drawing + * - Changed default value of ObjectListView.HeaderUsesThemes from true to false. Too many people were + * being confused, trying to make something interesting appear in the header and nothing showing up + * 2014-08-04 JPP - Final attempt to fix the multiple hyperlink events being raised. This involves turning + * a NM_CLICK notification into a NM_RCLICK. + * 2014-05-21 JPP - (Major) Added ability to disable rows. DisabledObjects, DisableObjects(), DisabledItemStyle. + * 2014-04-25 JPP - Fixed issue where virtual lists containing a single row didn't update hyperlinks on MouseOver + * - Added sanity check before BuildGroups() + * 2014-03-22 JPP - Fixed some subtle bugs resulting from misuse of TryGetValue() + * 2014-03-09 JPP - Added CollapsedGroups property + * - Several minor Resharper complaints quiesced. + * v2.7 + * 2014-02-14 JPP - Fixed issue with ShowHeaderInAllViews (another one!) where setting it to false caused the list to lose + * its other extended styles, leading to nasty flickering and worse. + * 2014-02-06 JPP - Fix issue on virtual lists where the filter was not correctly reapplied after columns were added or removed. + * - Made disposing of cell editors optional (defaults to true). This allows controls to be cached and reused. + * - Bracketed column resizing with BeginUpdate/EndUpdate to smooth redraws (thanks to Davide) + * 2014-02-01 JPP - Added static property ObjectListView.GroupTitleDefault to allow the default group title to be localised. + * 2013-09-24 JPP - Fixed issue in RefreshObjects() when model objects overrode the Equals()/GetHashCode() methods. + * - Made sure get state checker were used when they should have been + * 2013-04-21 JPP - Clicking on a non-groupable column header when showing groups will now sort + * the group contents by that column. + * v2.6 + * 2012-08-16 JPP - Added ObjectListView.EditModel() -- a convenience method to start an edit operation on a model + * 2012-08-10 JPP - Don't trigger selection changed events during sorting/grouping or add/removing columns + * 2012-08-06 JPP - Don't start a cell edit operation when the user clicks on the background of a checkbox cell. + * - Honor values from the BeforeSorting event when calling a CustomSorter + * 2012-08-02 JPP - Added CellVerticalAlignment and CellPadding properties. + * 2012-07-04 JPP - Fixed issue with cell editing where the cell editing didn't finish until the first idle event. + * This meant that if you clicked and held on the scroll thumb to finish a cell edit, the editor + * wouldn't be removed until the mouse was released. + * 2012-07-03 JPP - Fixed issue with SingleClick cell edit mode where the cell editing would not begin until the + * mouse moved after the click. + * 2012-06-25 JPP - Fixed bug where removing a column from a LargeIcon or SmallIcon view would crash the control. + * 2012-06-15 JPP - Added Reset() method, which definitively removes all rows *and* columns from an ObjectListView. + * 2012-06-11 JPP - Added FilteredObjects property which returns the collection of objects that survives any installed filters. + * 2012-06-04 JPP - [Big] Added UseNotifyPropertyChanged to allow OLV to listen for INotifyPropertyChanged events on models. + * 2012-05-30 JPP - Added static property ObjectListView.IgnoreMissingAspects. If this is set to true, all + * ObjectListViews will silently ignore missing aspect errors. Read the remarks to see why this would be useful. + * 2012-05-23 JPP - Setting UseFilterIndicator to true now sets HeaderUsesTheme to false. + * Also, changed default value of UseFilterIndicator to false. Previously, HeaderUsesTheme and UseFilterIndicator + * defaulted to true, which was pointless since when the HeaderUsesTheme is true, UseFilterIndicator does nothing. + * v2.5.1 + * 2012-05-06 JPP - Fix bug where collapsing the first group would cause decorations to stop being drawn (SR #3502608) + * 2012-04-23 JPP - Trigger GroupExpandingCollapsing event to allow the expand/collapse to be cancelled + * - Fixed SetGroupSpacing() so it corrects updates the space between all groups. + * - ResizeLastGroup() now does nothing since it was broken and I can't remember what it was + * even supposed to do :) + * 2012-04-18 JPP - Upgraded hit testing to include hits on groups. + * - HotItemChanged is now correctly recalculated on each mouse move. Includes "hot" group information. + * 2012-04-14 JPP - Added GroupStateChanged event. Useful for knowing when a group is collapsed/expanded. + * - Added AdditionalFilter property. This filter is combined with the Excel-like filtering that + * the end user might enact at runtime. + * 2012-04-10 JPP - Added PersistentCheckBoxes property to allow primary checkboxes to remember their values + * across list rebuilds. + * 2012-04-05 JPP - Reverted some code to .NET 2.0 standard. + * - Tweaked some code + * 2012-02-05 JPP - Fixed bug when selecting a separator on a drop down menu + * 2011-06-24 JPP - Added CanUseApplicationIdle property to cover cases where Application.Idle events + * are not triggered. For example, when used within VS (and probably Office) extensions + * Application.Idle is never triggered. Set CanUseApplicationIdle to false to handle + * these cases. + * - Handle cases where a second tool tip is installed onto the ObjectListView. + * - Correctly recolour rows after an Insert or Move + * - Removed m.LParam cast which could cause overflow issues on Win7/64 bit. + * v2.5.0 + * 2011-05-31 JPP - SelectObject() and SelectObjects() no longer deselect all other rows. + Set the SelectedObject or SelectedObjects property to do that. + * - Added CheckedObjectsEnumerable + * - Made setting CheckedObjects more efficient on large collections + * - Deprecated GetSelectedObject() and GetSelectedObjects() + * 2011-04-25 JPP - Added SubItemChecking event + * - Fixed bug in handling of NewValue on CellEditFinishing event + * 2011-04-12 JPP - Added UseFilterIndicator + * - Added some more localizable messages + * 2011-04-10 JPP - FormatCellEventArgs now has a CellValue property, which is the model value displayed + * by the cell. For example, for the Birthday column, the CellValue might be + * DateTime(1980, 12, 31), whereas the cell's text might be 'Dec 31, 1980'. + * 2011-04-04 JPP - Tweaked UseTranslucentSelection and UseTranslucentHotItem to look (a little) more + * like Vista/Win7. + * - Alternate colours are now only applied in Details view (as they always should have been) + * - Alternate colours are now correctly recalculated after removing objects + * 2011-03-29 JPP - Added SelectColumnsOnRightClickBehaviour to allow the selecting of columns mechanism + * to be changed. Can now be InlineMenu (the default), SubMenu, or ModelDialog. + * - ColumnSelectionForm was moved from the demo into the ObjectListView project itself. + * - Ctrl-C copying is now able to use the DragSource to create the data transfer object. + * 2011-03-19 JPP - All model object comparisons now use Equals rather than == (thanks to vulkanino) + * - [Small Break] GetNextItem() and GetPreviousItem() now accept and return OLVListView + * rather than ListViewItems. + * 2011-03-07 JPP - [Big] Added Excel-style filtering. Right click on a header to show a Filtering menu. + * - Added CellEditKeyEngine to allow key handling when cell editing to be completely customised. + * Add CellEditTabChangesRows and CellEditEnterChangesRows to show some of these abilities. + * 2011-03-06 JPP - Added OLVColumn.AutoCompleteEditorMode in preference to AutoCompleteEditor + * (which is now just a wrapper). Thanks to Clive Haskins + * - Added lots of docs to new classes + * 2011-02-25 JPP - Preserve word wrap settings on TreeListView + * - Resize last group to keep it on screen (thanks to ?) + * 2010-11-16 JPP - Fixed (once and for all) DisplayIndex problem with Generator + * - Changed the serializer used in SaveState()/RestoreState() so that it resolves on + * class name alone. + * - Fixed bug in GroupWithItemCountSingularFormatOrDefault + * - Fixed strange flickering in grouped, owner drawn OLV's using RefreshObject() + * v2.4.1 + * 2010-08-25 JPP - Fixed bug where setting OLVColumn.CheckBoxes to false gave it a renderer + * specialized for checkboxes. Oddly, this made Generator created owner drawn + * lists appear to be completely empty. + * - In IDE, all ObjectListView properties are now in a single "ObjectListView" category, + * rather than splitting them between "Appearance" and "Behavior" categories. + * - Added GroupingParameters.GroupComparer to allow groups to be sorted in a customizable fashion. + * - Sorting of items within a group can be disabled by setting + * GroupingParameters.PrimarySortOrder to None. + * 2010-08-24 JPP - Added OLVColumn.IsHeaderVertical to make a column draw its header vertical. + * - Added OLVColumn.HeaderTextAlign to control the alignment of a column's header text. + * - Added HeaderMaximumHeight to limit how tall the header section can become + * 2010-08-18 JPP - Fixed long standing bug where having 0 columns caused a InvalidCast exception. + * - Added IncludeAllColumnsInDataObject property + * - Improved BuildList(bool) so that it preserves scroll position even when + * the listview is grouped. + * 2010-08-08 JPP - Added OLVColumn.HeaderImageKey to allow column headers to have an image. + * - CellEdit validation and finish events now have NewValue property. + * 2010-08-03 JPP - Subitem checkboxes improvements: obey IsEditable, can be hot, can be disabled. + * - No more flickering of selection when tabbing between cells + * - Added EditingCellBorderDecoration to make it clearer which cell is being edited. + * 2010-08-01 JPP - Added ObjectListView.SmoothingMode to control the smoothing of all graphics + * operations + * - Columns now cache their group item format strings so that they still work as + * grouping columns after they have been removed from the listview. This cached + * value is only used when the column is not part of the listview. + * 2010-07-25 JPP - Correctly trigger a Click event when the mouse is clicked. + * 2010-07-16 JPP - Invalidate the control before and after cell editing to make sure it looks right + * 2010-06-23 JPP - Right mouse clicks on checkboxes no longer confuse them + * 2010-06-21 JPP - Avoid bug in underlying ListView control where virtual lists in SmallIcon view + * generate GETTOOLINFO msgs with invalid item indices. + * - Fixed bug where FastObjectListView would throw an exception when showing hyperlinks + * in any view except Details. + * 2010-06-15 JPP - Fixed bug in ChangeToFilteredColumns() that resulted in column display order + * being lost when a column was hidden. + * - Renamed IsVista property to IsVistaOrLater which more accurately describes its function. + * v2.4 + * 2010-04-14 JPP - Prevent object disposed errors when mouse event handlers cause the + * ObjectListView to be destroyed (e.g. closing a form during a + * double click event). + * - Avoid checkbox munging bug in standard ListView when shift clicking on non-primary + * columns when FullRowSelect is true. + * 2010-04-12 JPP - Fixed bug in group sorting (thanks Mike). + * 2010-04-07 JPP - Prevent hyperlink processing from triggering spurious MouseUp events. + * This showed itself by launching the same url multiple times. + * 2010-04-06 JPP - Space filling columns correctly resize upon initial display + * - ShowHeaderInAllViews is better but still not working reliably. + * See comments on property for more details. + * 2010-03-23 JPP - Added ObjectListView.HeaderFormatStyle and OLVColumn.HeaderFormatStyle. + * This makes HeaderFont and HeaderForeColor properties unnecessary -- + * they will be marked obsolete in the next version and removed after that. + * 2010-03-16 JPP - Changed object checking so that objects can be pre-checked before they + * are added to the list. Normal ObjectListViews managed "checkedness" in + * the ListViewItem, so this won't work for them, unless check state getters + * and putters have been installed. It will work not on virtual lists (thus fast lists and + * tree views) since they manage their own check state. + * 2010-03-06 JPP - Hide "Items" and "Groups" from the IDE properties grid since they shouldn't be set like that. + * They can still be accessed through "Custom Commands" and there's nothing we can do + * about that. + * 2010-03-05 JPP - Added filtering + * 2010-01-18 JPP - Overlays can be turned off. They also only work on 32-bit displays + * v2.3 + * 2009-10-30 JPP - Plugged possible resource leak by using using() with CreateGraphics() + * 2009-10-28 JPP - Fix bug when right clicking in the empty area of the header + * 2009-10-20 JPP - Redraw the control after setting EmptyListMsg property + * v2.3 + * 2009-09-30 JPP - Added Dispose() method to properly release resources + * 2009-09-16 JPP - Added OwnerDrawnHeader, which you can set to true if you want to owner draw + * the header yourself. + * 2009-09-15 JPP - Added UseExplorerTheme, which allow complete visual compliance with Vista explorer. + * But see property documentation for its many limitations. + * - Added ShowHeaderInAllViews. To make this work, Columns are no longer + * changed when switching to/from Tile view. + * 2009-09-11 JPP - Added OLVColumn.AutoCompleteEditor to allow the autocomplete of cell editors + * to be disabled. + * 2009-09-01 JPP - Added ObjectListView.TextRenderingHint property which controls the + * text rendering hint of all drawn text. + * 2009-08-28 JPP - [BIG] Added group formatting to supercharge what is possible with groups + * - [BIG] Virtual groups now work + * - Extended MakeGroupies() to handle more aspects of group creation + * 2009-08-19 JPP - Added ability to show basic column commands when header is right clicked + * - Added SelectedRowDecoration, UseTranslucentSelection and UseTranslucentHotItem. + * - Added PrimarySortColumn and PrimarySortOrder + * 2009-08-15 JPP - Correct problems with standard hit test and subitems + * 2009-08-14 JPP - [BIG] Support Decorations + * - [BIG] Added header formatting capabilities: font, color, word wrap + * - Gave ObjectListView its own designer to hide unwanted properties + * - Separated design time stuff into separate file + * - Added FormatRow and FormatCell events + * 2009-08-09 JPP - Get around bug in HitTest when not FullRowSelect + * - Added OLVListItem.GetSubItemBounds() method which works correctly + * for all columns including column 0 + * 2009-08-07 JPP - Added Hot* properties that track where the mouse is + * - Added HotItemChanged event + * - Overrode TextAlign on columns so that column 0 can have something other + * than just left alignment. This is only honored when owner drawn. + * v2.2.1 + * 2009-08-03 JPP - Subitem edit rectangles always allowed for an image in the cell, even if there was none. + * Now they only allow for an image when there actually is one. + * - Added Bounds property to OLVListItem which handles items being part of collapsed groups. + * 2009-07-29 JPP - Added GetSubItem() methods to ObjectListView and OLVListItem + * 2009-07-26 JPP - Avoided bug in .NET framework involving column 0 of owner drawn listviews not being + * redrawn when the listview was scrolled horizontally (this was a LOT of work to track + * down and fix!) + * - The cell edit rectangle is now correctly calculated when the listview is scrolled + * horizontally. + * 2009-07-14 JPP - If the user clicks/double clicks on a tree list cell, an edit operation will no longer begin + * if the click was to the left of the expander. This is implemented in such a way that + * other renderers can have similar "dead" zones. + * 2009-07-11 JPP - CalculateCellBounds() messed with the FullRowSelect property, which confused the + * tooltip handling on the underlying control. It no longer does this. + * - The cell edit rectangle is now correctly calculated for owner-drawn, non-Details views. + * 2009-07-08 JPP - Added Cell events (CellClicked, CellOver, CellRightClicked) + * - Made BuildList(), AddObject() and RemoveObject() thread-safe + * 2009-07-04 JPP - Space bar now properly toggles checkedness of selected rows + * 2009-07-02 JPP - Fixed bug with tooltips when the underlying Windows control was destroyed. + * - CellToolTipShowing events are now triggered in all views. + * v2.2 + * 2009-06-02 JPP - BeforeSortingEventArgs now has a Handled property to let event handlers do + * the item sorting themselves. + * - AlwaysGroupByColumn works again, as does SortGroupItemsByPrimaryColumn and all their + * various permutations. + * - SecondarySortOrder and SecondarySortColumn are now "null" by default + * 2009-05-15 JPP - Fixed bug so that KeyPress events are again triggered + * 2009-05-10 JPP - Removed all unsafe code + * 2009-05-07 JPP - Don't use glass panel for overlays when in design mode. It's too confusing. + * 2009-05-05 JPP - Added Scroll event (thanks to Christophe Hosten for the complete patch to implement this) + * - Added Unfocused foreground and background colors (also thanks to Christophe Hosten) + * 2009-04-29 JPP - Added SelectedColumn property, which puts a slight tint on that column. Combine + * this with TintSortColumn property and the sort column is automatically tinted. + * - Use an overlay to implement "empty list" msg. Default empty list msg is now prettier. + * 2009-04-28 JPP - Fixed bug where DoubleClick events were not triggered when CheckBoxes was true + * 2009-04-23 JPP - Fixed various bugs under Vista. + * - Made groups collapsible - Vista only. Thanks to Crustyapplesniffer. + * - Forward events from DropSink to the control itself. This allows handlers to be defined + * within the IDE for drop events + * 2009-04-16 JPP - Made several properties localizable. + * 2009-04-11 JPP - Correctly renderer checkboxes when RowHeight is non-standard + * 2009-04-11 JPP - Implemented overlay architecture, based on CustomDraw scheme. + * This unified drag drop feedback, empty list msgs and overlay images. + * - Added OverlayImage and friends, which allows an image to be drawn + * transparently over the listview + * 2009-04-10 JPP - Fixed long-standing annoying flicker on owner drawn virtual lists! + * This means, amongst other things, that grid lines no longer get confused, + * and drag-select no longer flickers. + * 2009-04-07 JPP - Calculate edit rectangles more accurately + * 2009-04-06 JPP - Double-clicking no longer toggles the checkbox + * - Double-clicking on a checkbox no longer confuses the checkbox + * 2009-03-16 JPP - Optimized the build of autocomplete lists + * v2.1 + * 2009-02-24 JPP - Fix bug where double-clicking VERY quickly on two different cells + * could give two editors + * - Maintain focused item when rebuilding list (SF #2547060) + * 2009-02-22 JPP - Reworked checkboxes so that events are triggered for virtual lists + * 2009-02-15 JPP - Added ObjectListView.ConfigureAutoComplete utility method + * 2009-02-02 JPP - Fixed bug with AlwaysGroupByColumn where column header clicks would not resort groups. + * 2009-02-01 JPP - OLVColumn.CheckBoxes and TriStateCheckBoxes now work. + * 2009-01-28 JPP - Complete overhaul of renderers! + * - Use IRenderer + * - Added ObjectListView.ItemRenderer to draw whole items + * 2009-01-23 JPP - Simple Checkboxes now work properly + * - Added TriStateCheckBoxes property to control whether the user can + * set the row checkbox to have the Indeterminate value + * - CheckState property is now just a wrapper around the StateImageIndex property + * 2009-01-20 JPP - Changed to always draw columns when owner drawn, rather than falling back on DrawDefault. + * This simplified several owner drawn problems + * - Added DefaultRenderer property to help with the above + * - HotItem background color is applied to all cells even when FullRowSelect is false + * - Allow grouping by CheckedAspectName columns + * - Commented out experimental animations. Still needs work. + * 2009-01-17 JPP - Added HotItemStyle and UseHotItem to highlight the row under the cursor + * - Added UseCustomSelectionColors property + * - Owner draw mode now honors ForeColor and BackColor settings on the list + * 2009-01-16 JPP - Changed to use EditorRegistry rather than hard coding cell editors + * 2009-01-10 JPP - Changed to use Equals() method rather than == to compare model objects. + * v2.0.1 + * 2009-01-08 JPP - Fixed long-standing "multiple columns generated" problem. + * Thanks to pinkjones for his help with solving this one! + * - Added EnsureGroupVisible() + * 2009-01-07 JPP - Made all public and protected methods virtual + * - FinishCellEditing, PossibleFinishCellEditing and CancelCellEditing are now public + * 2008-12-20 JPP - Fixed bug with group comparisons when a group key was null (SF#2445761) + * 2008-12-19 JPP - Fixed bug with space filling columns and layout events + * - Fixed RowHeight so that it only changes the row height, not the width of the images. + * v2.0 + * 2008-12-10 JPP - Handle Backspace key. Resets the search-by-typing state without delay + * - Made some changes to the column collection editor to try and avoid + * the multiple column generation problem. + * - Updated some documentation + * 2008-12-07 JPP - Search-by-typing now works when showing groups + * - Added BeforeSearching and AfterSearching events which are triggered when the user types + * into the list. + * - Added secondary sort information to Before/AfterSorting events + * - Reorganized group sorting code. Now triggers Sorting events. + * - Added GetItemIndexInDisplayOrder() + * - Tweaked in the interaction of the column editor with the IDE so that we (normally) + * don't rely on a hack to find the owning ObjectListView + * - Changed all 'DefaultValue(typeof(Color), "Empty")' to 'DefaultValue(typeof(Color), "")' + * since the first does not given Color.Empty as I thought, but the second does. + * 2008-11-28 JPP - Fixed long standing bug with horizontal scrollbar when shrinking the window. + * (thanks to Bartosz Borowik) + * 2008-11-25 JPP - Added support for dynamic tooltips + * - Split out comparers and header controls stuff into their own files + * 2008-11-21 JPP - Fixed bug where enabling grouping when there was not a sort column would not + * produce a grouped list. Grouping column now defaults to column 0. + * - Preserve selection on virtual lists when sorting + * 2008-11-20 JPP - Added ability to search by sort column to ObjectListView. Unified this with + * ability that was already in VirtualObjectListView + * 2008-11-19 JPP - Fixed bug in ChangeToFilteredColumns() where DisplayOrder was not always restored correctly. + * 2008-10-29 JPP - Event argument blocks moved to directly within the namespace, rather than being + * nested inside ObjectListView class. + * - Removed OLVColumn.CellEditor since it was never used. + * - Marked OLVColumn.AspectGetterAutoGenerated as obsolete (it has not been used for + * several versions now). + * 2008-10-28 JPP - SelectedObjects is now an IList, rather than an ArrayList. This allows + * it to accept generic list (e.g. List). + * 2008-10-09 JPP - Support indeterminate checkbox values. + * [BREAKING CHANGE] CheckStateGetter/CheckStatePutter now use CheckState types only. + * BooleanCheckStateGetter and BooleanCheckStatePutter added to ease transition. + * 2008-10-08 JPP - Added setFocus parameter to SelectObject(), which allows focus to be set + * at the same time as selecting. + * 2008-09-27 JPP - BIG CHANGE: Fissioned this file into separate files for each component + * 2008-09-24 JPP - Corrected bug with owner drawn lists where a column 0 with a renderer + * would draw at column 0 even if column 0 was dragged to another position. + * - Correctly handle space filling columns when columns are added/removed + * 2008-09-16 JPP - Consistently use try..finally for BeginUpdate()/EndUpdate() pairs + * 2008-08-24 JPP - If LastSortOrder is None when adding objects, don't force a resort. + * 2008-08-22 JPP - Catch and ignore some problems with setting TopIndex on FastObjectListViews. + * 2008-08-05 JPP - In the right-click column select menu, columns are now sorted by display order, rather than alphabetically + * v1.13 + * 2008-07-23 JPP - Consistently use copy-on-write semantics with Add/RemoveObject methods + * 2008-07-10 JPP - Enable validation on cell editors through a CellEditValidating event. + * (thanks to Artiom Chilaru for the initial suggestion and implementation). + * 2008-07-09 JPP - Added HeaderControl.Handle to allow OLV to be used within UserControls. + * (thanks to Michael Coffey for tracking this down). + * 2008-06-23 JPP - Split the more generally useful CopyObjectsToClipboard() method + * out of CopySelectionToClipboard() + * 2008-06-22 JPP - Added AlwaysGroupByColumn and AlwaysGroupBySortOrder, which + * force the list view to always be grouped by a particular column. + * 2008-05-31 JPP - Allow check boxes on FastObjectListViews + * - Added CheckedObject and CheckedObjects properties + * 2008-05-11 JPP - Allow selection foreground and background colors to be changed. + * Windows doesn't allow this, so we can only make it happen when owner + * drawing. Set the HighlightForegroundColor and HighlightBackgroundColor + * properties and then call EnableCustomSelectionColors(). + * v1.12 + * 2008-05-08 JPP - Fixed bug where the column select menu would not appear if the + * ObjectListView has a context menu installed. + * 2008-05-05 JPP - Non detail views can now be owner drawn. The renderer installed for + * primary column is given the chance to render the whole item. + * See BusinessCardRenderer in the demo for an example. + * - BREAKING CHANGE: RenderDelegate now returns a bool to indicate if default + * rendering should be done. Previously returned void. Only important if your + * code used RendererDelegate directly. Renderers derived from BaseRenderer + * are unchanged. + * 2008-05-03 JPP - Changed cell editing to use values directly when the values are Strings. + * Previously, values were always handed to the AspectToStringConverter. + * - When editing a cell, tabbing no longer tries to edit the next subitem + * when not in details view! + * 2008-05-02 JPP - MappedImageRenderer can now handle a Aspects that return a collection + * of values. Each value will be drawn as its own image. + * - Made AddObjects() and RemoveObjects() work for all flavours (or at least not crash) + * - Fixed bug with clearing virtual lists that has been scrolled vertically + * - Made TopItemIndex work with virtual lists. + * 2008-05-01 JPP - Added AddObjects() and RemoveObjects() to allow faster mods to the list + * - Reorganised public properties. Now alphabetical. + * - Made the class ObjectListViewState internal, as it always should have been. + * v1.11 + * 2008-04-29 JPP - Preserve scroll position when building the list or changing columns. + * - Added TopItemIndex property. Due to problems with the underlying control, this + * property is not always reliable. See property docs for info. + * 2008-04-27 JPP - Added SelectedIndex property. + * - Use a different, more general strategy to handle Invoke(). Removed all delegates + * that were only declared to support Invoke(). + * - Check all native structures for 64-bit correctness. + * 2008-04-25 JPP - Released on SourceForge. + * 2008-04-13 JPP - Added ColumnRightClick event. + * - Made the assembly CLS-compliant. To do this, our cell editors were made internal, and + * the constraint on FlagRenderer template parameter was removed (the type must still + * be an IConvertible, but if it isn't, the error will be caught at runtime, not compile time). + * 2008-04-12 JPP - Changed HandleHeaderRightClick() to have a columnIndex parameter, which tells + * exactly which column was right-clicked. + * 2008-03-31 JPP - Added SaveState() and RestoreState() + * - When cell editing, scrolling with a mouse wheel now ends the edit operation. + * v1.10 + * 2008-03-25 JPP - Added space filling columns. See OLVColumn.FreeSpaceProportion property for details. + * A space filling columns fills all (or a portion) of the width unoccupied by other columns. + * 2008-03-23 JPP - Finished tinkering with support for Mono. Compile with conditional compilation symbol 'MONO' + * to enable. On Windows, current problems with Mono: + * - grid lines on virtual lists crashes + * - when grouped, items sometimes are not drawn when any item is scrolled out of view + * - i can't seem to get owner drawing to work + * - when editing cell values, the editing controls always appear behind the listview, + * where they function fine -- the user just can't see them :-) + * 2008-03-16 JPP - Added some methods suggested by Chris Marlowe (thanks for the suggestions Chris) + * - ClearObjects() + * - GetCheckedObject(), GetCheckedObjects() + * - GetItemAt() variation that gets both the item and the column under a point + * 2008-02-28 JPP - Fixed bug with subitem colors when using OwnerDrawn lists and a RowFormatter. + * v1.9.1 + * 2008-01-29 JPP - Fixed bug that caused owner-drawn virtual lists to use 100% CPU + * - Added FlagRenderer to help draw bitwise-OR'ed flag values + * 2008-01-23 JPP - Fixed bug (introduced in v1.9) that made alternate row colour with groups not quite right + * - Ensure that DesignerSerializationVisibility.Hidden is set on all non-browsable properties + * - Make sure that sort indicators are shown after changing which columns are visible + * 2008-01-21 JPP - Added FastObjectListView + * v1.9 + * 2008-01-18 JPP - Added IncrementalUpdate() + * 2008-01-16 JPP - Right clicking on column header will allow the user to choose which columns are visible. + * Set SelectColumnsOnRightClick to false to prevent this behaviour. + * - Added ImagesRenderer to draw more than one images in a column + * - Changed the positioning of the empty list m to use all the client area. Thanks to Matze. + * 2007-12-13 JPP - Added CopySelectionToClipboard(). Ctrl-C invokes this method. Supports text + * and HTML formats. + * 2007-12-12 JPP - Added support for checkboxes via CheckStateGetter and CheckStatePutter properties. + * - Made ObjectListView and OLVColumn into partial classes so that others can extend them. + * 2007-12-09 JPP - Added ability to have hidden columns, i.e. columns that the ObjectListView knows + * about but that are not visible to the user. Controlled by OLVColumn.IsVisible. + * Added ColumnSelectionForm to the project to show how it could be used in an application. + * + * v1.8 + * 2007-11-26 JPP - Cell editing fully functional + * 2007-11-21 JPP - Added SelectionChanged event. This event is triggered once when the + * selection changes, no matter how many items are selected or deselected (in + * contrast to SelectedIndexChanged which is called once for every row that + * is selected or deselected). Thanks to lupokehl42 (Daniel) for his suggestions and + * improvements on this idea. + * 2007-11-19 JPP - First take at cell editing + * 2007-11-17 JPP - Changed so that items within a group are not sorted if lastSortOrder == None + * - Only call MakeSortIndicatorImages() if we haven't already made the sort indicators + * (Corrected misspelling in the name of the method too) + * 2007-11-06 JPP - Added ability to have secondary sort criteria when sorting + * (SecondarySortColumn and SecondarySortOrder properties) + * - Added SortGroupItemsByPrimaryColumn to allow group items to be sorted by the + * primary column. Previous default was to sort by the grouping column. + * v1.7 + * No big changes to this version but made to work with ListViewPrinter and released with it. + * + * 2007-11-05 JPP - Changed BaseRenderer to use DrawString() rather than TextRenderer, since TextRenderer + * does not work when printing. + * v1.6 + * 2007-11-03 JPP - Fixed some bugs in the rebuilding of DataListView. + * 2007-10-31 JPP - Changed to use builtin sort indicators on XP and later. This also avoids alignment + * problems on Vista. (thanks to gravybod for the suggestion and example implementation) + * 2007-10-21 JPP - Added MinimumWidth and MaximumWidth properties to OLVColumn. + * - Added ability for BuildList() to preserve selection. Calling BuildList() directly + * tries to preserve selection; calling SetObjects() does not. + * - Added SelectAll() and DeselectAll() methods. Useful for working with large lists. + * 2007-10-08 JPP - Added GetNextItem() and GetPreviousItem(), which walk sequentially through the + * listview items, even when the view is grouped. + * - Added SelectedItem property + * 2007-09-28 JPP - Optimized aspect-to-string conversion. BuildList() 15% faster. + * - Added empty implementation of RefreshObjects() to VirtualObjectListView since + * RefreshObjects() cannot work on virtual lists. + * 2007-09-13 JPP - Corrected bug with custom sorter in VirtualObjectListView (thanks for mpgjunky) + * 2007-09-07 JPP - Corrected image scaling bug in DrawAlignedImage() (thanks to krita970) + * 2007-08-29 JPP - Allow item count labels on groups to be set per column (thanks to cmarlow for idea) + * 2007-08-14 JPP - Major rework of DataListView based on Ian Griffiths's great work + * 2007-08-11 JPP - When empty, the control can now draw a "List Empty" m + * - Added GetColumn() and GetItem() methods + * v1.5 + * 2007-08-03 JPP - Support animated GIFs in ImageRenderer + * - Allow height of rows to be specified - EXPERIMENTAL! + * 2007-07-26 JPP - Optimised redrawing of owner-drawn lists by remembering the update rect + * - Allow sort indicators to be turned off + * 2007-06-30 JPP - Added RowFormatter delegate + * - Allow a different label when there is only one item in a group (thanks to cmarlow) + * v1.4 + * 2007-04-12 JPP - Allow owner drawn on steriods! + * - Column headers now display sort indicators + * - ImageGetter delegates can now return ints, strings or Images + * (Images are only visible if the list is owner drawn) + * - Added OLVColumn.MakeGroupies to help with group partitioning + * - All normal listview views are now supported + * - Allow dotted aspect names, e.g. Owner.Workgroup.Name (thanks to OlafD) + * - Added SelectedObject and SelectedObjects properties + * v1.3 + * 2007-03-01 JPP - Added DataListView + * - Added VirtualObjectListView + * - Added Freeze/Unfreeze capabilities + * - Allowed sort handler to be installed + * - Simplified sort comparisons: handles 95% of cases with only 6 lines of code! + * - Fixed bug with alternative line colors on unsorted lists (thanks to cmarlow) + * 2007-01-13 JPP - Fixed bug with lastSortOrder (thanks to Kwan Fu Sit) + * - Non-OLVColumns are no longer allowed + * 2007-01-04 JPP - Clear sorter before rebuilding list. 10x faster! (thanks to aaberg) + * - Include GetField in GetAspectByName() so field values can be Invoked too. + * - Fixed subtle bug in RefreshItem() that erased background colors. + * 2006-11-01 JPP - Added alternate line colouring + * 2006-10-20 JPP - Refactored all sorting comparisons and made it extendable. See ComparerManager. + * - Improved IDE integration + * - Made control DoubleBuffered + * - Added object selection methods + * 2006-10-13 JPP Implemented grouping and column sorting + * 2006-10-09 JPP Initial version + * + * TO DO: + * - Support undocumented group features: subseted groups, group footer items + * + * Copyright (C) 2006-2018 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Serialization.Formatters.Binary; +using System.Windows.Forms; +using System.Windows.Forms.VisualStyles; +using System.Runtime.Serialization.Formatters; +using System.Threading; + +namespace BrightIdeasSoftware +{ + /// + /// An ObjectListView is a much easier to use, and much more powerful, version of the ListView. + /// + /// + /// + /// An ObjectListView automatically populates a ListView control with information taken + /// from a given collection of objects. It can do this because each column is configured + /// to know which bit of the model object (the "aspect") it should be displaying. Columns similarly + /// understand how to sort the list based on their aspect, and how to construct groups + /// using their aspect. + /// + /// + /// Aspects are extracted by giving the name of a method to be called or a + /// property to be fetched. These names can be simple names or they can be dotted + /// to chain property access e.g. "Owner.Address.Postcode". + /// Aspects can also be extracted by installing a delegate. + /// + /// + /// An ObjectListView can show a "this list is empty" message when there is nothing to show in the list, + /// so that the user knows the control is supposed to be empty. + /// + /// + /// Right clicking on a column header should present a menu which can contain: + /// commands (sort, group, ungroup); filtering; and column selection. Whether these + /// parts of the menu appear is controlled by ShowCommandMenuOnRightClick, + /// ShowFilterMenuOnRightClick and SelectColumnsOnRightClick respectively. + /// + /// + /// The groups created by an ObjectListView can be configured to include other formatting + /// information, including a group icon, subtitle and task button. Using some undocumented + /// interfaces, these groups can even on virtual lists. + /// + /// + /// ObjectListView supports dragging rows to other places, including other application. + /// Special support is provide for drops from other ObjectListViews in the same application. + /// In many cases, an ObjectListView becomes a full drag source by setting to + /// true. Similarly, to accept drops, it is usually enough to set to true, + /// and then handle the and events (or the and + /// events, if you only want to handle drops from other ObjectListViews in your application). + /// + /// + /// For these classes to build correctly, the project must have references to these assemblies: + /// + /// + /// System + /// System.Data + /// System.Design + /// System.Drawing + /// System.Windows.Forms (obviously) + /// + /// + [Designer(typeof(BrightIdeasSoftware.Design.ObjectListViewDesigner))] + public partial class ObjectListView : ListView, ISupportInitialize { + + #region Life and death + + /// + /// Create an ObjectListView + /// + public ObjectListView() { + this.ColumnClick += new ColumnClickEventHandler(this.HandleColumnClick); + this.Layout += new LayoutEventHandler(this.HandleLayout); + this.ColumnWidthChanging += new ColumnWidthChangingEventHandler(this.HandleColumnWidthChanging); + this.ColumnWidthChanged += new ColumnWidthChangedEventHandler(this.HandleColumnWidthChanged); + + base.View = View.Details; + + // Turn on owner draw so that we are responsible for our own fates (and isolated from bugs in the underlying ListView) + this.OwnerDraw = true; + +// ReSharper disable DoNotCallOverridableMethodsInConstructor + this.DoubleBuffered = true; // kill nasty flickers. hiss... me hates 'em + this.ShowSortIndicators = true; + + // Setup the overlays that will be controlled by the IDE settings + this.InitializeStandardOverlays(); + this.InitializeEmptyListMsgOverlay(); +// ReSharper restore DoNotCallOverridableMethodsInConstructor + } + + /// + /// Dispose of any resources this instance has been using + /// + /// + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + + if (!disposing) + return; + + foreach (GlassPanelForm glassPanel in this.glassPanels) { + glassPanel.Unbind(); + glassPanel.Dispose(); + } + this.glassPanels.Clear(); + + this.UnsubscribeNotifications(null); + } + + #endregion + + // TODO + //public CheckBoxSettings CheckBoxSettings { + // get { return checkBoxSettings; } + // private set { checkBoxSettings = value; } + //} + + #region Static properties + + /// + /// Gets whether or not the left mouse button is down at this very instant + /// + public static bool IsLeftMouseDown { + get { return (Control.MouseButtons & MouseButtons.Left) == MouseButtons.Left; } + } + + /// + /// Gets whether the program running on Vista or later? + /// + public static bool IsVistaOrLater { + get { + if (!ObjectListView.sIsVistaOrLater.HasValue) + ObjectListView.sIsVistaOrLater = Environment.OSVersion.Version.Major >= 6; + return ObjectListView.sIsVistaOrLater.Value; + } + } + private static bool? sIsVistaOrLater; + + /// + /// Gets whether the program running on Win7 or later? + /// + public static bool IsWin7OrLater { + get { + if (!ObjectListView.sIsWin7OrLater.HasValue) { + // For some reason, Win7 is v6.1, not v7.0 + Version version = Environment.OSVersion.Version; + ObjectListView.sIsWin7OrLater = version.Major > 6 || (version.Major == 6 && version.Minor > 0); + } + return ObjectListView.sIsWin7OrLater.Value; + } + } + private static bool? sIsWin7OrLater; + + /// + /// Gets or sets how what smoothing mode will be applied to graphic operations. + /// + public static System.Drawing.Drawing2D.SmoothingMode SmoothingMode { + get { return ObjectListView.sSmoothingMode; } + set { ObjectListView.sSmoothingMode = value; } + } + private static System.Drawing.Drawing2D.SmoothingMode sSmoothingMode = + System.Drawing.Drawing2D.SmoothingMode.HighQuality; + + /// + /// Gets or sets how should text be rendered. + /// + public static System.Drawing.Text.TextRenderingHint TextRenderingHint { + get { return ObjectListView.sTextRendereringHint; } + set { ObjectListView.sTextRendereringHint = value; } + } + private static System.Drawing.Text.TextRenderingHint sTextRendereringHint = + System.Drawing.Text.TextRenderingHint.SystemDefault; + + /// + /// Gets or sets the string that will be used to title groups when the group key is null. + /// Exposed so it can be localized. + /// + public static string GroupTitleDefault { + get { return ObjectListView.sGroupTitleDefault; } + set { ObjectListView.sGroupTitleDefault = value ?? "{null}"; } + } + private static string sGroupTitleDefault = "{null}"; + + /// + /// Convert the given enumerable into an ArrayList as efficiently as possible + /// + /// The source collection + /// If true, this method will always create a new + /// collection. + /// An ArrayList with the same contents as the given collection. + /// + /// When we move to .NET 3.5, we can use LINQ and not need this method. + /// + public static ArrayList EnumerableToArray(IEnumerable collection, bool alwaysCreate) { + if (collection == null) + return new ArrayList(); + + if (!alwaysCreate) { + ArrayList array = collection as ArrayList; + if (array != null) + return array; + + IList iList = collection as IList; + if (iList != null) + return ArrayList.Adapter(iList); + } + + ICollection iCollection = collection as ICollection; + if (iCollection != null) + return new ArrayList(iCollection); + + ArrayList newObjects = new ArrayList(); + foreach (object x in collection) + newObjects.Add(x); + return newObjects; + } + + + /// + /// Return the count of items in the given enumerable + /// + /// + /// + /// When we move to .NET 3.5, we can use LINQ and not need this method. + public static int EnumerableCount(IEnumerable collection) { + if (collection == null) + return 0; + + ICollection iCollection = collection as ICollection; + if (iCollection != null) + return iCollection.Count; + + int i = 0; +// ReSharper disable once UnusedVariable + foreach (object x in collection) + i++; + return i; + } + + /// + /// Return whether or not the given enumerable is empty. A string is regarded as + /// an empty collection. + /// + /// + /// True if the given collection is null or empty + /// + /// When we move to .NET 3.5, we can use LINQ and not need this method. + /// + public static bool IsEnumerableEmpty(IEnumerable collection) { + return collection == null || (collection is string) || !collection.GetEnumerator().MoveNext(); + } + + /// + /// Gets or sets whether all ObjectListViews will silently ignore missing aspect errors. + /// + /// + /// + /// By default, if an ObjectListView is asked to display an aspect + /// (i.e. a field/property/method) + /// that does not exist from a model, it displays an error message in that cell, since that + /// condition is normally a programming error. There are some use cases where + /// this is not an error -- in those cases, set this to true and ObjectListView will + /// simply display an empty cell. + /// + /// Be warned: if you set this to true, it can be very difficult to track down + /// typing mistakes or name changes in AspectNames. + /// + public static bool IgnoreMissingAspects { + get { return Munger.IgnoreMissingAspects; } + set { Munger.IgnoreMissingAspects = value; } + } + + /// + /// Gets or sets whether the control will draw a rectangle in each cell showing the cell padding. + /// + /// + /// + /// This can help with debugging display problems from cell padding. + /// + /// As with all cell padding, this setting only takes effect when the control is owner drawn. + /// + public static bool ShowCellPaddingBounds { + get { return sShowCellPaddingBounds; } + set { sShowCellPaddingBounds = value; } + } + private static bool sShowCellPaddingBounds; + + /// + /// Gets the style that will be used by default to format disabled rows + /// + public static SimpleItemStyle DefaultDisabledItemStyle { + get { + if (sDefaultDisabledItemStyle == null) { + sDefaultDisabledItemStyle = new SimpleItemStyle(); + sDefaultDisabledItemStyle.ForeColor = Color.DarkGray; + } + return sDefaultDisabledItemStyle; + } + } + private static SimpleItemStyle sDefaultDisabledItemStyle; + + /// + /// Gets the style that will be used by default to format hot rows + /// + public static HotItemStyle DefaultHotItemStyle { + get { + if (sDefaultHotItemStyle == null) { + sDefaultHotItemStyle = new HotItemStyle(); + sDefaultHotItemStyle.BackColor = Color.FromArgb(224, 235, 253); + } + return sDefaultHotItemStyle; + } + } + private static HotItemStyle sDefaultHotItemStyle; + + #endregion + + #region Public properties + + /// + /// Gets or sets an model filter that is combined with any column filtering that the end-user specifies. + /// + /// This is different from the ModelFilter property, since setting that will replace + /// any column filtering, whereas setting this will combine this filter with the column filtering + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IModelFilter AdditionalFilter + { + get { return this.additionalFilter; } + set + { + if (this.additionalFilter == value) + return; + this.additionalFilter = value; + this.UpdateColumnFiltering(); + } + } + private IModelFilter additionalFilter; + + /// + /// Get or set all the columns that this control knows about. + /// Only those columns where IsVisible is true will be seen by the user. + /// + /// + /// + /// If you want to add new columns programmatically, add them to + /// AllColumns and then call RebuildColumns(). Normally, you do not have to + /// deal with this property directly. Just use the IDE. + /// + /// If you do add or remove columns from the AllColumns collection, + /// you have to call RebuildColumns() to make those changes take effect. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public virtual List AllColumns { + get { return this.allColumns; } + set { this.allColumns = value ?? new List(); } + } + private List allColumns = new List(); + + /// + /// Gets or sets whether or not ObjectListView will allow cell editors to response to mouse wheel events. Default is true. + /// If this is true, cell editors that respond to mouse wheel events (e.g. numeric edit, DateTimeEditor, combo boxes) will operate + /// as expected. + /// If this is false, a mouse wheel event is interpreted as a request to scroll the control vertically. This will automatically + /// finish any cell edit operation that was in flight. This was the default behaviour prior to v2.9. + /// + [Category("ObjectListView"), + Description("Should ObjectListView allow cell editors to response to mouse wheel events (default: true)"), + DefaultValue(true)] + public virtual bool AllowCellEditorsToProcessMouseWheel + { + get { return allowCellEditorsToProcessMouseWheel; } + set { allowCellEditorsToProcessMouseWheel = value; } + } + private bool allowCellEditorsToProcessMouseWheel = true; + + /// + /// Gets or sets the background color of every second row + /// + [Category("ObjectListView"), + Description("If using alternate colors, what color should the background of alternate rows be?"), + DefaultValue(typeof(Color), "")] + public Color AlternateRowBackColor { + get { return alternateRowBackColor; } + set { alternateRowBackColor = value; } + } + private Color alternateRowBackColor = Color.Empty; + + /// + /// Gets the alternate row background color that has been set, or the default color + /// + [Browsable(false)] + public virtual Color AlternateRowBackColorOrDefault { + get { + return this.alternateRowBackColor == Color.Empty ? Color.LemonChiffon : this.alternateRowBackColor; + } + } + + /// + /// This property forces the ObjectListView to always group items by the given column. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVColumn AlwaysGroupByColumn { + get { return alwaysGroupByColumn; } + set { alwaysGroupByColumn = value; } + } + private OLVColumn alwaysGroupByColumn; + + /// + /// If AlwaysGroupByColumn is not null, this property will be used to decide how + /// those groups are sorted. If this property has the value SortOrder.None, then + /// the sort order will toggle according to the users last header click. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortOrder AlwaysGroupBySortOrder { + get { return alwaysGroupBySortOrder; } + set { alwaysGroupBySortOrder = value; } + } + private SortOrder alwaysGroupBySortOrder = SortOrder.None; + + /// + /// Give access to the image list that is actually being used by the control + /// + /// + /// Normally, it is preferable to use SmallImageList. Only use this property + /// if you know exactly what you are doing. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual ImageList BaseSmallImageList { + get { return base.SmallImageList; } + set { base.SmallImageList = value; } + } + + /// + /// How does the user indicate that they want to edit a cell? + /// None means that the listview cannot be edited. + /// + /// Columns can also be marked as editable. + [Category("ObjectListView"), + Description("How does the user indicate that they want to edit a cell?"), + DefaultValue(CellEditActivateMode.None)] + public virtual CellEditActivateMode CellEditActivation { + get { return cellEditActivation; } + set { + cellEditActivation = value; + if (this.Created) + this.Invalidate(); + } + } + private CellEditActivateMode cellEditActivation = CellEditActivateMode.None; + + /// + /// When a cell is edited, should the whole cell be used (minus any space used by checkbox or image)? + /// Defaults to true. + /// + /// + /// This is always treated as true when the control is NOT owner drawn. + /// + /// When this is false and the control is owner drawn, + /// ObjectListView will try to calculate the width of the cell's + /// actual contents, and then size the editing control to be just the right width. If this is true, + /// the whole width of the cell will be used, regardless of the cell's contents. + /// + /// Each column can have a different value for property. This value from the control is only + /// used when a column is not specified one way or another. + /// Regardless of this setting, developers can specify the exact size of the editing control + /// by listening for the CellEditStarting event. + /// + [Category("ObjectListView"), + Description("When a cell is edited, should the whole cell be used?"), + DefaultValue(true)] + public virtual bool CellEditUseWholeCell { + get { return cellEditUseWholeCell; } + set { cellEditUseWholeCell = value; } + } + private bool cellEditUseWholeCell = true; + + /// + /// Gets or sets the engine that will handle key presses during a cell edit operation. + /// Settings this to null will reset it to default value. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public CellEditKeyEngine CellEditKeyEngine { + get { return this.cellEditKeyEngine ?? (this.cellEditKeyEngine = new CellEditKeyEngine()); } + set { this.cellEditKeyEngine = value; } + } + private CellEditKeyEngine cellEditKeyEngine; + + /// + /// Gets the control that is currently being used for editing a cell. + /// + /// This will obviously be null if no cell is being edited. + [Browsable(false)] + public Control CellEditor { + get { + return this.cellEditor; + } + } + + /// + /// Gets or sets the behaviour of the Tab key when editing a cell on the left or right + /// edge of the control. If this is false (the default), pressing Tab will wrap to the other side + /// of the same row. If this is true, pressing Tab when editing the right most cell will advance + /// to the next row + /// and Shift-Tab when editing the left-most cell will change to the previous row. + /// + [Category("ObjectListView"), + Description("Should Tab/Shift-Tab change rows while cell editing?"), + DefaultValue(false)] + public virtual bool CellEditTabChangesRows { + get { return cellEditTabChangesRows; } + set { + cellEditTabChangesRows = value; + if (cellEditTabChangesRows) { + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab, CellEditCharacterBehaviour.ChangeColumnRight, CellEditAtEdgeBehaviour.ChangeRow); + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab|Keys.Shift, CellEditCharacterBehaviour.ChangeColumnLeft, CellEditAtEdgeBehaviour.ChangeRow); + } else { + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab, CellEditCharacterBehaviour.ChangeColumnRight, CellEditAtEdgeBehaviour.Wrap); + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab | Keys.Shift, CellEditCharacterBehaviour.ChangeColumnLeft, CellEditAtEdgeBehaviour.Wrap); + } + } + } + private bool cellEditTabChangesRows; + + /// + /// Gets or sets the behaviour of the Enter keys while editing a cell. + /// If this is false (the default), pressing Enter will simply finish the editing operation. + /// If this is true, Enter will finish the edit operation and start a new edit operation + /// on the cell below the current cell, wrapping to the top of the next row when at the bottom cell. + /// + [Category("ObjectListView"), + Description("Should Enter change rows while cell editing?"), + DefaultValue(false)] + public virtual bool CellEditEnterChangesRows { + get { return cellEditEnterChangesRows; } + set { + cellEditEnterChangesRows = value; + if (cellEditEnterChangesRows) { + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter, CellEditCharacterBehaviour.ChangeRowDown, CellEditAtEdgeBehaviour.ChangeColumn); + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter | Keys.Shift, CellEditCharacterBehaviour.ChangeRowUp, CellEditAtEdgeBehaviour.ChangeColumn); + } else { + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter, CellEditCharacterBehaviour.EndEdit, CellEditAtEdgeBehaviour.EndEdit); + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter | Keys.Shift, CellEditCharacterBehaviour.EndEdit, CellEditAtEdgeBehaviour.EndEdit); + } + } + } + private bool cellEditEnterChangesRows; + + /// + /// Gets the tool tip control that shows tips for the cells + /// + [Browsable(false)] + public ToolTipControl CellToolTip { + get { + if (this.cellToolTip == null) { + this.CreateCellToolTip(); + } + return this.cellToolTip; + } + } + private ToolTipControl cellToolTip; + + /// + /// Gets or sets how many pixels will be left blank around each cell of this item. + /// Cell contents are aligned after padding has been taken into account. + /// + /// + /// Each value of the given rectangle will be treated as an inset from + /// the corresponding side. The width of the rectangle is the padding for the + /// right cell edge. The height of the rectangle is the padding for the bottom + /// cell edge. + /// + /// + /// So, this.olv1.CellPadding = new Rectangle(1, 2, 3, 4); will leave one pixel + /// of space to the left of the cell, 2 pixels at the top, 3 pixels of space + /// on the right edge, and 4 pixels of space at the bottom of each cell. + /// + /// + /// This setting only takes effect when the control is owner drawn. + /// + /// This setting only affects the contents of the cell. The background is + /// not affected. + /// If you set this to a foolish value, your control will appear to be empty. + /// + [Category("ObjectListView"), + Description("How much padding will be applied to each cell in this control?"), + DefaultValue(null)] + public Rectangle? CellPadding { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets or sets how cells will be vertically aligned by default. + /// + /// This setting only takes effect when the control is owner drawn. It will only be noticeable + /// when RowHeight has been set such that there is some vertical space in each row. + [Category("ObjectListView"), + Description("How will cell values be vertically aligned?"), + DefaultValue(StringAlignment.Center)] + public virtual StringAlignment CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment cellVerticalAlignment = StringAlignment.Center; + + /// + /// Should this list show checkboxes? + /// + public new bool CheckBoxes { + get { return base.CheckBoxes; } + set { + // Due to code in the base ListView class, turning off CheckBoxes on a virtual + // list always throws an InvalidOperationException. We have to do some major hacking + // to get around that + if (this.VirtualMode) { + // Leave virtual mode + this.StateImageList = null; + this.VirtualListSize = 0; + this.VirtualMode = false; + + // Change the CheckBox setting while not in virtual mode + base.CheckBoxes = value; + + // Reinstate virtual mode + this.VirtualMode = true; + + // Re-enact the bits that we lost by switching to virtual mode + this.ShowGroups = this.ShowGroups; + this.BuildList(true); + } else { + base.CheckBoxes = value; + // Initialize the state image list so we can display indeterminate values. + this.InitializeStateImageList(); + } + } + } + + /// + /// Return the model object of the row that is checked or null if no row is checked + /// or more than one row is checked + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual Object CheckedObject { + get { + IList checkedObjects = this.CheckedObjects; + return checkedObjects.Count == 1 ? checkedObjects[0] : null; + } + set { + this.CheckedObjects = new ArrayList(new Object[] { value }); + } + } + + /// + /// Get or set the collection of model objects that are checked. + /// When setting this property, any row whose model object isn't + /// in the given collection will be unchecked. Setting to null is + /// equivalent to unchecking all. + /// + /// + /// + /// This property returns a simple collection. Changes made to the returned + /// collection do NOT affect the list. This is different to the behaviour of + /// CheckedIndicies collection. + /// + /// + /// .NET's CheckedItems property is not helpful. It is just a short-hand for + /// iterating through the list looking for items that are checked. + /// + /// + /// The performance of the get method is O(n), where n is the number of items + /// in the control. The performance of the set method is + /// O(n + m) where m is the number of objects being checked. Be careful on long lists. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IList CheckedObjects { + get { + ArrayList list = new ArrayList(); + if (this.CheckBoxes) { + for (int i = 0; i < this.GetItemCount(); i++) { + OLVListItem olvi = this.GetItem(i); + if (olvi.CheckState == CheckState.Checked) + list.Add(olvi.RowObject); + } + } + return list; + } + set { + if (!this.CheckBoxes) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + // Set up an efficient way of testing for the presence of a particular model + Hashtable table = new Hashtable(this.GetItemCount()); + if (value != null) { + foreach (object x in value) + table[x] = true; + } + + this.BeginUpdate(); + foreach (Object x in this.Objects) { + this.SetObjectCheckedness(x, table.ContainsKey(x) ? CheckState.Checked : CheckState.Unchecked); + } + this.EndUpdate(); + + // Debug.WriteLine(String.Format("PERF - Setting CheckedObjects on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); + + } + } + + /// + /// Gets or sets the checked objects from an enumerable. + /// + /// + /// Useful for checking all objects in the list. + /// + /// + /// this.olv1.CheckedObjectsEnumerable = this.olv1.Objects; + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable CheckedObjectsEnumerable { + get { + return this.CheckedObjects; + } + set { + this.CheckedObjects = ObjectListView.EnumerableToArray(value, true); + } + } + + /// + /// Gets Columns for this list. We hide the original so we can associate + /// a specialised editor with it. + /// + [Editor("BrightIdeasSoftware.Design.OLVColumnCollectionEditor", "System.Drawing.Design.UITypeEditor")] + public new ListView.ColumnHeaderCollection Columns { + get { + return base.Columns; + } + } + + /// + /// Get/set the list of columns that should be used when the list switches to tile view. + /// + [Browsable(false), + Obsolete("Use GetFilteredColumns() and OLVColumn.IsTileViewColumn instead"), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public List ColumnsForTileView { + get { return this.GetFilteredColumns(View.Tile); } + } + + /// + /// Return the visible columns in the order they are displayed to the user + /// + [Browsable(false)] + public virtual List ColumnsInDisplayOrder { + get { + OLVColumn[] columnsInDisplayOrder = new OLVColumn[this.Columns.Count]; + foreach (OLVColumn col in this.Columns) { + columnsInDisplayOrder[col.DisplayIndex] = col; + } + return new List(columnsInDisplayOrder); + } + } + + + /// + /// Get the area of the control that shows the list, minus any header control + /// + [Browsable(false)] + public Rectangle ContentRectangle { + get { + Rectangle r = this.ClientRectangle; + + // If the listview has a header control, remove the header from the control area + if ((this.View == View.Details || this.ShowHeaderInAllViews) && this.HeaderControl != null) { + Rectangle hdrBounds = new Rectangle(); + NativeMethods.GetClientRect(this.HeaderControl.Handle, ref hdrBounds); + r.Y = hdrBounds.Height; + r.Height = r.Height - hdrBounds.Height; + } + + return r; + } + } + + /// + /// Gets or sets if the selected rows should be copied to the clipboard when the user presses Ctrl-C + /// + [Category("ObjectListView"), + Description("Should the control copy the selection to the clipboard when the user presses Ctrl-C?"), + DefaultValue(true)] + public virtual bool CopySelectionOnControlC { + get { return copySelectionOnControlC; } + set { copySelectionOnControlC = value; } + } + private bool copySelectionOnControlC = true; + + + /// + /// Gets or sets whether the Control-C copy to clipboard functionality should use + /// the installed DragSource to create the data object that is placed onto the clipboard. + /// + /// This is normally what is desired, unless a custom DragSource is installed + /// that does some very specialized drag-drop behaviour. + [Category("ObjectListView"), + Description("Should the Ctrl-C copy process use the DragSource to create the Clipboard data object?"), + DefaultValue(true)] + public bool CopySelectionOnControlCUsesDragSource { + get { return this.copySelectionOnControlCUsesDragSource; } + set { this.copySelectionOnControlCUsesDragSource = value; } + } + private bool copySelectionOnControlCUsesDragSource = true; + + /// + /// Gets the list of decorations that will be drawn the ListView + /// + /// + /// + /// Do not modify the contents of this list directly. Use the AddDecoration() and RemoveDecoration() methods. + /// + /// + /// A decoration scrolls with the list contents. An overlay is fixed in place. + /// + /// + [Browsable(false)] + protected IList Decorations { + get { return this.decorations; } + } + private readonly List decorations = new List(); + + /// + /// When owner drawing, this renderer will draw columns that do not have specific renderer + /// given to them + /// + /// If you try to set this to null, it will revert to a HighlightTextRenderer + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IRenderer DefaultRenderer { + get { return this.defaultRenderer; } + set { this.defaultRenderer = value ?? new HighlightTextRenderer(); } + } + private IRenderer defaultRenderer = new HighlightTextRenderer(); + + /// + /// Get the renderer to be used to draw the given cell. + /// + /// The row model for the row + /// The column to be drawn + /// The renderer used for drawing a cell. Must not return null. + public IRenderer GetCellRenderer(object model, OLVColumn column) { + IRenderer renderer = this.CellRendererGetter == null ? null : this.CellRendererGetter(model, column); + return renderer ?? column.Renderer ?? this.DefaultRenderer; + } + + /// + /// Gets or sets the style that will be applied to disabled items. + /// + /// If this is not set explicitly, will be used. + [Category("ObjectListView"), + Description("The style that will be applied to disabled items"), + DefaultValue(null)] + public SimpleItemStyle DisabledItemStyle + { + get { return disabledItemStyle; } + set { disabledItemStyle = value; } + } + private SimpleItemStyle disabledItemStyle; + + /// + /// Gets or sets the list of model objects that are disabled. + /// Disabled objects cannot be selected or activated. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable DisabledObjects + { + get + { + return disabledObjects.Keys; + } + set + { + this.disabledObjects.Clear(); + DisableObjects(value); + } + } + private readonly Hashtable disabledObjects = new Hashtable(); + + /// + /// Is this given model object disabled? + /// + /// + /// + public bool IsDisabled(object model) + { + return model != null && this.disabledObjects.ContainsKey(model); + } + + /// + /// Disable the given model object. + /// Disabled objects cannot be selected or activated. + /// + /// Must not be null + public void DisableObject(object model) { + ArrayList list = new ArrayList(); + list.Add(model); + this.DisableObjects(list); + } + + /// + /// Disable all the given model objects + /// + /// + public void DisableObjects(IEnumerable models) + { + if (models == null) + return; + ArrayList list = ObjectListView.EnumerableToArray(models, false); + foreach (object model in list) + { + if (model == null) + continue; + + this.disabledObjects[model] = true; + int modelIndex = this.IndexOf(model); + if (modelIndex >= 0) + NativeMethods.DeselectOneItem(this, modelIndex); + } + this.RefreshObjects(list); + } + + /// + /// Enable the given model object, so it can be selected and activated again. + /// + /// Must not be null + public void EnableObject(object model) + { + this.disabledObjects.Remove(model); + this.RefreshObject(model); + } + + /// + /// Enable all the given model objects + /// + /// + public void EnableObjects(IEnumerable models) + { + if (models == null) + return; + ArrayList list = ObjectListView.EnumerableToArray(models, false); + foreach (object model in list) + { + if (model != null) + this.disabledObjects.Remove(model); + } + this.RefreshObjects(list); + } + + /// + /// Forget all disabled objects. This does not trigger a redraw or rebuild + /// + protected void ClearDisabledObjects() + { + this.disabledObjects.Clear(); + } + + /// + /// Gets or sets the object that controls how drags start from this control + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IDragSource DragSource { + get { return this.dragSource; } + set { this.dragSource = value; } + } + private IDragSource dragSource; + + /// + /// Gets or sets the object that controls how drops are accepted and processed + /// by this ListView. + /// + /// + /// + /// If the given sink is an instance of SimpleDropSink, then events from the drop sink + /// will be automatically forwarded to the ObjectListView (which means that handlers + /// for those event can be configured within the IDE). + /// + /// If this is set to null, the control will not accept drops. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IDropSink DropSink { + get { return this.dropSink; } + set { + if (this.dropSink == value) + return; + + // Stop listening for events on the old sink + SimpleDropSink oldSink = this.dropSink as SimpleDropSink; + if (oldSink != null) { + oldSink.CanDrop -= new EventHandler(this.DropSinkCanDrop); + oldSink.Dropped -= new EventHandler(this.DropSinkDropped); + oldSink.ModelCanDrop -= new EventHandler(this.DropSinkModelCanDrop); + oldSink.ModelDropped -= new EventHandler(this.DropSinkModelDropped); + } + + this.dropSink = value; + this.AllowDrop = (value != null); + if (this.dropSink != null) + this.dropSink.ListView = this; + + // Start listening for events on the new sink + SimpleDropSink newSink = value as SimpleDropSink; + if (newSink != null) { + newSink.CanDrop += new EventHandler(this.DropSinkCanDrop); + newSink.Dropped += new EventHandler(this.DropSinkDropped); + newSink.ModelCanDrop += new EventHandler(this.DropSinkModelCanDrop); + newSink.ModelDropped += new EventHandler(this.DropSinkModelDropped); + } + } + } + private IDropSink dropSink; + + // Forward events from the drop sink to the control itself + void DropSinkCanDrop(object sender, OlvDropEventArgs e) { this.OnCanDrop(e); } + void DropSinkDropped(object sender, OlvDropEventArgs e) { this.OnDropped(e); } + void DropSinkModelCanDrop(object sender, ModelDropEventArgs e) { this.OnModelCanDrop(e); } + void DropSinkModelDropped(object sender, ModelDropEventArgs e) { this.OnModelDropped(e); } + + /// + /// This registry decides what control should be used to edit what cells, based + /// on the type of the value in the cell. + /// + /// + /// All instances of ObjectListView share the same editor registry. +// ReSharper disable FieldCanBeMadeReadOnly.Global + public static EditorRegistry EditorRegistry = new EditorRegistry(); +// ReSharper restore FieldCanBeMadeReadOnly.Global + + /// + /// Gets or sets the text that should be shown when there are no items in this list view. + /// + /// If the EmptyListMsgOverlay has been changed to something other than a TextOverlay, + /// this property does nothing + [Category("ObjectListView"), + Description("When the list has no items, show this message in the control"), + DefaultValue(null), + Localizable(true)] + public virtual String EmptyListMsg { + get { + TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; + return overlay == null ? null : overlay.Text; + } + set { + TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; + if (overlay != null) { + overlay.Text = value; + this.Invalidate(); + } + } + } + + /// + /// Gets or sets the font in which the List Empty message should be drawn + /// + /// If the EmptyListMsgOverlay has been changed to something other than a TextOverlay, + /// this property does nothing + [Category("ObjectListView"), + Description("What font should the 'list empty' message be drawn in?"), + DefaultValue(null)] + public virtual Font EmptyListMsgFont { + get { + TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; + return overlay == null ? null : overlay.Font; + } + set { + TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; + if (overlay != null) + overlay.Font = value; + } + } + + /// + /// Return the font for the 'list empty' message or a reasonable default + /// + [Browsable(false)] + public virtual Font EmptyListMsgFontOrDefault { + get { + return this.EmptyListMsgFont ?? new Font("Tahoma", 14); + } + } + + /// + /// Gets or sets the overlay responsible for drawing the List Empty msg. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IOverlay EmptyListMsgOverlay { + get { return this.emptyListMsgOverlay; } + set { + if (this.emptyListMsgOverlay != value) { + this.emptyListMsgOverlay = value; + this.Invalidate(); + } + } + } + private IOverlay emptyListMsgOverlay; + + /// + /// Gets the collection of objects that survive any filtering that may be in place. + /// + /// + /// + /// This collection is the result of filtering the current list of objects. + /// It is not a snapshot of the filtered list that was last used to build the control. + /// + /// + /// Normal warnings apply when using this with virtual lists. It will work, but it + /// may take a while. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable FilteredObjects { + get { + if (this.UseFiltering) + return this.FilterObjects(this.Objects, this.ModelFilter, this.ListFilter); + + return this.Objects; + } + } + + /// + /// Gets or sets the strategy object that will be used to build the Filter menu + /// + /// If this is null, no filter menu will be built. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public FilterMenuBuilder FilterMenuBuildStrategy { + get { return filterMenuBuilder; } + set { filterMenuBuilder = value; } + } + private FilterMenuBuilder filterMenuBuilder = new FilterMenuBuilder(); + + /// + /// Gets or sets the row that has keyboard focus + /// + /// + /// + /// Setting an object to be focused does *not* select it. If you want to select and focus a row, + /// use . + /// + /// + /// This property is not generally used and is only useful in specialized situations. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual Object FocusedObject { + get { return this.FocusedItem == null ? null : ((OLVListItem)this.FocusedItem).RowObject; } + set { + OLVListItem item = this.ModelToItem(value); + if (item != null) + item.Focused = true; + } + } + + /// + /// Hide the Groups collection so it's not visible in the Properties grid. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public new ListViewGroupCollection Groups { + get { return base.Groups; } + } + + /// + /// Gets or sets the image list from which group header will take their images + /// + /// If this is not set, then group headers will not show any images. + [Category("ObjectListView"), + Description("The image list from which group header will take their images"), + DefaultValue(null)] + public ImageList GroupImageList { + get { return this.groupImageList; } + set { + this.groupImageList = value; + if (this.Created) { + NativeMethods.SetGroupImageList(this, value); + } + } + } + private ImageList groupImageList; + + /// + /// Gets how the group label should be formatted when a group is empty or + /// contains more than one item + /// + /// + /// The given format string must have two placeholders: + /// + /// {0} - the original group title + /// {1} - the number of items in the group + /// + /// + /// "{0} [{1} items]" + [Category("ObjectListView"), + Description("The format to use when suffixing item counts to group titles"), + DefaultValue(null), + Localizable(true)] + public virtual string GroupWithItemCountFormat { + get { return groupWithItemCountFormat; } + set { groupWithItemCountFormat = value; } + } + private string groupWithItemCountFormat; + + /// + /// Return this.GroupWithItemCountFormat or a reasonable default + /// + [Browsable(false)] + public virtual string GroupWithItemCountFormatOrDefault { + get { + return String.IsNullOrEmpty(this.GroupWithItemCountFormat) ? "{0} [{1} items]" : this.GroupWithItemCountFormat; + } + } + + /// + /// Gets how the group label should be formatted when a group contains only a single item + /// + /// + /// The given format string must have two placeholders: + /// + /// {0} - the original group title + /// {1} - the number of items in the group (always 1) + /// + /// + /// "{0} [{1} item]" + [Category("ObjectListView"), + Description("The format to use when suffixing item counts to group titles"), + DefaultValue(null), + Localizable(true)] + public virtual string GroupWithItemCountSingularFormat { + get { return groupWithItemCountSingularFormat; } + set { groupWithItemCountSingularFormat = value; } + } + private string groupWithItemCountSingularFormat; + + /// + /// Gets GroupWithItemCountSingularFormat or a reasonable default + /// + [Browsable(false)] + public virtual string GroupWithItemCountSingularFormatOrDefault { + get { + return String.IsNullOrEmpty(this.GroupWithItemCountSingularFormat) ? "{0} [{1} item]" : this.GroupWithItemCountSingularFormat; + } + } + + /// + /// Gets or sets whether or not the groups in this ObjectListView should be collapsible. + /// + /// + /// This feature only works under Vista and later. + /// + [Browsable(true), + Category("ObjectListView"), + Description("Should the groups in this control be collapsible (Vista and later only)."), + DefaultValue(true)] + public bool HasCollapsibleGroups { + get { return hasCollapsibleGroups; } + set { hasCollapsibleGroups = value; } + } + private bool hasCollapsibleGroups = true; + + /// + /// Does this listview have a message that should be drawn when the list is empty? + /// + [Browsable(false)] + public virtual bool HasEmptyListMsg { + get { return !String.IsNullOrEmpty(this.EmptyListMsg); } + } + + /// + /// Get whether there are any overlays to be drawn + /// + [Browsable(false)] + public bool HasOverlays { + get { + return (this.Overlays.Count > 2 || + this.imageOverlay.Image != null || + !String.IsNullOrEmpty(this.textOverlay.Text)); + } + } + + /// + /// Gets the header control for the ListView + /// + [Browsable(false)] + public HeaderControl HeaderControl { + get { return this.headerControl ?? (this.headerControl = new HeaderControl(this)); } + } + private HeaderControl headerControl; + + /// + /// Gets or sets the font in which the text of the column headers will be drawn + /// + /// Individual columns can override this through their HeaderFormatStyle property. + [DefaultValue(null)] + [Browsable(false)] + [Obsolete("Use a HeaderFormatStyle instead", false)] + public Font HeaderFont { + get { return this.HeaderFormatStyle == null ? null : this.HeaderFormatStyle.Normal.Font; } + set { + if (value == null && this.HeaderFormatStyle == null) + return; + + if (this.HeaderFormatStyle == null) + this.HeaderFormatStyle = new HeaderFormatStyle(); + + this.HeaderFormatStyle.SetFont(value); + } + } + + /// + /// Gets or sets the style that will be used to draw the column headers of the listview + /// + /// + /// + /// This is only used when HeaderUsesThemes is false. + /// + /// + /// Individual columns can override this through their HeaderFormatStyle property. + /// + /// + [Category("ObjectListView"), + Description("What style will be used to draw the control's header"), + DefaultValue(null)] + public HeaderFormatStyle HeaderFormatStyle { + get { return this.headerFormatStyle; } + set { this.headerFormatStyle = value; } + } + private HeaderFormatStyle headerFormatStyle; + + /// + /// Gets or sets the maximum height of the header. -1 means no maximum. + /// + [Category("ObjectListView"), + Description("What is the maximum height of the header? -1 means no maximum"), + DefaultValue(-1)] + public int HeaderMaximumHeight + { + get { return headerMaximumHeight; } + set { headerMaximumHeight = value; } + } + private int headerMaximumHeight = -1; + + /// + /// Gets or sets the minimum height of the header. -1 means no minimum. + /// + [Category("ObjectListView"), + Description("What is the minimum height of the header? -1 means no minimum"), + DefaultValue(-1)] + public int HeaderMinimumHeight + { + get { return headerMinimumHeight; } + set { headerMinimumHeight = value; } + } + private int headerMinimumHeight = -1; + + /// + /// Gets or sets whether the header will be drawn strictly according to the OS's theme. + /// + /// + /// + /// If this is set to true, the header will be rendered completely by the system, without + /// any of ObjectListViews fancy processing -- no images in header, no filter indicators, + /// no word wrapping, no header styling, no checkboxes. + /// + /// If this is set to false, ObjectListView will render the header as it thinks best. + /// If no special features are required, then ObjectListView will delegate rendering to the OS. + /// Otherwise, ObjectListView will draw the header according to the configuration settings. + /// + /// + /// The effect of not being themed will be different from OS to OS. At + /// very least, the sort indicator will not be standard. + /// + /// + [Category("ObjectListView"), + Description("Will the column headers be drawn strictly according to OS theme?"), + DefaultValue(false)] + public bool HeaderUsesThemes { + get { return this.headerUsesThemes; } + set { this.headerUsesThemes = value; } + } + private bool headerUsesThemes; + + /// + /// Gets or sets the whether the text in the header will be word wrapped. + /// + /// + /// Line breaks will be applied between words. Words that are too long + /// will still be ellipsed. + /// + /// As with all settings that make the header look different, HeaderUsesThemes must be set to false, otherwise + /// the OS will be responsible for drawing the header, and it does not allow word wrapped text. + /// + /// + [Category("ObjectListView"), + Description("Will the text of the column headers be word wrapped?"), + DefaultValue(false)] + public bool HeaderWordWrap { + get { return this.headerWordWrap; } + set { + this.headerWordWrap = value; + if (this.headerControl != null) + this.headerControl.WordWrap = value; + } + } + private bool headerWordWrap; + + /// + /// Gets the tool tip that shows tips for the column headers + /// + [Browsable(false)] + public ToolTipControl HeaderToolTip { + get { + return this.HeaderControl.ToolTip; + } + } + + /// + /// Gets the index of the row that the mouse is currently over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual int HotRowIndex { + get { return this.hotRowIndex; } + protected set { this.hotRowIndex = value; } + } + private int hotRowIndex; + + /// + /// Gets the index of the subitem that the mouse is currently over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual int HotColumnIndex { + get { return this.hotColumnIndex; } + protected set { this.hotColumnIndex = value; } + } + private int hotColumnIndex; + + /// + /// Gets the part of the item/subitem that the mouse is currently over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual HitTestLocation HotCellHitLocation { + get { return this.hotCellHitLocation; } + protected set { this.hotCellHitLocation = value; } + } + private HitTestLocation hotCellHitLocation; + + /// + /// Gets an extended indication of the part of item/subitem/group that the mouse is currently over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual HitTestLocationEx HotCellHitLocationEx + { + get { return this.hotCellHitLocationEx; } + protected set { this.hotCellHitLocationEx = value; } + } + private HitTestLocationEx hotCellHitLocationEx; + + /// + /// Gets the group that the mouse is over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVGroup HotGroup + { + get { return hotGroup; } + internal set { hotGroup = value; } + } + private OLVGroup hotGroup; + + /// + /// The index of the item that is 'hot', i.e. under the cursor. -1 means no item. + /// + [Browsable(false), + Obsolete("Use HotRowIndex instead", false)] + public virtual int HotItemIndex { + get { return this.HotRowIndex; } + } + + /// + /// What sort of formatting should be applied to the row under the cursor? + /// + /// + /// + /// This only takes effect when UseHotItem is true. + /// + /// If the style has an overlay, it must be set + /// *before* assigning it to this property. Adding it afterwards will be ignored. + /// + [Category("ObjectListView"), + Description("How should the row under the cursor be highlighted"), + DefaultValue(null)] + public virtual HotItemStyle HotItemStyle { + get { return this.hotItemStyle; } + set { + if (this.HotItemStyle != null) + this.RemoveOverlay(this.HotItemStyle.Overlay); + this.hotItemStyle = value; + if (this.HotItemStyle != null) + this.AddOverlay(this.HotItemStyle.Overlay); + } + } + private HotItemStyle hotItemStyle; + + /// + /// Gets the installed hot item style or a reasonable default. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual HotItemStyle HotItemStyleOrDefault { + get { return this.HotItemStyle ?? ObjectListView.DefaultHotItemStyle; } + } + + /// + /// What sort of formatting should be applied to hyperlinks? + /// + [Category("ObjectListView"), + Description("How should hyperlinks be drawn"), + DefaultValue(null)] + public virtual HyperlinkStyle HyperlinkStyle { + get { return this.hyperlinkStyle; } + set { this.hyperlinkStyle = value; } + } + private HyperlinkStyle hyperlinkStyle; + + /// + /// What color should be used for the background of selected rows? + /// + [Category("ObjectListView"), + Description("The background of selected rows when the control is owner drawn"), + DefaultValue(typeof(Color), "")] + public virtual Color SelectedBackColor { + get { return this.selectedBackColor; } + set { this.selectedBackColor = value; } + } + private Color selectedBackColor = Color.Empty; + + /// + /// Return the color should be used for the background of selected rows or a reasonable default + /// + [Browsable(false)] + public virtual Color SelectedBackColorOrDefault { + get { + return this.SelectedBackColor.IsEmpty ? SystemColors.Highlight : this.SelectedBackColor; + } + } + + /// + /// What color should be used for the foreground of selected rows? + /// + [Category("ObjectListView"), + Description("The foreground color of selected rows (when the control is owner drawn)"), + DefaultValue(typeof(Color), "")] + public virtual Color SelectedForeColor { + get { return this.selectedForeColor; } + set { this.selectedForeColor = value; } + } + private Color selectedForeColor = Color.Empty; + + /// + /// Return the color should be used for the foreground of selected rows or a reasonable default + /// + [Browsable(false)] + public virtual Color SelectedForeColorOrDefault { + get { + return this.SelectedForeColor.IsEmpty ? SystemColors.HighlightText : this.SelectedForeColor; + } + } + + /// + /// + /// + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Obsolete("Use SelectedBackColor instead")] + public virtual Color HighlightBackgroundColor { get { return this.SelectedBackColor; } set { this.SelectedBackColor = value; } } + + /// + /// + /// + [Obsolete("Use SelectedBackColorOrDefault instead")] + public virtual Color HighlightBackgroundColorOrDefault { get { return this.SelectedBackColorOrDefault; } } + + /// + /// + /// + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Obsolete("Use SelectedForeColor instead")] + public virtual Color HighlightForegroundColor { get { return this.SelectedForeColor; } set { this.SelectedForeColor = value; } } + + /// + /// + /// + [Obsolete("Use SelectedForeColorOrDefault instead")] + public virtual Color HighlightForegroundColorOrDefault { get { return this.SelectedForeColorOrDefault; } } + + /// + /// + /// + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Obsolete("Use UnfocusedSelectedBackColor instead")] + public virtual Color UnfocusedHighlightBackgroundColor { get { return this.UnfocusedSelectedBackColor; } set { this.UnfocusedSelectedBackColor = value; } } + + /// + /// + /// + [Obsolete("Use UnfocusedSelectedBackColorOrDefault instead")] + public virtual Color UnfocusedHighlightBackgroundColorOrDefault { get { return this.UnfocusedSelectedBackColorOrDefault; } } + + /// + /// + /// + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Obsolete("Use UnfocusedSelectedForeColor instead")] + public virtual Color UnfocusedHighlightForegroundColor { get { return this.UnfocusedSelectedForeColor; } set { this.UnfocusedSelectedForeColor = value; } } + + /// + /// + /// + [Obsolete("Use UnfocusedSelectedForeColorOrDefault instead")] + public virtual Color UnfocusedHighlightForegroundColorOrDefault { get { return this.UnfocusedSelectedForeColorOrDefault; } } + + /// + /// Gets or sets whether or not hidden columns should be included in the text representation + /// of rows that are copied or dragged to another application. If this is false (the default), + /// only visible columns will be included. + /// + [Category("ObjectListView"), + Description("When rows are copied or dragged, will data in hidden columns be included in the text? If this is false, only visible columns will be included."), + DefaultValue(false)] + public virtual bool IncludeHiddenColumnsInDataTransfer + { + get { return includeHiddenColumnsInDataTransfer; } + set { includeHiddenColumnsInDataTransfer = value; } + } + private bool includeHiddenColumnsInDataTransfer; + + /// + /// Gets or sets whether or not hidden columns should be included in the text representation + /// of rows that are copied or dragged to another application. If this is false (the default), + /// only visible columns will be included. + /// + [Category("ObjectListView"), + Description("When rows are copied, will column headers be in the text?."), + DefaultValue(false)] + public virtual bool IncludeColumnHeadersInCopy + { + get { return includeColumnHeadersInCopy; } + set { includeColumnHeadersInCopy = value; } + } + private bool includeColumnHeadersInCopy; + + /// + /// Return true if a cell edit operation is currently happening + /// + [Browsable(false)] + public virtual bool IsCellEditing { + get { return this.cellEditor != null; } + } + + /// + /// Return true if the ObjectListView is being used within the development environment. + /// + [Browsable(false)] + public virtual bool IsDesignMode { + get { return this.DesignMode; } + } + + /// + /// Gets whether or not the current list is filtering its contents + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual bool IsFiltering { + get { return this.UseFiltering && (this.ModelFilter != null || this.ListFilter != null); } + } + + /// + /// When the user types into a list, should the values in the current sort column be searched to find a match? + /// If this is false, the primary column will always be used regardless of the sort column. + /// + /// When this is true, the behavior is like that of ITunes. + [Category("ObjectListView"), + Description("When the user types into a list, should the values in the current sort column be searched to find a match?"), + DefaultValue(true)] + public virtual bool IsSearchOnSortColumn { + get { return isSearchOnSortColumn; } + set { isSearchOnSortColumn = value; } + } + private bool isSearchOnSortColumn = true; + + /// + /// Gets or sets if this control will use a SimpleDropSink to receive drops + /// + /// + /// + /// Setting this replaces any previous DropSink. + /// + /// + /// After setting this to true, the SimpleDropSink will still need to be configured + /// to say when it can accept drops and what should happen when something is dropped. + /// The need to do these things makes this property mostly useless :( + /// + /// + [Category("ObjectListView"), + Description("Should this control will use a SimpleDropSink to receive drops."), + DefaultValue(false)] + public virtual bool IsSimpleDropSink { + get { return this.DropSink != null; } + set { + this.DropSink = value ? new SimpleDropSink() : null; + } + } + + /// + /// Gets or sets if this control will use a SimpleDragSource to initiate drags + /// + /// Setting this replaces any previous DragSource + [Category("ObjectListView"), + Description("Should this control use a SimpleDragSource to initiate drags out from this control"), + DefaultValue(false)] + public virtual bool IsSimpleDragSource { + get { return this.DragSource != null; } + set { + this.DragSource = value ? new SimpleDragSource() : null; + } + } + + /// + /// Hide the Items collection so it's not visible in the Properties grid. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public new ListViewItemCollection Items { + get { return base.Items; } + } + + /// + /// This renderer draws the items when in the list is in non-details view. + /// In details view, the renderers for the individuals columns are responsible. + /// + [Category("ObjectListView"), + Description("The owner drawn renderer that draws items when the list is in non-Details view."), + DefaultValue(null)] + public IRenderer ItemRenderer { + get { return itemRenderer; } + set { itemRenderer = value; } + } + private IRenderer itemRenderer; + + /// + /// Which column did we last sort by + /// + /// This is an alias for PrimarySortColumn + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVColumn LastSortColumn { + get { return this.PrimarySortColumn; } + set { this.PrimarySortColumn = value; } + } + + /// + /// Which direction did we last sort + /// + /// This is an alias for PrimarySortOrder + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortOrder LastSortOrder { + get { return this.PrimarySortOrder; } + set { this.PrimarySortOrder = value; } + } + + /// + /// Gets or sets the filter that is applied to our whole list of objects. + /// + /// + /// The list is updated immediately to reflect this filter. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IListFilter ListFilter { + get { return listFilter; } + set { + listFilter = value; + if (this.UseFiltering) + this.UpdateFiltering(); + } + } + private IListFilter listFilter; + + /// + /// Gets or sets the filter that is applied to each model objects in the list + /// + /// + /// You may want to consider using instead of this property, + /// since AdditionalFilter combines with column filtering at runtime. Setting this property simply + /// replaces any column filter the user may have given. + /// + /// The list is updated immediately to reflect this filter. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IModelFilter ModelFilter { + get { return modelFilter; } + set { + modelFilter = value; + this.NotifyNewModelFilter(); + if (this.UseFiltering) { + this.UpdateFiltering(); + + // When the filter changes, it's likely/possible that the selection has also changed. + // It's expensive to see if the selection has actually changed (for large virtual lists), + // so we just fake a selection changed event, just in case. SF #144 + this.OnSelectedIndexChanged(EventArgs.Empty); + } + } + } + private IModelFilter modelFilter; + + /// + /// Gets the hit test info last time the mouse was moved. + /// + /// Useful for hot item processing. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OlvListViewHitTestInfo MouseMoveHitTest { + get { return mouseMoveHitTest; } + private set { mouseMoveHitTest = value; } + } + private OlvListViewHitTestInfo mouseMoveHitTest; + + /// + /// Gets or sets the list of groups shown by the listview. + /// + /// + /// This property does not work like the .NET Groups property. It should + /// be treated as a read-only property. + /// Changes made to the list are NOT reflected in the ListView itself -- it is pointless to add + /// or remove groups to/from this list. Such modifications will do nothing. + /// To do such things, you must listen for + /// BeforeCreatingGroups or AboutToCreateGroups events, and change the list of + /// groups in those events. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IList OLVGroups { + get { return this.olvGroups; } + set { this.olvGroups = value; } + } + private IList olvGroups; + + /// + /// Gets or sets the collection of OLVGroups that are collapsed. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IEnumerable CollapsedGroups { + get + { + if (this.OLVGroups != null) + { + foreach (OLVGroup group in this.OLVGroups) + { + if (group.Collapsed) + yield return group; + } + } + } + set + { + if (this.OLVGroups == null) + return; + + Hashtable shouldCollapse = new Hashtable(); + if (value != null) + { + foreach (OLVGroup group in value) + shouldCollapse[group.Key] = true; + } + foreach (OLVGroup group in this.OLVGroups) + { + group.Collapsed = shouldCollapse.ContainsKey(group.Key); + } + + } + } + + /// + /// Gets or sets whether the user wants to owner draw the header control + /// themselves. If this is false (the default), ObjectListView will use + /// custom drawing to render the header, if needed. + /// + /// + /// If you listen for the DrawColumnHeader event, you need to set this to true, + /// otherwise your event handler will not be called. + /// + [Category("ObjectListView"), + Description("Should the DrawColumnHeader event be triggered"), + DefaultValue(false)] + public bool OwnerDrawnHeader { + get { return ownerDrawnHeader; } + set { ownerDrawnHeader = value; } + } + private bool ownerDrawnHeader; + + /// + /// Get/set the collection of objects that this list will show + /// + /// + /// + /// The contents of the control will be updated immediately after setting this property. + /// + /// This method preserves selection, if possible. Use if + /// you do not want to preserve the selection. Preserving selection is the slowest part of this + /// code and performance is O(n) where n is the number of selected rows. + /// This method is not thread safe. + /// The property DOES work on virtual lists: setting is problem-free, but if you try to get it + /// and the list has 10 million objects, it may take some time to return. + /// This collection is unfiltered. Use to access just those objects + /// that survive any installed filters. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable Objects { + get { return this.objects; } + set { this.SetObjects(value, true); } + } + private IEnumerable objects; + + /// + /// Gets the collection of objects that will be considered when creating clusters + /// (which are used to generate Excel-like column filters) + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable ObjectsForClustering { + get { return this.Objects; } + } + + /// + /// Gets or sets the image that will be drawn over the top of the ListView + /// + [Category("ObjectListView"), + Description("The image that will be drawn over the top of the ListView"), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public ImageOverlay OverlayImage { + get { return this.imageOverlay; } + set { + if (this.imageOverlay == value) + return; + + this.RemoveOverlay(this.imageOverlay); + this.imageOverlay = value; + this.AddOverlay(this.imageOverlay); + } + } + private ImageOverlay imageOverlay; + + /// + /// Gets or sets the text that will be drawn over the top of the ListView + /// + [Category("ObjectListView"), + Description("The text that will be drawn over the top of the ListView"), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public TextOverlay OverlayText { + get { return this.textOverlay; } + set { + if (this.textOverlay == value) + return; + + this.RemoveOverlay(this.textOverlay); + this.textOverlay = value; + this.AddOverlay(this.textOverlay); + } + } + private TextOverlay textOverlay; + + /// + /// Gets or sets the transparency of all the overlays. + /// 0 is completely transparent, 255 is completely opaque. + /// + /// + /// This is obsolete. Use Transparency on each overlay. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int OverlayTransparency { + get { return this.overlayTransparency; } + set { this.overlayTransparency = Math.Min(255, Math.Max(0, value)); } + } + private int overlayTransparency = 128; + + /// + /// Gets the list of overlays that will be drawn on top of the ListView + /// + /// + /// You can add new overlays and remove overlays that you have added, but + /// don't mess with the overlays that you didn't create. + /// + [Browsable(false)] + protected IList Overlays { + get { return this.overlays; } + } + private readonly List overlays = new List(); + + /// + /// Gets or sets whether the ObjectListView will be owner drawn. Defaults to true. + /// + /// + /// + /// When this is true, all of ObjectListView's neat features are available. + /// + /// We have to reimplement this property, even though we just call the base + /// property, in order to change the [DefaultValue] to true. + /// + /// + [Category("Appearance"), + Description("Should the ListView do its own rendering"), + DefaultValue(true)] + public new bool OwnerDraw { + get { return base.OwnerDraw; } + set { base.OwnerDraw = value; } + } + + /// + /// Gets or sets whether or not primary checkboxes will persistent their values across list rebuild + /// and filtering operations. + /// + /// + /// + /// This property is only useful when you don't explicitly set CheckStateGetter/Putter. + /// If you use CheckStateGetter/Putter, the checkedness of a row will already be persisted + /// by those methods. + /// + /// This defaults to true. If this is false, checkboxes will lose their values when the + /// list if rebuild or filtered. + /// If you set it to false on virtual lists, + /// you have to install CheckStateGetter/Putters. + /// + [Category("ObjectListView"), + Description("Will primary checkboxes persistent their values across list rebuilds"), + DefaultValue(true)] + public virtual bool PersistentCheckBoxes { + get { return persistentCheckBoxes; } + set { + if (persistentCheckBoxes == value) + return; + persistentCheckBoxes = value; + this.ClearPersistentCheckState(); + } + } + private bool persistentCheckBoxes = true; + + /// + /// Gets or sets a dictionary that remembers the check state of model objects + /// + /// This is used when PersistentCheckBoxes is true and for virtual lists. + protected Dictionary CheckStateMap { + get { return checkStateMap ?? (checkStateMap = new Dictionary()); } + set { checkStateMap = value; } + } + private Dictionary checkStateMap; + + /// + /// Which column did we last sort by + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVColumn PrimarySortColumn { + get { return this.primarySortColumn; } + set { + this.primarySortColumn = value; + if (this.TintSortColumn) + this.SelectedColumn = value; + } + } + private OLVColumn primarySortColumn; + + /// + /// Which direction did we last sort + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortOrder PrimarySortOrder { + get { return primarySortOrder; } + set { primarySortOrder = value; } + } + private SortOrder primarySortOrder; + + /// + /// Gets or sets if non-editable checkboxes are drawn as disabled. Default is false. + /// + /// + /// This only has effect in owner drawn mode. + /// + [Category("ObjectListView"), + Description("Should non-editable checkboxes be drawn as disabled?"), + DefaultValue(false)] + public virtual bool RenderNonEditableCheckboxesAsDisabled { + get { return renderNonEditableCheckboxesAsDisabled; } + set { renderNonEditableCheckboxesAsDisabled = value; } + } + private bool renderNonEditableCheckboxesAsDisabled; + + /// + /// Specify the height of each row in the control in pixels. + /// + /// The row height in a listview is normally determined by the font size and the small image list size. + /// This setting allows that calculation to be overridden (within reason: you still cannot set the line height to be + /// less than the line height of the font used in the control). + /// Setting it to -1 means use the normal calculation method. + /// This feature is experimental! Strange things may happen to your program, + /// your spouse or your pet if you use it. + /// + [Category("ObjectListView"), + Description("Specify the height of each row in pixels. -1 indicates default height"), + DefaultValue(-1)] + public virtual int RowHeight { + get { return rowHeight; } + set { + if (value < 1) + rowHeight = -1; + else + rowHeight = value; + if (this.DesignMode) + return; + this.SetupBaseImageList(); + if (this.CheckBoxes) + this.InitializeStateImageList(); + } + } + private int rowHeight = -1; + + /// + /// How many pixels high is each row? + /// + [Browsable(false)] + public virtual int RowHeightEffective { + get { + switch (this.View) { + case View.List: + case View.SmallIcon: + case View.Details: + return Math.Max(this.SmallImageSize.Height, this.Font.Height); + + case View.Tile: + return this.TileSize.Height; + + case View.LargeIcon: + if (this.LargeImageList == null) + return this.Font.Height; + + return Math.Max(this.LargeImageList.ImageSize.Height, this.Font.Height); + + default: + // This should never happen + return 0; + } + } + } + + /// + /// How many rows appear on each page of this control + /// + [Browsable(false)] + public virtual int RowsPerPage { + get { + return NativeMethods.GetCountPerPage(this); + } + } + + /// + /// Get/set the column that will be used to resolve comparisons that are equal when sorting. + /// + /// There is no user interface for this setting. It must be set programmatically. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVColumn SecondarySortColumn { + get { return this.secondarySortColumn; } + set { this.secondarySortColumn = value; } + } + private OLVColumn secondarySortColumn; + + /// + /// When the SecondarySortColumn is used, in what order will it compare results? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortOrder SecondarySortOrder { + get { return this.secondarySortOrder; } + set { this.secondarySortOrder = value; } + } + private SortOrder secondarySortOrder = SortOrder.None; + + /// + /// Gets or sets if all rows should be selected when the user presses Ctrl-A + /// + [Category("ObjectListView"), + Description("Should the control select all rows when the user presses Ctrl-A?"), + DefaultValue(true)] + public virtual bool SelectAllOnControlA { + get { return selectAllOnControlA; } + set { selectAllOnControlA = value; } + } + private bool selectAllOnControlA = true; + + /// + /// When the user right clicks on the column headers, should a menu be presented which will allow + /// them to choose which columns will be shown in the view? + /// + /// This is just a compatibility wrapper for the SelectColumnsOnRightClickBehaviour + /// property. + [Category("ObjectListView"), + Description("When the user right clicks on the column headers, should a menu be presented which will allow them to choose which columns will be shown in the view?"), + DefaultValue(true)] + public virtual bool SelectColumnsOnRightClick { + get { return this.SelectColumnsOnRightClickBehaviour != ColumnSelectBehaviour.None; } + set { + if (value) { + if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.None) + this.SelectColumnsOnRightClickBehaviour = ColumnSelectBehaviour.InlineMenu; + } else { + this.SelectColumnsOnRightClickBehaviour = ColumnSelectBehaviour.None; + } + } + } + + /// + /// Gets or sets how the user will be able to select columns when the header is right clicked + /// + [Category("ObjectListView"), + Description("When the user right clicks on the column headers, how will the user be able to select columns?"), + DefaultValue(ColumnSelectBehaviour.InlineMenu)] + public virtual ColumnSelectBehaviour SelectColumnsOnRightClickBehaviour { + get { return selectColumnsOnRightClickBehaviour; } + set { selectColumnsOnRightClickBehaviour = value; } + } + private ColumnSelectBehaviour selectColumnsOnRightClickBehaviour = ColumnSelectBehaviour.InlineMenu; + + /// + /// When the column select menu is open, should it stay open after an item is selected? + /// Staying open allows the user to turn more than one column on or off at a time. + /// + /// This only works when SelectColumnsOnRightClickBehaviour is set to InlineMenu. + /// It has no effect when the behaviour is set to SubMenu. + [Category("ObjectListView"), + Description("When the column select inline menu is open, should it stay open after an item is selected?"), + DefaultValue(true)] + public virtual bool SelectColumnsMenuStaysOpen { + get { return selectColumnsMenuStaysOpen; } + set { selectColumnsMenuStaysOpen = value; } + } + private bool selectColumnsMenuStaysOpen = true; + + /// + /// Gets or sets the column that is drawn with a slight tint. + /// + /// + /// + /// If TintSortColumn is true, the sort column will automatically + /// be made the selected column. + /// + /// + /// The colour of the tint is controlled by SelectedColumnTint. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVColumn SelectedColumn { + get { return this.selectedColumn; } + set { + this.selectedColumn = value; + if (value == null) { + this.RemoveDecoration(this.selectedColumnDecoration); + } else { + if (!this.HasDecoration(this.selectedColumnDecoration)) + this.AddDecoration(this.selectedColumnDecoration); + } + } + } + private OLVColumn selectedColumn; + private readonly TintedColumnDecoration selectedColumnDecoration = new TintedColumnDecoration(); + + /// + /// Gets or sets the decoration that will be drawn on all selected rows + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IDecoration SelectedRowDecoration { + get { return this.selectedRowDecoration; } + set { this.selectedRowDecoration = value; } + } + private IDecoration selectedRowDecoration; + + /// + /// What color should be used to tint the selected column? + /// + /// + /// The tint color must be alpha-blendable, so if the given color is solid + /// (i.e. alpha = 255), it will be changed to have a reasonable alpha value. + /// + [Category("ObjectListView"), + Description("The color that will be used to tint the selected column"), + DefaultValue(typeof(Color), "")] + public virtual Color SelectedColumnTint { + get { return selectedColumnTint; } + set { + this.selectedColumnTint = value.A == 255 ? Color.FromArgb(15, value) : value; + this.selectedColumnDecoration.Tint = this.selectedColumnTint; + } + } + private Color selectedColumnTint = Color.Empty; + + /// + /// Gets or sets the index of the row that is currently selected. + /// When getting the index, if no row is selected,or more than one is selected, return -1. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual int SelectedIndex { + get { return this.SelectedIndices.Count == 1 ? this.SelectedIndices[0] : -1; } + set { + this.SelectedIndices.Clear(); + if (value >= 0 && value < this.Items.Count) + this.SelectedIndices.Add(value); + } + } + + /// + /// Gets or sets the ListViewItem that is currently selected . If no row is selected, or more than one is selected, return null. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVListItem SelectedItem { + get { + return this.SelectedIndices.Count == 1 ? this.GetItem(this.SelectedIndices[0]) : null; + } + set { + this.SelectedIndices.Clear(); + if (value != null) + this.SelectedIndices.Add(value.Index); + } + } + + /// + /// Gets the model object from the currently selected row, if there is only one row selected. + /// If no row is selected, or more than one is selected, returns null. + /// When setting, this will select the row that is displaying the given model object and focus on it. + /// All other rows are deselected. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual Object SelectedObject { + get { + return this.SelectedIndices.Count == 1 ? this.GetModelObject(this.SelectedIndices[0]) : null; + } + set { + // If the given model is already selected, don't do anything else (prevents an flicker) + object selectedObject = this.SelectedObject; + if (selectedObject != null && selectedObject.Equals(value)) + return; + + this.SelectedIndices.Clear(); + this.SelectObject(value, true); + } + } + + /// + /// Get the model objects from the currently selected rows. If no row is selected, the returned List will be empty. + /// When setting this value, select the rows that is displaying the given model objects. All other rows are deselected. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IList SelectedObjects { + get { + ArrayList list = new ArrayList(); + foreach (int index in this.SelectedIndices) + list.Add(this.GetModelObject(index)); + return list; + } + set { + this.SelectedIndices.Clear(); + this.SelectObjects(value); + } + } + + /// + /// When the user right clicks on the column headers, should a menu be presented which will allow + /// them to choose common tasks to perform on the listview? + /// + [Category("ObjectListView"), + Description("When the user right clicks on the column headers, should a menu be presented which will allow them to perform common tasks on the listview?"), + DefaultValue(false)] + public virtual bool ShowCommandMenuOnRightClick { + get { return showCommandMenuOnRightClick; } + set { showCommandMenuOnRightClick = value; } + } + private bool showCommandMenuOnRightClick; + + /// + /// Gets or sets whether this ObjectListView will show Excel like filtering + /// menus when the header control is right clicked + /// + [Category("ObjectListView"), + Description("If this is true, right clicking on a column header will show a Filter menu option"), + DefaultValue(true)] + public bool ShowFilterMenuOnRightClick { + get { return showFilterMenuOnRightClick; } + set { showFilterMenuOnRightClick = value; } + } + private bool showFilterMenuOnRightClick = true; + + /// + /// Should this list show its items in groups? + /// + [Category("Appearance"), + Description("Should the list view show items in groups?"), + DefaultValue(true)] + public new virtual bool ShowGroups { + get { return base.ShowGroups; } + set { + this.GroupImageList = this.GroupImageList; + base.ShowGroups = value; + } + } + + /// + /// Should the list view show a bitmap in the column header to show the sort direction? + /// + /// + /// The only reason for not wanting to have sort indicators is that, on pre-XP versions of + /// Windows, having sort indicators required the ListView to have a small image list, and + /// as soon as you give a ListView a SmallImageList, the text of column 0 is bumped 16 + /// pixels to the right, even if you never used an image. + /// + [Category("ObjectListView"), + Description("Should the list view show sort indicators in the column headers?"), + DefaultValue(true)] + public virtual bool ShowSortIndicators { + get { return showSortIndicators; } + set { showSortIndicators = value; } + } + private bool showSortIndicators; + + /// + /// Should the list view show images on subitems? + /// + /// + /// Virtual lists have to be owner drawn in order to show images on subitems + /// + [Category("ObjectListView"), + Description("Should the list view show images on subitems?"), + DefaultValue(false)] + public virtual bool ShowImagesOnSubItems { + get { return showImagesOnSubItems; } + set { + showImagesOnSubItems = value; + if (this.Created) + this.ApplyExtendedStyles(); + if (value && this.VirtualMode) + this.OwnerDraw = true; + } + } + private bool showImagesOnSubItems; + + /// + /// This property controls whether group labels will be suffixed with a count of items. + /// + /// + /// The format of the suffix is controlled by GroupWithItemCountFormat/GroupWithItemCountSingularFormat properties + /// + [Category("ObjectListView"), + Description("Will group titles be suffixed with a count of the items in the group?"), + DefaultValue(false)] + public virtual bool ShowItemCountOnGroups { + get { return showItemCountOnGroups; } + set { showItemCountOnGroups = value; } + } + private bool showItemCountOnGroups; + + /// + /// Gets or sets whether the control will show column headers in all + /// views (true), or only in Details view (false) + /// + /// + /// + /// This property is not working correctly. JPP 2010/04/06. + /// It works fine if it is set before the control is created. + /// But if it turned off once the control is created, the control + /// loses its checkboxes (weird!) + /// + /// + /// To changed this setting after the control is created, things + /// are complicated. If it is off and we want it on, we have + /// to change the View and the header will appear. If it is currently + /// on and we want to turn it off, we have to both change the view + /// AND recreate the handle. Recreating the handle is a problem + /// since it makes our checkbox style disappear. + /// + /// + /// This property doesn't work on XP. + /// + [Category("ObjectListView"), + Description("Will the control will show column headers in all views?"), + DefaultValue(true)] + public bool ShowHeaderInAllViews { + get { return ObjectListView.IsVistaOrLater && showHeaderInAllViews; } + set { + if (showHeaderInAllViews == value) + return; + + showHeaderInAllViews = value; + + // If the control isn't already created, everything is fine. + if (!this.Created) + return; + + // If the header is being hidden, we have to recreate the control + // to remove the style (not sure why this is) + if (showHeaderInAllViews) + this.ApplyExtendedStyles(); + else + this.RecreateHandle(); + + // Still more complications. The change doesn't become visible until the View is changed + if (this.View != View.Details) { + View temp = this.View; + this.View = View.Details; + this.View = temp; + } + } + } + private bool showHeaderInAllViews = true; + + /// + /// Override the SmallImageList property so we can correctly shadow its operations. + /// + /// If you use the RowHeight property to specify the row height, the SmallImageList + /// must be fully initialised before setting/changing the RowHeight. If you add new images to the image + /// list after setting the RowHeight, you must assign the imagelist to the control again. Something as simple + /// as this will work: + /// listView1.SmallImageList = listView1.SmallImageList; + /// + public new ImageList SmallImageList { + get { return this.shadowedImageList; } + set { + this.shadowedImageList = value; + if (this.UseSubItemCheckBoxes) + this.SetupSubItemCheckBoxes(); + this.SetupBaseImageList(); + } + } + private ImageList shadowedImageList; + + /// + /// Return the size of the images in the small image list or a reasonable default + /// + [Browsable(false)] + public virtual Size SmallImageSize { + get { + return this.BaseSmallImageList == null ? new Size(16, 16) : this.BaseSmallImageList.ImageSize; + } + } + + /// + /// When the listview is grouped, should the items be sorted by the primary column? + /// If this is false, the items will be sorted by the same column as they are grouped. + /// + /// + /// + /// The primary column is always column 0 and is unrelated to the PrimarySort column. + /// + /// + [Category("ObjectListView"), + Description("When the listview is grouped, should the items be sorted by the primary column? If this is false, the items will be sorted by the same column as they are grouped."), + DefaultValue(true)] + public virtual bool SortGroupItemsByPrimaryColumn { + get { return this.sortGroupItemsByPrimaryColumn; } + set { this.sortGroupItemsByPrimaryColumn = value; } + } + private bool sortGroupItemsByPrimaryColumn = true; + + /// + /// When the listview is grouped, how many pixels should exist between the end of one group and the + /// beginning of the next? + /// + [Category("ObjectListView"), + Description("How many pixels of space will be between groups"), + DefaultValue(0)] + public virtual int SpaceBetweenGroups { + get { return this.spaceBetweenGroups; } + set { + if (this.spaceBetweenGroups == value) + return; + + this.spaceBetweenGroups = value; + this.SetGroupSpacing(); + } + } + private int spaceBetweenGroups; + + private void SetGroupSpacing() { + if (!this.IsHandleCreated) + return; + + NativeMethods.LVGROUPMETRICS metrics = new NativeMethods.LVGROUPMETRICS(); + metrics.cbSize = ((uint)Marshal.SizeOf(typeof(NativeMethods.LVGROUPMETRICS))); + metrics.mask = (uint)GroupMetricsMask.LVGMF_BORDERSIZE; + metrics.Bottom = (uint)this.SpaceBetweenGroups; + NativeMethods.SetGroupMetrics(this, metrics); + } + + /// + /// Should the sort column show a slight tinge? + /// + [Category("ObjectListView"), + Description("Should the sort column show a slight tinting?"), + DefaultValue(false)] + public virtual bool TintSortColumn { + get { return this.tintSortColumn; } + set { + this.tintSortColumn = value; + if (value && this.PrimarySortColumn != null) + this.SelectedColumn = this.PrimarySortColumn; + else + this.SelectedColumn = null; + } + } + private bool tintSortColumn; + + /// + /// Should each row have a tri-state checkbox? + /// + /// + /// If this is true, the user can choose the third state (normally Indeterminate). Otherwise, user clicks + /// alternate between checked and unchecked. CheckStateGetter can still return Indeterminate when this + /// setting is false. + /// + [Category("ObjectListView"), + Description("Should the primary column have a checkbox that behaves as a tri-state checkbox?"), + DefaultValue(false)] + public virtual bool TriStateCheckBoxes { + get { return triStateCheckBoxes; } + set { + triStateCheckBoxes = value; + if (value && !this.CheckBoxes) + this.CheckBoxes = true; + this.InitializeStateImageList(); + } + } + private bool triStateCheckBoxes; + + /// + /// Get or set the index of the top item of this listview + /// + /// + /// + /// This property only works when the listview is in Details view and not showing groups. + /// + /// + /// The reason that it does not work when showing groups is that, when groups are enabled, + /// the Windows msg LVM_GETTOPINDEX always returns 0, regardless of the + /// scroll position. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual int TopItemIndex { + get { + if (this.View == View.Details && this.IsHandleCreated) + return NativeMethods.GetTopIndex(this); + + return -1; + } + set { + int newTopIndex = Math.Min(value, this.GetItemCount() - 1); + if (this.View != View.Details || newTopIndex < 0) + return; + + try { + this.TopItem = this.Items[newTopIndex]; + + // Setting the TopItem sometimes gives off by one errors, + // that (bizarrely) are correct on a second attempt + if (this.TopItem != null && this.TopItem.Index != newTopIndex) + this.TopItem = this.GetItem(newTopIndex); + } + catch (NullReferenceException) { + // There is a bug in the .NET code where setting the TopItem + // will sometimes throw null reference exceptions + // There is nothing we can do to get around it. + } + } + } + + /// + /// Gets or sets whether moving the mouse over the header will trigger CellOver events. + /// Defaults to true. + /// + /// + /// Moving the mouse over the header did not previously trigger CellOver events, since the + /// header is considered a separate control. + /// If this change in behaviour causes your application problems, set this to false. + /// If you are interested in knowing when the mouse moves over the header, set this property to true (the default). + /// + [Category("ObjectListView"), + Description("Should moving the mouse over the header trigger CellOver events?"), + DefaultValue(true)] + public bool TriggerCellOverEventsWhenOverHeader + { + get { return triggerCellOverEventsWhenOverHeader; } + set { triggerCellOverEventsWhenOverHeader = value; } + } + private bool triggerCellOverEventsWhenOverHeader = true; + + /// + /// When resizing a column by dragging its divider, should any space filling columns be + /// resized at each mouse move? If this is false, the filling columns will be + /// updated when the mouse is released. + /// + /// + /// + /// If you have a space filling column + /// is in the left of the column that is being resized, this will look odd: + /// the right edge of the column will be dragged, but + /// its left edge will move since the space filling column is shrinking. + /// + /// This is logical behaviour -- it just looks wrong. + /// + /// + /// Given the above behavior is probably best to turn this property off if your space filling + /// columns aren't the right-most columns. + /// + [Category("ObjectListView"), + Description("When resizing a column by dragging its divider, should any space filling columns be resized at each mouse move?"), + DefaultValue(true)] + public virtual bool UpdateSpaceFillingColumnsWhenDraggingColumnDivider { + get { return updateSpaceFillingColumnsWhenDraggingColumnDivider; } + set { updateSpaceFillingColumnsWhenDraggingColumnDivider = value; } + } + private bool updateSpaceFillingColumnsWhenDraggingColumnDivider = true; + + /// + /// What color should be used for the background of selected rows when the control doesn't have the focus? + /// + [Category("ObjectListView"), + Description("The background color of selected rows when the control doesn't have the focus"), + DefaultValue(typeof(Color), "")] + public virtual Color UnfocusedSelectedBackColor { + get { return this.unfocusedSelectedBackColor; } + set { this.unfocusedSelectedBackColor = value; } + } + private Color unfocusedSelectedBackColor = Color.Empty; + + /// + /// Return the color should be used for the background of selected rows when the control doesn't have the focus or a reasonable default + /// + [Browsable(false)] + public virtual Color UnfocusedSelectedBackColorOrDefault { + get { + return this.UnfocusedSelectedBackColor.IsEmpty ? SystemColors.Control : this.UnfocusedSelectedBackColor; + } + } + + /// + /// What color should be used for the foreground of selected rows when the control doesn't have the focus? + /// + [Category("ObjectListView"), + Description("The foreground color of selected rows when the control is owner drawn and doesn't have the focus"), + DefaultValue(typeof(Color), "")] + public virtual Color UnfocusedSelectedForeColor { + get { return this.unfocusedSelectedForeColor; } + set { this.unfocusedSelectedForeColor = value; } + } + private Color unfocusedSelectedForeColor = Color.Empty; + + /// + /// Return the color should be used for the foreground of selected rows when the control doesn't have the focus or a reasonable default + /// + [Browsable(false)] + public virtual Color UnfocusedSelectedForeColorOrDefault { + get { + return this.UnfocusedSelectedForeColor.IsEmpty ? SystemColors.ControlText : this.UnfocusedSelectedForeColor; + } + } + + /// + /// Gets or sets whether the list give a different background color to every second row? Defaults to false. + /// + /// The color of the alternate rows is given by AlternateRowBackColor. + /// There is a "feature" in .NET for listviews in non-full-row-select mode, where + /// selected rows are not drawn with their correct background color. + [Category("ObjectListView"), + Description("Should the list view use a different backcolor to alternate rows?"), + DefaultValue(false)] + public virtual bool UseAlternatingBackColors { + get { return useAlternatingBackColors; } + set { useAlternatingBackColors = value; } + } + private bool useAlternatingBackColors; + + /// + /// Should FormatCell events be called for each cell in the control? + /// + /// + /// In many situations, no cell level formatting is performed. ObjectListView + /// can run somewhat faster if it does not trigger a format cell event for every cell + /// unless it is required. So, by default, it does not raise an event for each cell. + /// + /// ObjectListView *does* raise a FormatRow event every time a row is rebuilt. + /// Individual rows can decide whether to raise FormatCell + /// events for every cell in row. + /// + /// + /// Regardless of this setting, FormatCell events are only raised when the ObjectListView + /// is in Details view. + /// + [Category("ObjectListView"), + Description("Should FormatCell events be triggered to every cell that is built?"), + DefaultValue(false)] + public bool UseCellFormatEvents { + get { return useCellFormatEvents; } + set { useCellFormatEvents = value; } + } + private bool useCellFormatEvents; + + /// + /// Should the selected row be drawn with non-standard foreground and background colors? + /// + /// v2.9 This property is no longer required + [Category("ObjectListView"), + Description("Should the selected row be drawn with non-standard foreground and background colors?"), + DefaultValue(false)] + public bool UseCustomSelectionColors { + get { return false; } + // ReSharper disable once ValueParameterNotUsed + set { } + } + + /// + /// Gets or sets whether this ObjectListView will use the same hot item and selection + /// mechanism that Vista Explorer does. + /// + /// + /// + /// This property has many imperfections: + /// + /// This only works on Vista and later + /// It does not work well with AlternateRowBackColors. + /// It does not play well with HotItemStyles. + /// It looks a little bit silly is FullRowSelect is false. + /// It doesn't work at all when the list is owner drawn (since the renderers + /// do all the drawing). As such, it won't work with TreeListView's since they *have to be* + /// owner drawn. You can still set it, but it's just not going to be happy. + /// + /// But if you absolutely have to look like Vista/Win7, this is your property. + /// Do not complain if settings this messes up other things. + /// + /// + /// When this property is set to true, the ObjectListView will be not owner drawn. This will + /// disable many of the pretty drawing-based features of ObjectListView. + /// + /// Because of the above, this property should never be set to true for TreeListViews, + /// since they *require* owner drawing to be rendered correctly. + /// + [Category("ObjectListView"), + Description("Should the list use the same hot item and selection mechanism as Vista?"), + DefaultValue(false)] + public bool UseExplorerTheme { + get { return useExplorerTheme; } + set { + useExplorerTheme = value; + if (this.Created) + NativeMethods.SetWindowTheme(this.Handle, value ? "explorer" : "", null); + + this.OwnerDraw = !value; + } + } + private bool useExplorerTheme; + + /// + /// Gets or sets whether the list should enable filtering + /// + [Category("ObjectListView"), + Description("Should the list enable filtering?"), + DefaultValue(false)] + public virtual bool UseFiltering { + get { return useFiltering; } + set { + if (useFiltering == value) + return; + useFiltering = value; + this.UpdateFiltering(); + } + } + private bool useFiltering; + + /// + /// Gets or sets whether the list should put an indicator into a column's header to show that + /// it is filtering on that column + /// + /// If you set this to true, HeaderUsesThemes is automatically set to false, since + /// we can only draw a filter indicator when not using a themed header. + [Category("ObjectListView"), + Description("Should an image be drawn in a column's header when that column is being used for filtering?"), + DefaultValue(false)] + public virtual bool UseFilterIndicator { + get { return useFilterIndicator; } + set { + if (this.useFilterIndicator == value) + return; + useFilterIndicator = value; + if (this.useFilterIndicator) + this.HeaderUsesThemes = false; + this.Invalidate(); + } + } + private bool useFilterIndicator; + + /// + /// Should controls (checkboxes or buttons) that are under the mouse be drawn "hot"? + /// + /// + /// If this is false, control will not be drawn differently when the mouse is over them. + /// + /// If this is false AND UseHotItem is false AND UseHyperlinks is false, then the ObjectListView + /// can skip some processing on mouse move. This make mouse move processing use almost no CPU. + /// + /// + [Category("ObjectListView"), + Description("Should controls (checkboxes or buttons) that are under the mouse be drawn hot?"), + DefaultValue(true)] + public bool UseHotControls { + get { return this.useHotControls; } + set { this.useHotControls = value; } + } + private bool useHotControls = true; + + /// + /// Should the item under the cursor be formatted in a special way? + /// + [Category("ObjectListView"), + Description("Should HotTracking be used? Hot tracking applies special formatting to the row under the cursor"), + DefaultValue(false)] + public bool UseHotItem { + get { return this.useHotItem; } + set { + this.useHotItem = value; + if (value) + this.AddOverlay(this.HotItemStyleOrDefault.Overlay); + else + this.RemoveOverlay(this.HotItemStyleOrDefault.Overlay); + } + } + private bool useHotItem; + + /// + /// Gets or sets whether this listview should show hyperlinks in the cells. + /// + [Category("ObjectListView"), + Description("Should hyperlinks be shown on this control?"), + DefaultValue(false)] + public bool UseHyperlinks { + get { return this.useHyperlinks; } + set { + this.useHyperlinks = value; + if (value && this.HyperlinkStyle == null) + this.HyperlinkStyle = new HyperlinkStyle(); + } + } + private bool useHyperlinks; + + /// + /// Should this control show overlays + /// + /// Overlays are enabled by default and would only need to be disabled + /// if they were causing problems in your development environment. + [Category("ObjectListView"), + Description("Should this control show overlays"), + DefaultValue(true)] + public bool UseOverlays { + get { return this.useOverlays; } + set { this.useOverlays = value; } + } + private bool useOverlays = true; + + /// + /// Should this control be configured to show check boxes on subitems? + /// + /// If this is set to True, the control will be given a SmallImageList if it + /// doesn't already have one. Also, if it is a virtual list, it will be set to owner + /// drawn, since virtual lists can't draw check boxes without being owner drawn. + [Category("ObjectListView"), + Description("Should this control be configured to show check boxes on subitems."), + DefaultValue(false)] + public bool UseSubItemCheckBoxes { + get { return this.useSubItemCheckBoxes; } + set { + this.useSubItemCheckBoxes = value; + if (value) + this.SetupSubItemCheckBoxes(); + } + } + private bool useSubItemCheckBoxes; + + /// + /// Gets or sets if the ObjectListView will use a translucent selection mechanism like Vista. + /// + /// + /// + /// Unlike UseExplorerTheme, this Vista-like scheme works on XP and for both + /// owner and non-owner drawn lists. + /// + /// + /// This will replace any SelectedRowDecoration that has been installed. + /// + /// + /// If you don't like the colours used for the selection, ignore this property and + /// just create your own RowBorderDecoration and assigned it to SelectedRowDecoration, + /// just like this property setter does. + /// + /// + [Category("ObjectListView"), + Description("Should the list use a translucent selection mechanism (like Vista)"), + DefaultValue(false)] + public bool UseTranslucentSelection { + get { return useTranslucentSelection; } + set { + useTranslucentSelection = value; + if (value) { + RowBorderDecoration rbd = new RowBorderDecoration(); + rbd.BorderPen = new Pen(Color.FromArgb(154, 223, 251)); + rbd.FillBrush = new SolidBrush(Color.FromArgb(48, 163, 217, 225)); + rbd.BoundsPadding = new Size(0, 0); + rbd.CornerRounding = 6.0f; + this.SelectedRowDecoration = rbd; + } else + this.SelectedRowDecoration = null; + } + } + private bool useTranslucentSelection; + + /// + /// Gets or sets if the ObjectListView will use a translucent hot row highlighting mechanism like Vista. + /// + /// + /// + /// Setting this will replace any HotItemStyle that has been installed. + /// + /// + /// If you don't like the colours used for the hot item, ignore this property and + /// just create your own HotItemStyle, fill in the values you want, and assigned it to HotItemStyle property, + /// just like this property setter does. + /// + /// + [Category("ObjectListView"), + Description("Should the list use a translucent hot row highlighting mechanism (like Vista)"), + DefaultValue(false)] + public bool UseTranslucentHotItem { + get { return useTranslucentHotItem; } + set { + useTranslucentHotItem = value; + if (value) { + RowBorderDecoration rbd = new RowBorderDecoration(); + rbd.BorderPen = new Pen(Color.FromArgb(154, 223, 251)); + rbd.BoundsPadding = new Size(0, 0); + rbd.CornerRounding = 6.0f; + rbd.FillGradientFrom = Color.FromArgb(0, 255, 255, 255); + rbd.FillGradientTo = Color.FromArgb(64, 183, 237, 240); + HotItemStyle his = new HotItemStyle(); + his.Decoration = rbd; + this.HotItemStyle = his; + } else + this.HotItemStyle = null; + this.UseHotItem = value; + } + } + private bool useTranslucentHotItem; + + /// + /// Get/set the style of view that this listview is using + /// + /// Switching to tile or details view installs the columns appropriate to that view. + /// Confusingly, in tile view, every column is shown as a row of information. + [Category("Appearance"), + Description("Select the layout of the items within this control)"), + DefaultValue(null)] + public new View View + { + get { return base.View; } + set { + if (base.View == value) + return; + + if (this.Frozen) { + base.View = value; + this.SetupBaseImageList(); + } else { + this.Freeze(); + + if (value == View.Tile) + this.CalculateReasonableTileSize(); + + base.View = value; + this.SetupBaseImageList(); + this.Unfreeze(); + } + } + } + + #endregion + + #region Callbacks + + /// + /// This delegate fetches the checkedness of an object as a boolean only. + /// + /// Use this if you never want to worry about the + /// Indeterminate state (which is fairly common). + /// + /// This is a convenience wrapper around the CheckStateGetter property. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual BooleanCheckStateGetterDelegate BooleanCheckStateGetter { + set { + if (value == null) + this.CheckStateGetter = null; + else + this.CheckStateGetter = delegate(Object x) { + return value(x) ? CheckState.Checked : CheckState.Unchecked; + }; + } + } + + /// + /// This delegate sets the checkedness of an object as a boolean only. It must return + /// true or false indicating if the object was checked or not. + /// + /// Use this if you never want to worry about the + /// Indeterminate state (which is fairly common). + /// + /// This is a convenience wrapper around the CheckStatePutter property. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual BooleanCheckStatePutterDelegate BooleanCheckStatePutter { + set { + if (value == null) + this.CheckStatePutter = null; + else + this.CheckStatePutter = delegate(Object x, CheckState state) { + bool isChecked = (state == CheckState.Checked); + return value(x, isChecked) ? CheckState.Checked : CheckState.Unchecked; + }; + } + } + + /// + /// Gets whether or not this listview is capable of showing groups + /// + [Browsable(false)] + public virtual bool CanShowGroups { + get { + return true; + } + } + + /// + /// Gets or sets whether ObjectListView can rely on Application.Idle events + /// being raised. + /// + /// In some host environments (e.g. when running as an extension within + /// VisualStudio and possibly Office), Application.Idle events are never raised. + /// Set this to false when Idle events will not be raised, and ObjectListView will + /// raise those events itself. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual bool CanUseApplicationIdle { + get { return this.canUseApplicationIdle; } + set { this.canUseApplicationIdle = value; } + } + private bool canUseApplicationIdle = true; + + /// + /// This delegate fetches the renderer for a particular cell. + /// + /// + /// + /// If this returns null (or is not installed), the renderer for the column will be used. + /// If the column renderer is null, then will be used. + /// + /// + /// This is called every time any cell is drawn. It must be efficient! + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual CellRendererGetterDelegate CellRendererGetter + { + get { return this.cellRendererGetter; } + set { this.cellRendererGetter = value; } + } + private CellRendererGetterDelegate cellRendererGetter; + + /// + /// This delegate is called when the list wants to show a tooltip for a particular cell. + /// The delegate should return the text to display, or null to use the default behavior + /// (which is to show the full text of truncated cell values). + /// + /// + /// Displaying the full text of truncated cell values only work for FullRowSelect listviews. + /// This is MS's behavior, not mine. Don't complain to me :) + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual CellToolTipGetterDelegate CellToolTipGetter { + get { return cellToolTipGetter; } + set { cellToolTipGetter = value; } + } + private CellToolTipGetterDelegate cellToolTipGetter; + + /// + /// The name of the property (or field) that holds whether or not a model is checked. + /// + /// + /// The property be modifiable. It must have a return type of bool (or of bool? if + /// TriStateCheckBoxes is true). + /// Setting this property replaces any CheckStateGetter or CheckStatePutter that have been installed. + /// Conversely, later setting the CheckStateGetter or CheckStatePutter properties will take precedence + /// over the behavior of this property. + /// + [Category("ObjectListView"), + Description("The name of the property or field that holds the 'checkedness' of the model"), + DefaultValue(null)] + public virtual string CheckedAspectName { + get { return checkedAspectName; } + set { + checkedAspectName = value; + if (String.IsNullOrEmpty(checkedAspectName)) { + this.checkedAspectMunger = null; + this.CheckStateGetter = null; + this.CheckStatePutter = null; + } else { + this.checkedAspectMunger = new Munger(checkedAspectName); + this.CheckStateGetter = delegate(Object modelObject) { + bool? result = this.checkedAspectMunger.GetValue(modelObject) as bool?; + if (result.HasValue) + return result.Value ? CheckState.Checked : CheckState.Unchecked; + return this.TriStateCheckBoxes ? CheckState.Indeterminate : CheckState.Unchecked; + }; + this.CheckStatePutter = delegate(Object modelObject, CheckState newValue) { + if (this.TriStateCheckBoxes && newValue == CheckState.Indeterminate) + this.checkedAspectMunger.PutValue(modelObject, null); + else + this.checkedAspectMunger.PutValue(modelObject, newValue == CheckState.Checked); + return this.CheckStateGetter(modelObject); + }; + } + } + } + private string checkedAspectName; + private Munger checkedAspectMunger; + + /// + /// This delegate will be called whenever the ObjectListView needs to know the check state + /// of the row associated with a given model object. + /// + /// + /// .NET has no support for indeterminate values, but as of v2.0, this class allows + /// indeterminate values. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual CheckStateGetterDelegate CheckStateGetter { + get { return checkStateGetter; } + set { checkStateGetter = value; } + } + private CheckStateGetterDelegate checkStateGetter; + + /// + /// This delegate will be called whenever the user tries to change the check state of a row. + /// The delegate should return the state that was actually set, which may be different + /// to the state given. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual CheckStatePutterDelegate CheckStatePutter { + get { return checkStatePutter; } + set { checkStatePutter = value; } + } + private CheckStatePutterDelegate checkStatePutter; + + /// + /// This delegate can be used to sort the table in a custom fashion. + /// + /// + /// + /// The delegate must install a ListViewItemSorter on the ObjectListView. + /// Installing the ItemSorter does the actual work of sorting the ListViewItems. + /// See ColumnComparer in the code for an example of what an ItemSorter has to do. + /// + /// + /// Do not install a CustomSorter on a VirtualObjectListView. Override the SortObjects() + /// method of the IVirtualListDataSource instead. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortDelegate CustomSorter { + get { return customSorter; } + set { customSorter = value; } + } + private SortDelegate customSorter; + + /// + /// This delegate is called when the list wants to show a tooltip for a particular header. + /// The delegate should return the text to display, or null to use the default behavior + /// (which is to not show any tooltip). + /// + /// + /// Installing a HeaderToolTipGetter takes precedence over any text in OLVColumn.ToolTipText. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual HeaderToolTipGetterDelegate HeaderToolTipGetter { + get { return headerToolTipGetter; } + set { headerToolTipGetter = value; } + } + private HeaderToolTipGetterDelegate headerToolTipGetter; + + /// + /// This delegate can be used to format a OLVListItem before it is added to the control. + /// + /// + /// The model object for the row can be found through the RowObject property of the OLVListItem object. + /// All subitems normally have the same style as list item, so setting the forecolor on one + /// subitem changes the forecolor of all subitems. + /// To allow subitems to have different attributes, do this: + /// myListViewItem.UseItemStyleForSubItems = false;. + /// + /// If UseAlternatingBackColors is true, the backcolor of the listitem will be calculated + /// by the control and cannot be controlled by the RowFormatter delegate. + /// In general, trying to use a RowFormatter + /// when UseAlternatingBackColors is true does not work well. + /// As it says in the summary, this is called before the item is added to the control. + /// Many properties of the OLVListItem itself are not available at that point, including: + /// Index, Selected, Focused, Bounds, Checked, DisplayIndex. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual RowFormatterDelegate RowFormatter { + get { return rowFormatter; } + set { rowFormatter = value; } + } + private RowFormatterDelegate rowFormatter; + + #endregion + + #region List commands + + /// + /// Add the given model object to this control. + /// + /// The model object to be displayed + /// See AddObjects() for more details + public virtual void AddObject(object modelObject) { + if (this.InvokeRequired) + this.Invoke((MethodInvoker)delegate() { this.AddObject(modelObject); }); + else + this.AddObjects(new object[] { modelObject }); + } + + /// + /// Add the given collection of model objects to this control. + /// + /// A collection of model objects + /// + /// The added objects will appear in their correct sort position, if sorting + /// is active (i.e. if PrimarySortColumn is not null). Otherwise, they will appear at the end of the list. + /// No check is performed to see if any of the objects are already in the ListView. + /// Null objects are silently ignored. + /// + public virtual void AddObjects(ICollection modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate() { this.AddObjects(modelObjects); }); + return; + } + this.InsertObjects(ObjectListView.EnumerableCount(this.Objects), modelObjects); + this.Sort(this.PrimarySortColumn, this.PrimarySortOrder); + } + + /// + /// Resize the columns to the maximum of the header width and the data. + /// + public virtual void AutoResizeColumns() { + foreach (OLVColumn c in this.Columns) { + this.AutoResizeColumn(c.Index, ColumnHeaderAutoResizeStyle.HeaderSize); + } + } + + /// + /// Set up any automatically initialized column widths (columns that + /// have a width of 0 or -1 will be resized to the width of their + /// contents or header respectively). + /// + /// + /// Obviously, this will only work once. Once it runs, the columns widths will + /// be changed to something else (other than 0 or -1), so it wont do anything the + /// second time through. Use to force all columns + /// to change their size. + /// + public virtual void AutoSizeColumns() { + // If we are supposed to resize to content, but if there is no content, + // resize to the header size instead. + ColumnHeaderAutoResizeStyle resizeToContentStyle = this.GetItemCount() == 0 ? + ColumnHeaderAutoResizeStyle.HeaderSize : + ColumnHeaderAutoResizeStyle.ColumnContent; + foreach (ColumnHeader column in this.Columns) { + switch (column.Width) { + case 0: + this.AutoResizeColumn(column.Index, resizeToContentStyle); + break; + case -1: + this.AutoResizeColumn(column.Index, ColumnHeaderAutoResizeStyle.HeaderSize); + break; + } + } + } + + /// + /// Organise the view items into groups, based on the last sort column or the first column + /// if there is no last sort column + /// + public virtual void BuildGroups() { + this.BuildGroups(this.PrimarySortColumn, this.PrimarySortOrder == SortOrder.None ? SortOrder.Ascending : this.PrimarySortOrder); + } + + /// + /// Organise the view items into groups, based on the given column + /// + /// + /// + /// If the AlwaysGroupByColumn property is not null, + /// the list view items will be organised by that column, + /// and the 'column' parameter will be ignored. + /// + /// This method triggers sorting events: BeforeSorting and AfterSorting. + /// + /// The column whose values should be used for sorting. + /// + public virtual void BuildGroups(OLVColumn column, SortOrder order) { + // Sanity + if (this.GetItemCount() == 0 || this.Columns.Count == 0) + return; + + BeforeSortingEventArgs args = this.BuildBeforeSortingEventArgs(column, order); + this.OnBeforeSorting(args); + if (args.Canceled) + return; + + this.BuildGroups(args.ColumnToGroupBy, args.GroupByOrder, + args.ColumnToSort, args.SortOrder, args.SecondaryColumnToSort, args.SecondarySortOrder); + + this.OnAfterSorting(new AfterSortingEventArgs(args)); + } + + private BeforeSortingEventArgs BuildBeforeSortingEventArgs(OLVColumn column, SortOrder order) { + OLVColumn groupBy = this.AlwaysGroupByColumn ?? column ?? this.GetColumn(0); + SortOrder groupByOrder = this.AlwaysGroupBySortOrder; + if (order == SortOrder.None) { + order = this.Sorting; + if (order == SortOrder.None) + order = SortOrder.Ascending; + } + if (groupByOrder == SortOrder.None) + groupByOrder = order; + + BeforeSortingEventArgs args = new BeforeSortingEventArgs( + groupBy, groupByOrder, + column, order, + this.SecondarySortColumn ?? this.GetColumn(0), + this.SecondarySortOrder == SortOrder.None ? order : this.SecondarySortOrder); + if (column != null) + args.Canceled = !column.Sortable; + return args; + } + + /// + /// Organise the view items into groups, based on the given columns + /// + /// What column will be used for grouping + /// What ordering will be used for groups + /// The column whose values should be used for sorting. Cannot be null + /// The order in which the values from column will be sorted + /// When the values from 'column' are equal, use the values provided by this column + /// How will the secondary values be sorted + /// This method does not trigger sorting events. Use BuildGroups() to do that + public virtual void BuildGroups(OLVColumn groupByColumn, SortOrder groupByOrder, + OLVColumn column, SortOrder order, OLVColumn secondaryColumn, SortOrder secondaryOrder) { + // Sanity checks + if (groupByColumn == null) + return; + + // Getting the Count forces any internal cache of the ListView to be flushed. Without + // this, iterating over the Items will not work correctly if the ListView handle + // has not yet been created. +#pragma warning disable 168 +// ReSharper disable once UnusedVariable + int dummy = this.Items.Count; +#pragma warning restore 168 + + // Collect all the information that governs the creation of groups + GroupingParameters parms = this.CollectGroupingParameters(groupByColumn, groupByOrder, + column, order, secondaryColumn, secondaryOrder); + + // Trigger an event to let the world create groups if they want + CreateGroupsEventArgs args = new CreateGroupsEventArgs(parms); + if (parms.GroupByColumn != null) + args.Canceled = !parms.GroupByColumn.Groupable; + this.OnBeforeCreatingGroups(args); + if (args.Canceled) + return; + + // If the event didn't create them for us, use our default strategy + if (args.Groups == null) + args.Groups = this.MakeGroups(parms); + + // Give the world a chance to munge the groups before they are created + this.OnAboutToCreateGroups(args); + if (args.Canceled) + return; + + // Create the groups now + this.OLVGroups = args.Groups; + this.CreateGroups(args.Groups); + + // Tell the world that new groups have been created + this.OnAfterCreatingGroups(args); + lastGroupingParameters = args.Parameters; + } + private GroupingParameters lastGroupingParameters; + + /// + /// Collect and return all the variables that influence the creation of groups + /// + /// + protected virtual GroupingParameters CollectGroupingParameters(OLVColumn groupByColumn, SortOrder groupByOrder, + OLVColumn sortByColumn, SortOrder sortByOrder, OLVColumn secondaryColumn, SortOrder secondaryOrder) { + + // If the user tries to group by a non-groupable column, keep the current group by + // settings, but use the non-groupable column for sorting + if (!groupByColumn.Groupable && lastGroupingParameters != null) { + sortByColumn = groupByColumn; + sortByOrder = groupByOrder; + groupByColumn = lastGroupingParameters.GroupByColumn; + groupByOrder = lastGroupingParameters.GroupByOrder; + } + + string titleFormat = this.ShowItemCountOnGroups ? groupByColumn.GroupWithItemCountFormatOrDefault : null; + string titleSingularFormat = this.ShowItemCountOnGroups ? groupByColumn.GroupWithItemCountSingularFormatOrDefault : null; + GroupingParameters parms = new GroupingParameters(this, groupByColumn, groupByOrder, + sortByColumn, sortByOrder, secondaryColumn, secondaryOrder, + titleFormat, titleSingularFormat, + this.SortGroupItemsByPrimaryColumn && this.AlwaysGroupByColumn == null); + return parms; + } + + /// + /// Make a list of groups that should be shown according to the given parameters + /// + /// + /// The list of groups to be created + /// This should not change the state of the control. It is possible that the + /// groups created will not be used. They may simply be discarded. + protected virtual IList MakeGroups(GroupingParameters parms) { + + // There is a lot of overlap between this method and FastListGroupingStrategy.MakeGroups() + // Any changes made here may need to be reflected there + + // Separate the list view items into groups, using the group key as the descrimanent + NullableDictionary> map = new NullableDictionary>(); + foreach (OLVListItem olvi in parms.ListView.Items) { + object key = parms.GroupByColumn.GetGroupKey(olvi.RowObject); + if (!map.ContainsKey(key)) + map[key] = new List(); + map[key].Add(olvi); + } + + // Sort the items within each group (unless specifically turned off) + OLVColumn sortColumn = parms.SortItemsByPrimaryColumn ? parms.ListView.GetColumn(0) : parms.PrimarySort; + if (sortColumn != null && parms.PrimarySortOrder != SortOrder.None) { + IComparer itemSorter = parms.ItemComparer ?? + new ColumnComparer(sortColumn, parms.PrimarySortOrder, parms.SecondarySort, parms.SecondarySortOrder); + foreach (object key in map.Keys) { + map[key].Sort(itemSorter); + } + } + + // Make a list of the required groups + List groups = new List(); + foreach (object key in map.Keys) { + OLVGroup lvg = parms.CreateGroup(key, map[key].Count, HasCollapsibleGroups); + lvg.Items = map[key]; + if (parms.GroupByColumn.GroupFormatter != null) + parms.GroupByColumn.GroupFormatter(lvg, parms); + groups.Add(lvg); + } + + // Sort the groups + if (parms.GroupByOrder != SortOrder.None) + groups.Sort(parms.GroupComparer ?? new OLVGroupComparer(parms.GroupByOrder)); + + return groups; + } + + /// + /// Build/rebuild all the list view items in the list, preserving as much state as is possible + /// + public virtual void BuildList() { + if (this.InvokeRequired) + this.Invoke(new MethodInvoker(this.BuildList)); + else + this.BuildList(true); + } + + /// + /// Build/rebuild all the list view items in the list + /// + /// If this is true, the control will try to preserve the selection, + /// focused item, and the scroll position (see Remarks) + /// + /// + /// + /// Use this method in situations were the contents of the list is basically the same + /// as previously. + /// + /// + public virtual void BuildList(bool shouldPreserveState) { + if (this.Frozen) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + this.ApplyExtendedStyles(); + this.ClearHotItem(); + int previousTopIndex = this.TopItemIndex; + Point currentScrollPosition = this.LowLevelScrollPosition; + + IList previousSelection = new ArrayList(); + Object previousFocus = null; + if (shouldPreserveState && this.objects != null) { + previousSelection = this.SelectedObjects; + OLVListItem focusedItem = this.FocusedItem as OLVListItem; + if (focusedItem != null) + previousFocus = focusedItem.RowObject; + } + + IEnumerable objectsToDisplay = this.FilteredObjects; + + this.BeginUpdate(); + try { + this.Items.Clear(); + this.ListViewItemSorter = null; + + if (objectsToDisplay != null) { + // Build a list of all our items and then display them. (Building + // a list and then doing one AddRange is about 10-15% faster than individual adds) + List itemList = new List(); // use ListViewItem to avoid co-variant conversion + foreach (object rowObject in objectsToDisplay) { + OLVListItem lvi = new OLVListItem(rowObject); + this.FillInValues(lvi, rowObject); + itemList.Add(lvi); + } + this.Items.AddRange(itemList.ToArray()); + this.Sort(); + + if (shouldPreserveState) { + this.SelectedObjects = previousSelection; + this.FocusedItem = this.ModelToItem(previousFocus); + } + } + } finally { + this.EndUpdate(); + } + + this.RefreshHotItem(); + + // We can only restore the scroll position after the EndUpdate() because + // of caching that the ListView does internally during a BeginUpdate/EndUpdate pair. + if (shouldPreserveState) { + // Restore the scroll position. TopItemIndex is best, but doesn't work + // when the control is grouped. + if (this.ShowGroups) + this.LowLevelScroll(currentScrollPosition.X, currentScrollPosition.Y); + else + this.TopItemIndex = previousTopIndex; + } + + // System.Diagnostics.Debug.WriteLine(String.Format("PERF - Building list for {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); + } + + /// + /// Clear any cached info this list may have been using + /// + public virtual void ClearCachedInfo() + { + // ObjectListView doesn't currently cache information but subclass do (or might) + } + + /// + /// Apply all required extended styles to our control. + /// + /// + /// + /// Whenever .NET code sets an extended style, it erases all other extended styles + /// that it doesn't use. So, we have to explicit reapply the styles that we have + /// added. + /// + /// + /// Normally, we would override CreateParms property and update + /// the ExStyle member, but ListView seems to ignore all ExStyles that + /// it doesn't already know about. Worse, when we set the LVS_EX_HEADERINALLVIEWS + /// value, bad things happen (the control crashes!). + /// + /// + protected virtual void ApplyExtendedStyles() { + const int LVS_EX_SUBITEMIMAGES = 0x00000002; + //const int LVS_EX_TRANSPARENTBKGND = 0x00400000; + const int LVS_EX_HEADERINALLVIEWS = 0x02000000; + + const int STYLE_MASK = LVS_EX_SUBITEMIMAGES | LVS_EX_HEADERINALLVIEWS; + int style = 0; + + if (this.ShowImagesOnSubItems && !this.VirtualMode) + style ^= LVS_EX_SUBITEMIMAGES; + + if (this.ShowHeaderInAllViews) + style ^= LVS_EX_HEADERINALLVIEWS; + + NativeMethods.SetExtendedStyle(this, style, STYLE_MASK); + } + + /// + /// Give the listview a reasonable size of its tiles, based on the number of lines of + /// information that each tile is going to display. + /// + public virtual void CalculateReasonableTileSize() { + if (this.Columns.Count <= 0) + return; + + List columns = this.AllColumns.FindAll(delegate(OLVColumn x) { + return (x.Index == 0) || x.IsTileViewColumn; + }); + + int imageHeight = (this.LargeImageList == null ? 16 : this.LargeImageList.ImageSize.Height); + int dataHeight = (this.Font.Height + 1) * columns.Count; + int tileWidth = (this.TileSize.Width == 0 ? 200 : this.TileSize.Width); + int tileHeight = Math.Max(this.TileSize.Height, Math.Max(imageHeight, dataHeight)); + this.TileSize = new Size(tileWidth, tileHeight); + } + + /// + /// Rebuild this list for the given view + /// + /// + public virtual void ChangeToFilteredColumns(View view) { + // Store the state + this.SuspendSelectionEvents(); + IList previousSelection = this.SelectedObjects; + int previousTopIndex = this.TopItemIndex; + + this.Freeze(); + this.Clear(); + List columns = this.GetFilteredColumns(view); + if (view == View.Details || this.ShowHeaderInAllViews) { + // Make sure all columns have a reasonable LastDisplayIndex + for (int index = 0; index < columns.Count; index++) + { + if (columns[index].LastDisplayIndex == -1) + columns[index].LastDisplayIndex = index; + } + // ListView will ignore DisplayIndex FOR ALL COLUMNS if there are any errors, + // e.g. duplicates (two columns with the same DisplayIndex) or gaps. + // LastDisplayIndex isn't guaranteed to be unique, so we just sort the columns by + // the last position they were displayed and use that to generate a sequence + // we can use for the DisplayIndex values. + List columnsInDisplayOrder = new List(columns); + columnsInDisplayOrder.Sort(delegate(OLVColumn x, OLVColumn y) { return (x.LastDisplayIndex - y.LastDisplayIndex); }); + int i = 0; + foreach (OLVColumn col in columnsInDisplayOrder) + col.DisplayIndex = i++; + } + +// ReSharper disable once CoVariantArrayConversion + this.Columns.AddRange(columns.ToArray()); + if (view == View.Details || this.ShowHeaderInAllViews) + this.ShowSortIndicator(); + this.UpdateFiltering(); + this.Unfreeze(); + + // Restore the state + this.SelectedObjects = previousSelection; + this.TopItemIndex = previousTopIndex; + this.ResumeSelectionEvents(); + } + + /// + /// Remove all items from this list + /// + /// This method can safely be called from background threads. + public virtual void ClearObjects() { + if (this.InvokeRequired) + this.Invoke(new MethodInvoker(this.ClearObjects)); + else + this.SetObjects(null); + } + + /// + /// Reset the memory of which URLs have been visited + /// + public virtual void ClearUrlVisited() { + this.visitedUrlMap = new Dictionary(); + } + + /// + /// Copy a text and html representation of the selected rows onto the clipboard. + /// + /// Be careful when using this with virtual lists. If the user has selected + /// 10,000,000 rows, this method will faithfully try to copy all of them to the clipboard. + /// From the user's point of view, your program will appear to have hung. + public virtual void CopySelectionToClipboard() { + IList selection = this.SelectedObjects; + if (selection.Count == 0) + return; + + // Use the DragSource object to create the data object, if so configured. + // This relies on the assumption that DragSource will handle the selected objects only. + // It is legal for StartDrag to return null. + object data = null; + if (this.CopySelectionOnControlCUsesDragSource && this.DragSource != null) + data = this.DragSource.StartDrag(this, MouseButtons.Left, this.ModelToItem(selection[0])); + + Clipboard.SetDataObject(data ?? new OLVDataObject(this, selection)); + } + + /// + /// Copy a text and html representation of the given objects onto the clipboard. + /// + public virtual void CopyObjectsToClipboard(IList objectsToCopy) { + if (objectsToCopy.Count == 0) + return; + + // We don't know where these objects came from, so we can't use the DragSource to create + // the data object, like we do with CopySelectionToClipboard() above. + OLVDataObject dataObject = new OLVDataObject(this, objectsToCopy); + dataObject.CreateTextFormats(); + Clipboard.SetDataObject(dataObject); + } + + /// + /// Return a html representation of the given objects + /// + public virtual string ObjectsToHtml(IList objectsToConvert) { + if (objectsToConvert.Count == 0) + return String.Empty; + + OLVExporter exporter = new OLVExporter(this, objectsToConvert); + return exporter.ExportTo(OLVExporter.ExportFormat.HTML); + } + + /// + /// Deselect all rows in the listview + /// + public virtual void DeselectAll() { + NativeMethods.DeselectAllItems(this); + } + + /// + /// Return the ListViewItem that appears immediately after the given item. + /// If the given item is null, the first item in the list will be returned. + /// Return null if the given item is the last item. + /// + /// The item that is before the item that is returned, or null + /// A ListViewItem + public virtual OLVListItem GetNextItem(OLVListItem itemToFind) { + if (this.ShowGroups) { + bool isFound = (itemToFind == null); + foreach (ListViewGroup group in this.Groups) { + foreach (OLVListItem olvi in group.Items) { + if (isFound) + return olvi; + isFound = (itemToFind == olvi); + } + } + return null; + } + if (this.GetItemCount() == 0) + return null; + if (itemToFind == null) + return this.GetItem(0); + if (itemToFind.Index == this.GetItemCount() - 1) + return null; + return this.GetItem(itemToFind.Index + 1); + } + + /// + /// Return the last item in the order they are shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + public virtual OLVListItem GetLastItemInDisplayOrder() { + if (!this.ShowGroups) + return this.GetItem(this.GetItemCount() - 1); + + if (this.Groups.Count > 0) { + ListViewGroup lastGroup = this.Groups[this.Groups.Count - 1]; + if (lastGroup.Items.Count > 0) + return (OLVListItem)lastGroup.Items[lastGroup.Items.Count - 1]; + } + + return null; + } + + /// + /// Return the n'th item (0-based) in the order they are shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + /// + public virtual OLVListItem GetNthItemInDisplayOrder(int n) { + if (!this.ShowGroups || this.Groups.Count == 0) + return this.GetItem(n); + + foreach (ListViewGroup group in this.Groups) { + if (n < group.Items.Count) + return (OLVListItem)group.Items[n]; + + n -= group.Items.Count; + } + + return null; + } + + /// + /// Return the display index of the given listviewitem index. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + /// + public virtual int GetDisplayOrderOfItemIndex(int itemIndex) { + if (!this.ShowGroups || this.Groups.Count == 0) + return itemIndex; + + // TODO: This could be optimized + int i = 0; + foreach (ListViewGroup lvg in this.Groups) { + foreach (ListViewItem lvi in lvg.Items) { + if (lvi.Index == itemIndex) + return i; + i++; + } + } + + return -1; + } + + /// + /// Return the ListViewItem that appears immediately before the given item. + /// If the given item is null, the last item in the list will be returned. + /// Return null if the given item is the first item. + /// + /// The item that is before the item that is returned + /// A ListViewItem + public virtual OLVListItem GetPreviousItem(OLVListItem itemToFind) { + if (this.ShowGroups) { + OLVListItem previousItem = null; + foreach (ListViewGroup group in this.Groups) { + foreach (OLVListItem lvi in group.Items) { + if (lvi == itemToFind) + return previousItem; + + previousItem = lvi; + } + } + return itemToFind == null ? previousItem : null; + } + if (this.GetItemCount() == 0) + return null; + if (itemToFind == null) + return this.GetItem(this.GetItemCount() - 1); + if (itemToFind.Index == 0) + return null; + return this.GetItem(itemToFind.Index - 1); + } + + /// + /// Insert the given collection of objects before the given position + /// + /// Where to insert the objects + /// The objects to be inserted + /// + /// + /// This operation only makes sense of non-sorted, non-grouped + /// lists, since any subsequent sort/group operation will rearrange + /// the list. + /// + /// This method only works on ObjectListViews and FastObjectListViews. + /// + public virtual void InsertObjects(int index, ICollection modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate() { + this.InsertObjects(index, modelObjects); + }); + return; + } + if (modelObjects == null) + return; + + this.BeginUpdate(); + try { + // Give the world a chance to cancel or change the added objects + ItemsAddingEventArgs args = new ItemsAddingEventArgs(modelObjects); + this.OnItemsAdding(args); + if (args.Canceled) + return; + modelObjects = args.ObjectsToAdd; + + this.TakeOwnershipOfObjects(); + ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); + + // If we are filtering the list, there is no way to efficiently + // insert the objects, so just put them into our collection and rebuild. + // Sigh -- yet another ListView anomoly. In every view except Details, an item + // inserted into the Items collection always appear at the end regardless of + // their actual insertion index. + if (this.IsFiltering || this.View != View.Details) { + index = Math.Max(0, Math.Min(index, ourObjects.Count)); + ourObjects.InsertRange(index, modelObjects); + this.BuildList(true); + } else { + this.ListViewItemSorter = null; + index = Math.Max(0, Math.Min(index, this.GetItemCount())); + int i = index; + foreach (object modelObject in modelObjects) { + if (modelObject != null) { + ourObjects.Insert(i, modelObject); + OLVListItem lvi = new OLVListItem(modelObject); + this.FillInValues(lvi, modelObject); + this.Items.Insert(i, lvi); + i++; + } + } + + for (i = index; i < this.GetItemCount(); i++) { + OLVListItem lvi = this.GetItem(i); + this.SetSubItemImages(lvi.Index, lvi); + } + + this.PostProcessRows(); + } + + // Tell the world that the list has changed + this.SubscribeNotifications(modelObjects); + this.OnItemsChanged(new ItemsChangedEventArgs()); + } finally { + this.EndUpdate(); + } + } + + /// + /// Return true if the row representing the given model is selected + /// + /// The model object to look for + /// Is the row selected + public bool IsSelected(object model) { + OLVListItem item = this.ModelToItem(model); + return item != null && item.Selected; + } + + /// + /// Has the given URL been visited? + /// + /// The string to be consider + /// Has it been visited + public virtual bool IsUrlVisited(string url) { + return this.visitedUrlMap.ContainsKey(url); + } + + /// + /// Scroll the ListView by the given deltas. + /// + /// Horizontal delta + /// Vertical delta + public void LowLevelScroll(int dx, int dy) { + NativeMethods.Scroll(this, dx, dy); + } + + /// + /// Return a point that represents the current horizontal and vertical scroll positions + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Point LowLevelScrollPosition + { + get { + return new Point(NativeMethods.GetScrollPosition(this, true), NativeMethods.GetScrollPosition(this, false)); + } + } + + /// + /// Remember that the given URL has been visited + /// + /// The url to be remembered + /// This does not cause the control be redrawn + public virtual void MarkUrlVisited(string url) { + this.visitedUrlMap[url] = true; + } + + /// + /// Move the given collection of objects to the given index. + /// + /// This operation only makes sense on non-grouped ObjectListViews. + /// + /// + public virtual void MoveObjects(int index, ICollection modelObjects) { + + // We are going to remove all the given objects from our list + // and then insert them at the given location + this.TakeOwnershipOfObjects(); + ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); + + List indicesToRemove = new List(); + foreach (object modelObject in modelObjects) { + if (modelObject != null) { + int i = this.IndexOf(modelObject); + if (i >= 0) { + indicesToRemove.Add(i); + ourObjects.Remove(modelObject); + if (i <= index) + index--; + } + } + } + + // Remove the objects in reverse order so earlier + // deletes don't change the index of later ones + indicesToRemove.Sort(); + indicesToRemove.Reverse(); + try { + this.BeginUpdate(); + foreach (int i in indicesToRemove) { + this.Items.RemoveAt(i); + } + this.InsertObjects(index, modelObjects); + } finally { + this.EndUpdate(); + } + } + + /// + /// Calculate what item is under the given point? + /// + /// + /// + /// + public new ListViewHitTestInfo HitTest(int x, int y) { + // Everything costs something. Playing with the layout of the header can cause problems + // with the hit testing. If the header shrinks, the underlying control can throw a tantrum. + try { + return base.HitTest(x, y); + } catch (ArgumentOutOfRangeException) { + return new ListViewHitTestInfo(null, null, ListViewHitTestLocations.None); + } + } + + /// + /// Perform a hit test using the Windows control's SUBITEMHITTEST message. + /// This provides information about group hits that the standard ListView.HitTest() does not. + /// + /// + /// + /// + protected OlvListViewHitTestInfo LowLevelHitTest(int x, int y) { + + // If it's not even in the control, don't bother with anything else + if (!this.ClientRectangle.Contains(x, y)) + return new OlvListViewHitTestInfo(null, null, 0, null, 0); + + // If there are no columns, also don't bother with anything else + if (this.Columns.Count == 0) + return new OlvListViewHitTestInfo(null, null, 0, null, 0); + + // Is the point over the header? + OlvListViewHitTestInfo.HeaderHitTestInfo headerHitTestInfo = this.HeaderControl.HitTest(x, y); + if (headerHitTestInfo != null) + return new OlvListViewHitTestInfo(this, headerHitTestInfo.ColumnIndex, headerHitTestInfo.IsOverCheckBox, headerHitTestInfo.OverDividerIndex); + + // Call the native hit test method, which is a little confusing. + NativeMethods.LVHITTESTINFO lParam = new NativeMethods.LVHITTESTINFO(); + lParam.pt_x = x; + lParam.pt_y = y; + int index = NativeMethods.HitTest(this, ref lParam); + + // Setup the various values we need to make our hit test structure + bool isGroupHit = (lParam.flags & (int)HitTestLocationEx.LVHT_EX_GROUP) != 0; + OLVListItem hitItem = isGroupHit || index == -1 ? null : this.GetItem(index); + OLVListSubItem subItem = (this.View == View.Details && hitItem != null) ? hitItem.GetSubItem(lParam.iSubItem) : null; + + // Figure out which group is involved in the hit test. This is a little complicated: + // If the list is virtual: + // - the returned value is list view item index + // - iGroup is the *index* of the hit group. + // If the list is not virtual: + // - iGroup is always -1. + // - if the point is over a group, the returned value is the *id* of the hit group. + // - if the point is not over a group, the returned value is list view item index. + OLVGroup group = null; + if (this.ShowGroups && this.OLVGroups != null) { + if (this.VirtualMode) { + group = lParam.iGroup >= 0 && lParam.iGroup < this.OLVGroups.Count ? this.OLVGroups[lParam.iGroup] : null; + } else { + if (isGroupHit) { + foreach (OLVGroup olvGroup in this.OLVGroups) { + if (olvGroup.GroupId == index) { + group = olvGroup; + break; + } + } + } + } + } + OlvListViewHitTestInfo olvListViewHitTest = new OlvListViewHitTestInfo(hitItem, subItem, lParam.flags, group, lParam.iSubItem); + // System.Diagnostics.Debug.WriteLine(String.Format("HitTest({0}, {1})=>{2}", x, y, olvListViewHitTest)); + return olvListViewHitTest; + } + + /// + /// What is under the given point? This takes the various parts of a cell into account, including + /// any custom parts that a custom renderer might use + /// + /// + /// + /// An information block about what is under the point + public virtual OlvListViewHitTestInfo OlvHitTest(int x, int y) { + OlvListViewHitTestInfo hti = this.LowLevelHitTest(x, y); + + // There is a bug/"feature" of the ListView concerning hit testing. + // If FullRowSelect is false and the point is over cell 0 but not on + // the text or icon, HitTest will not register a hit. We could turn + // FullRowSelect on, do the HitTest, and then turn it off again, but + // toggling FullRowSelect in that way messes up the tooltip in the + // underlying control. So we have to find another way. + // + // It's too hard to try to write the hit test from scratch. Grouping (for + // example) makes it just too complicated. So, we have to use HitTest + // but try to get around its limits. + // + // First step is to determine if the point was within column 0. + // If it was, then we only have to determine if there is an actual row + // under the point. If there is, then we know that the point is over cell 0. + // So we try a Battleship-style approach: is there a subcell to the right + // of cell 0? This will return a false negative if column 0 is the rightmost column, + // so we also check for a subcell to the left. But if only column 0 is visible, + // then that will fail too, so we check for something at the very left of the + // control. + // + // This will still fail under pathological conditions. If column 0 fills + // the whole listview and no part of the text column 0 is visible + // (because it is horizontally scrolled offscreen), then the hit test will fail. + + // Are we in the buggy context? Details view, not full row select, and + // failing to find anything + if (hti.Item == null && !this.FullRowSelect && this.View == View.Details) { + // Is the point within the column 0? If it is, maybe it should have been a hit. + // Let's test slightly to the right and then to left of column 0. Hopefully one + // of those will hit a subitem + Point sides = NativeMethods.GetScrolledColumnSides(this, 0); + if (x >= sides.X && x <= sides.Y) { + // We look for: + // - any subitem to the right of cell 0? + // - any subitem to the left of cell 0? + // - cell 0 at the left edge of the screen + hti = this.LowLevelHitTest(sides.Y + 4, y); + if (hti.Item == null) + hti = this.LowLevelHitTest(sides.X - 4, y); + if (hti.Item == null) + hti = this.LowLevelHitTest(4, y); + + if (hti.Item != null) { + // We hit something! So, the original point must have been in cell 0 + hti.ColumnIndex = 0; + hti.SubItem = hti.Item.GetSubItem(0); + hti.Location = ListViewHitTestLocations.None; + hti.HitTestLocation = HitTestLocation.InCell; + } + } + } + + if (this.OwnerDraw) + this.CalculateOwnerDrawnHitTest(hti, x, y); + else + this.CalculateStandardHitTest(hti, x, y); + + return hti; + } + + /// + /// Perform a hit test when the control is not owner drawn + /// + /// + /// + /// + protected virtual void CalculateStandardHitTest(OlvListViewHitTestInfo hti, int x, int y) { + + // Standard hit test works fine for the primary column + if (this.View != View.Details || hti.ColumnIndex == 0 || + hti.SubItem == null || hti.Column == null) + return; + + Rectangle cellBounds = hti.SubItem.Bounds; + bool hasImage = (this.GetActualImageIndex(hti.SubItem.ImageSelector) != -1); + + // Unless we say otherwise, it was an general incell hit + hti.HitTestLocation = HitTestLocation.InCell; + + // Check if the point is over where an image should be. + // If there is a checkbox or image there, tag it and exit. + Rectangle r = cellBounds; + r.Width = this.SmallImageSize.Width; + if (r.Contains(x, y)) { + if (hti.Column.CheckBoxes) { + hti.HitTestLocation = HitTestLocation.CheckBox; + return; + } + if (hasImage) { + hti.HitTestLocation = HitTestLocation.Image; + return; + } + } + + // Figure out where the text actually is and if the point is in it + // The standard HitTest assumes that any point inside a subitem is + // a hit on Text -- which is clearly not true. + Rectangle textBounds = cellBounds; + textBounds.X += 4; + if (hasImage) + textBounds.X += this.SmallImageSize.Width; + + Size proposedSize = new Size(textBounds.Width, textBounds.Height); + Size textSize = TextRenderer.MeasureText(hti.SubItem.Text, this.Font, proposedSize, TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix); + textBounds.Width = textSize.Width; + + switch (hti.Column.TextAlign) { + case HorizontalAlignment.Center: + textBounds.X += (cellBounds.Right - cellBounds.Left - textSize.Width) / 2; + break; + case HorizontalAlignment.Right: + textBounds.X = cellBounds.Right - textSize.Width; + break; + } + if (textBounds.Contains(x, y)) { + hti.HitTestLocation = HitTestLocation.Text; + } + } + + /// + /// Perform a hit test when the control is owner drawn. This hands off responsibility + /// to the renderer. + /// + /// + /// + /// + protected virtual void CalculateOwnerDrawnHitTest(OlvListViewHitTestInfo hti, int x, int y) { + // If the click wasn't on an item, give up + if (hti.Item == null) + return; + + // If the list is showing column, but they clicked outside the columns, also give up + if (this.View == View.Details && hti.Column == null) + return; + + // Which renderer was responsible for drawing that point + IRenderer renderer = this.View == View.Details + ? this.GetCellRenderer(hti.RowObject, hti.Column) + : this.ItemRenderer; + + // We can't decide who was responsible. Give up + if (renderer == null) + return; + + // Ask the responsible renderer what is at that point + renderer.HitTest(hti, x, y); + } + + /// + /// Pause (or unpause) all animations in the list + /// + /// true to pause, false to unpause + public virtual void PauseAnimations(bool isPause) { + for (int i = 0; i < this.Columns.Count; i++) { + OLVColumn col = this.GetColumn(i); + ImageRenderer renderer = col.Renderer as ImageRenderer; + if (renderer != null) { + renderer.ListView = this; + renderer.Paused = isPause; + } + } + } + + /// + /// Rebuild the columns based upon its current view and column visibility settings + /// + public virtual void RebuildColumns() { + this.ChangeToFilteredColumns(this.View); + } + + /// + /// Remove the given model object from the ListView + /// + /// The model to be removed + /// See RemoveObjects() for more details + /// This method is thread-safe. + /// + public virtual void RemoveObject(object modelObject) { + if (this.InvokeRequired) + this.Invoke((MethodInvoker)delegate() { this.RemoveObject(modelObject); }); + else + this.RemoveObjects(new object[] { modelObject }); + } + + /// + /// Remove all of the given objects from the control. + /// + /// Collection of objects to be removed + /// + /// Nulls and model objects that are not in the ListView are silently ignored. + /// This method is thread-safe. + /// + public virtual void RemoveObjects(ICollection modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate() { this.RemoveObjects(modelObjects); }); + return; + } + if (modelObjects == null) + return; + + this.BeginUpdate(); + try { + // Give the world a chance to cancel or change the added objects + ItemsRemovingEventArgs args = new ItemsRemovingEventArgs(modelObjects); + this.OnItemsRemoving(args); + if (args.Canceled) + return; + modelObjects = args.ObjectsToRemove; + + this.TakeOwnershipOfObjects(); + ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); + foreach (object modelObject in modelObjects) { + if (modelObject != null) { +// ReSharper disable PossibleMultipleEnumeration + int i = ourObjects.IndexOf(modelObject); + if (i >= 0) + ourObjects.RemoveAt(i); +// ReSharper restore PossibleMultipleEnumeration + i = this.IndexOf(modelObject); + if (i >= 0) + this.Items.RemoveAt(i); + } + } + this.PostProcessRows(); + + // Tell the world that the list has changed + this.UnsubscribeNotifications(modelObjects); + this.OnItemsChanged(new ItemsChangedEventArgs()); + } finally { + this.EndUpdate(); + } + } + + /// + /// Select all rows in the listview + /// + public virtual void SelectAll() { + NativeMethods.SelectAllItems(this); + } + + /// + /// Set the given image to be fixed in the bottom right of the list view. + /// This image will not scroll when the list view scrolls. + /// + /// + /// + /// This method uses ListView's native ability to display a background image. + /// It has a few limitations: + /// + /// + /// It doesn't work well with owner drawn mode. In owner drawn mode, each cell draws itself, + /// including its background, which covers the background image. + /// It doesn't look very good when grid lines are enabled, since the grid lines are drawn over the image. + /// It does not work at all on XP. + /// Obviously, it doesn't look good when alternate row background colors are enabled. + /// + /// + /// If you can live with these limitations, native watermarks are quite neat. They are true backgrounds, not + /// translucent overlays like the OverlayImage uses. They also have the decided advantage over overlays in that + /// they work correctly even in MDI applications. + /// + /// Setting this clears any background image. + /// + /// The image to be drawn. If null, any existing image will be removed. + public void SetNativeBackgroundWatermark(Image image) { + NativeMethods.SetBackgroundImage(this, image, true, false, 0, 0); + } + + /// + /// Set the given image to be background of the ListView so that it appears at the given + /// percentage offsets within the list. + /// + /// + /// This has the same limitations as described in . Make sure those limitations + /// are understood before using the method. + /// This is very similar to setting the property of the standard .NET ListView, except that the standard + /// BackgroundImage does not handle images with transparent areas properly -- it renders transparent areas as black. This + /// method does not have that problem. + /// Setting this clears any background watermark. + /// + /// The image to be drawn. If null, any existing image will be removed. + /// The horizontal percentage where the image will be placed. 0 is absolute left, 100 is absolute right. + /// The vertical percentage where the image will be placed. + public void SetNativeBackgroundImage(Image image, int xOffset, int yOffset) { + NativeMethods.SetBackgroundImage(this, image, false, false, xOffset, yOffset); + } + + /// + /// Set the given image to be the tiled background of the ListView. + /// + /// + /// This has the same limitations as described in and . + /// Make sure those limitations + /// are understood before using the method. + /// + /// The image to be drawn. If null, any existing image will be removed. + public void SetNativeBackgroundTiledImage(Image image) { + NativeMethods.SetBackgroundImage(this, image, false, true, 0, 0); + } + + /// + /// Set the collection of objects that will be shown in this list view. + /// + /// This method can safely be called from background threads. + /// The list is updated immediately + /// The objects to be displayed + public virtual void SetObjects(IEnumerable collection) { + this.SetObjects(collection, false); + } + + /// + /// Set the collection of objects that will be shown in this list view. + /// + /// This method can safely be called from background threads. + /// The list is updated immediately + /// The objects to be displayed + /// Should the state of the list be preserved as far as is possible. + public virtual void SetObjects(IEnumerable collection, bool preserveState) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.SetObjects(collection, preserveState); }); + return; + } + + // Give the world a chance to cancel or change the assigned collection + ItemsChangingEventArgs args = new ItemsChangingEventArgs(this.objects, collection); + this.OnItemsChanging(args); + if (args.Canceled) + return; + collection = args.NewObjects; + + // If we own the current list and they change to another list, we don't own it any more + if (this.isOwnerOfObjects && !ReferenceEquals(this.objects, collection)) + this.isOwnerOfObjects = false; + this.objects = collection; + this.BuildList(preserveState); + + // Tell the world that the list has changed + this.UpdateNotificationSubscriptions(this.objects); + this.OnItemsChanged(new ItemsChangedEventArgs()); + } + + /// + /// Update the given model object into the ListView. The model will be added if it doesn't already exist. + /// + /// The model to be updated + /// + /// + /// See for more details + /// + /// This method is thread-safe. + /// This method will cause the list to be resorted. + /// This method only works on ObjectListViews and FastObjectListViews. + /// + public virtual void UpdateObject(object modelObject) { + if (this.InvokeRequired) + this.Invoke((MethodInvoker)delegate() { this.UpdateObject(modelObject); }); + else + this.UpdateObjects(new object[] { modelObject }); + } + + /// + /// Update the pre-existing models that are equal to the given objects. If any of the model doesn't + /// already exist in the control, they will be added. + /// + /// Collection of objects to be updated/added + /// + /// This method will cause the list to be resorted. + /// Nulls are silently ignored. + /// This method is thread-safe. + /// This method only works on ObjectListViews and FastObjectListViews. + /// + public virtual void UpdateObjects(ICollection modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate() { this.UpdateObjects(modelObjects); }); + return; + } + if (modelObjects == null || modelObjects.Count == 0) + return; + + this.BeginUpdate(); + try { + this.UnsubscribeNotifications(modelObjects); + + ArrayList objectsToAdd = new ArrayList(); + + this.TakeOwnershipOfObjects(); + ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); + foreach (object modelObject in modelObjects) { + if (modelObject != null) { + int i = ourObjects.IndexOf(modelObject); + if (i < 0) + objectsToAdd.Add(modelObject); + else { + ourObjects[i] = modelObject; + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi != null) { + olvi.RowObject = modelObject; + this.RefreshItem(olvi); + } + } + } + } + this.PostProcessRows(); + + this.AddObjects(objectsToAdd); + + // Tell the world that the list has changed + this.SubscribeNotifications(modelObjects); + this.OnItemsChanged(new ItemsChangedEventArgs()); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Change any subscriptions to INotifyPropertyChanged events on our current + /// model objects so that we no longer listen for events on the old models + /// and do listen for events on the given collection. + /// + /// This does nothing if UseNotifyPropertyChanged is false. + /// + protected virtual void UpdateNotificationSubscriptions(IEnumerable collection) { + if (!this.UseNotifyPropertyChanged) + return; + + // We could calculate a symmetric difference between the old models and the new models + // except that we don't have the previous models at this point. + + this.UnsubscribeNotifications(null); + this.SubscribeNotifications(collection ?? this.Objects); + } + + /// + /// Gets or sets whether or not ObjectListView should subscribe to INotifyPropertyChanged + /// events on the model objects that it is given. + /// + /// + /// + /// This should be set before calling SetObjects(). If you set this to false, + /// ObjectListView will unsubscribe to all current model objects. + /// + /// If you set this to true on a virtual list, the ObjectListView will + /// walk all the objects in the list trying to subscribe to change notifications. + /// If you have 10,000,000 items in your virtual list, this may take some time. + /// + [Category("ObjectListView"), + Description("Should ObjectListView listen for property changed events on the model objects?"), + DefaultValue(false)] + public bool UseNotifyPropertyChanged { + get { return this.useNotifyPropertyChanged; } + set { + if (this.useNotifyPropertyChanged == value) + return; + this.useNotifyPropertyChanged = value; + if (value) + this.SubscribeNotifications(this.Objects); + else + this.UnsubscribeNotifications(null); + } + } + private bool useNotifyPropertyChanged; + + /// + /// Subscribe to INotifyPropertyChanges on the given collection of objects. + /// + /// + protected void SubscribeNotifications(IEnumerable models) { + if (!this.UseNotifyPropertyChanged || models == null) + return; + foreach (object x in models) { + INotifyPropertyChanged notifier = x as INotifyPropertyChanged; + if (notifier != null && !subscribedModels.ContainsKey(notifier)) { + notifier.PropertyChanged += HandleModelOnPropertyChanged; + subscribedModels[notifier] = notifier; + } + } + } + + /// + /// Unsubscribe from INotifyPropertyChanges on the given collection of objects. + /// If the given collection is null, unsubscribe from all current subscriptions + /// + /// + protected void UnsubscribeNotifications(IEnumerable models) { + if (models == null) { + foreach (INotifyPropertyChanged notifier in this.subscribedModels.Keys) { + notifier.PropertyChanged -= HandleModelOnPropertyChanged; + } + subscribedModels = new Hashtable(); + } else { + foreach (object x in models) { + INotifyPropertyChanged notifier = x as INotifyPropertyChanged; + if (notifier != null) { + notifier.PropertyChanged -= HandleModelOnPropertyChanged; + subscribedModels.Remove(notifier); + } + } + } + } + + private void HandleModelOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs) { + // System.Diagnostics.Debug.WriteLine(String.Format("PropertyChanged: '{0}' on '{1}", propertyChangedEventArgs.PropertyName, sender)); + this.RefreshObject(sender); + } + + private Hashtable subscribedModels = new Hashtable(); + + #endregion + + #region Save/Restore State + + /// + /// Return a byte array that represents the current state of the ObjectListView, such + /// that the state can be restored by RestoreState() + /// + /// + /// The state of an ObjectListView includes the attributes that the user can modify: + /// + /// current view (i.e. Details, Tile, Large Icon...) + /// sort column and direction + /// column order + /// column widths + /// column visibility + /// + /// + /// + /// It does not include selection or the scroll position. + /// + /// + /// A byte array representing the state of the ObjectListView + public virtual byte[] SaveState() { + ObjectListViewState olvState = new ObjectListViewState(); + olvState.VersionNumber = 1; + olvState.NumberOfColumns = this.AllColumns.Count; + olvState.CurrentView = this.View; + + // If we have a sort column, it is possible that it is not currently being shown, in which + // case, it's Index will be -1. So we calculate its index directly. Technically, the sort + // column does not even have to a member of AllColumns, in which case IndexOf will return -1, + // which is works fine since we have no way of restoring such a column anyway. + if (this.PrimarySortColumn != null) + olvState.SortColumn = this.AllColumns.IndexOf(this.PrimarySortColumn); + olvState.LastSortOrder = this.PrimarySortOrder; + olvState.IsShowingGroups = this.ShowGroups; + + if (this.AllColumns.Count > 0 && this.AllColumns[0].LastDisplayIndex == -1) + this.RememberDisplayIndicies(); + + foreach (OLVColumn column in this.AllColumns) { + olvState.ColumnIsVisible.Add(column.IsVisible); + olvState.ColumnDisplayIndicies.Add(column.LastDisplayIndex); + olvState.ColumnWidths.Add(column.Width); + } + + // Now that we have stored our state, convert it to a byte array + using (MemoryStream ms = new MemoryStream()) { + BinaryFormatter serializer = new BinaryFormatter(); + serializer.AssemblyFormat = FormatterAssemblyStyle.Simple; + serializer.Serialize(ms, olvState); + return ms.ToArray(); + } + } + + /// + /// Restore the state of the control from the given string, which must have been + /// produced by SaveState() + /// + /// A byte array returned from SaveState() + /// Returns true if the state was restored + public virtual bool RestoreState(byte[] state) { + using (MemoryStream ms = new MemoryStream(state)) { + BinaryFormatter deserializer = new BinaryFormatter(); + ObjectListViewState olvState; + try { + olvState = deserializer.Deserialize(ms) as ObjectListViewState; + } catch (System.Runtime.Serialization.SerializationException) { + return false; + } + // The number of columns has changed. We have no way to match old + // columns to the new ones, so we just give up. + if (olvState == null || olvState.NumberOfColumns != this.AllColumns.Count) + return false; + if (olvState.SortColumn == -1) { + this.PrimarySortColumn = null; + this.PrimarySortOrder = SortOrder.None; + } else { + this.PrimarySortColumn = this.AllColumns[olvState.SortColumn]; + this.PrimarySortOrder = olvState.LastSortOrder; + } + for (int i = 0; i < olvState.NumberOfColumns; i++) { + OLVColumn column = this.AllColumns[i]; + column.Width = (int)olvState.ColumnWidths[i]; + column.IsVisible = (bool)olvState.ColumnIsVisible[i]; + column.LastDisplayIndex = (int)olvState.ColumnDisplayIndicies[i]; + } +// ReSharper disable RedundantCheckBeforeAssignment + if (olvState.IsShowingGroups != this.ShowGroups) +// ReSharper restore RedundantCheckBeforeAssignment + this.ShowGroups = olvState.IsShowingGroups; + if (this.View == olvState.CurrentView) + this.RebuildColumns(); + else + this.View = olvState.CurrentView; + } + + return true; + } + + /// + /// Instances of this class are used to store the state of an ObjectListView. + /// + [Serializable] + internal class ObjectListViewState + { +// ReSharper disable NotAccessedField.Global + public int VersionNumber = 1; +// ReSharper restore NotAccessedField.Global + public int NumberOfColumns = 1; + public View CurrentView; + public int SortColumn = -1; + public bool IsShowingGroups; + public SortOrder LastSortOrder = SortOrder.None; +// ReSharper disable FieldCanBeMadeReadOnly.Global + public ArrayList ColumnIsVisible = new ArrayList(); + public ArrayList ColumnDisplayIndicies = new ArrayList(); + public ArrayList ColumnWidths = new ArrayList(); +// ReSharper restore FieldCanBeMadeReadOnly.Global + } + + #endregion + + #region Event handlers + + /// + /// The application is idle. Trigger a SelectionChanged event. + /// + /// + /// + protected virtual void HandleApplicationIdle(object sender, EventArgs e) { + // Remove the handler before triggering the event + Application.Idle -= new EventHandler(HandleApplicationIdle); + this.hasIdleHandler = false; + + this.OnSelectionChanged(new EventArgs()); + } + + /// + /// The application is idle. Handle the column resizing event. + /// + /// + /// + protected virtual void HandleApplicationIdleResizeColumns(object sender, EventArgs e) { + // Remove the handler before triggering the event + Application.Idle -= new EventHandler(this.HandleApplicationIdleResizeColumns); + this.hasResizeColumnsHandler = false; + + this.ResizeFreeSpaceFillingColumns(); + } + + /// + /// Handle the BeginScroll listview notification + /// + /// + /// True if the event was completely handled + protected virtual bool HandleBeginScroll(ref Message m) { + //System.Diagnostics.Debug.WriteLine("LVN_BEGINSCROLL"); + + NativeMethods.NMLVSCROLL nmlvscroll = (NativeMethods.NMLVSCROLL)m.GetLParam(typeof(NativeMethods.NMLVSCROLL)); + if (nmlvscroll.dx != 0) { + int scrollPositionH = NativeMethods.GetScrollPosition(this, true); + ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, scrollPositionH - nmlvscroll.dx, scrollPositionH, ScrollOrientation.HorizontalScroll); + this.OnScroll(args); + + // Force any empty list msg to redraw when the list is scrolled horizontally + if (this.GetItemCount() == 0) + this.Invalidate(); + } + if (nmlvscroll.dy != 0) { + int scrollPositionV = NativeMethods.GetScrollPosition(this, false); + ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, scrollPositionV - nmlvscroll.dy, scrollPositionV, ScrollOrientation.VerticalScroll); + this.OnScroll(args); + } + + return false; + } + + /// + /// Handle the EndScroll listview notification + /// + /// + /// True if the event was completely handled + protected virtual bool HandleEndScroll(ref Message m) { + //System.Diagnostics.Debug.WriteLine("LVN_BEGINSCROLL"); + + // There is a bug in ListView under XP that causes the gridlines to be incorrectly scrolled + // when the left button is clicked to scroll. This is supposedly documented at + // KB 813791, but I couldn't find it anywhere. You can follow this thread to see the discussion + // http://www.ureader.com/msg/1484143.aspx + + if (!ObjectListView.IsVistaOrLater && ObjectListView.IsLeftMouseDown && this.GridLines) { + this.Invalidate(); + this.Update(); + } + + return false; + } + + /// + /// Handle the LinkClick listview notification + /// + /// + /// True if the event was completely handled + protected virtual bool HandleLinkClick(ref Message m) { + //System.Diagnostics.Debug.WriteLine("HandleLinkClick"); + + NativeMethods.NMLVLINK nmlvlink = (NativeMethods.NMLVLINK)m.GetLParam(typeof(NativeMethods.NMLVLINK)); + + // Find the group that was clicked and trigger an event + foreach (OLVGroup x in this.OLVGroups) { + if (x.GroupId == nmlvlink.iSubItem) { + this.OnGroupTaskClicked(new GroupTaskClickedEventArgs(x)); + return true; + } + } + + return false; + } + + /// + /// The cell tooltip control wants information about the tool tip that it should show. + /// + /// + /// + protected virtual void HandleCellToolTipShowing(object sender, ToolTipShowingEventArgs e) { + this.BuildCellEvent(e, this.PointToClient(Cursor.Position)); + if (e.Item != null) { + e.Text = this.GetCellToolTip(e.ColumnIndex, e.RowIndex); + this.OnCellToolTip(e); + } + } + + /// + /// Allow the HeaderControl to call back into HandleHeaderToolTipShowing without making that method public + /// + /// + /// + internal void HeaderToolTipShowingCallback(object sender, ToolTipShowingEventArgs e) { + this.HandleHeaderToolTipShowing(sender, e); + } + + /// + /// The header tooltip control wants information about the tool tip that it should show. + /// + /// + /// + protected virtual void HandleHeaderToolTipShowing(object sender, ToolTipShowingEventArgs e) { + e.ColumnIndex = this.HeaderControl.ColumnIndexUnderCursor; + if (e.ColumnIndex < 0) + return; + + e.RowIndex = -1; + e.Model = null; + e.Column = this.GetColumn(e.ColumnIndex); + e.Text = this.GetHeaderToolTip(e.ColumnIndex); + e.ListView = this; + this.OnHeaderToolTip(e); + } + + /// + /// Event handler for the column click event + /// + protected virtual void HandleColumnClick(object sender, ColumnClickEventArgs e) { + if (!this.PossibleFinishCellEditing()) + return; + + // Toggle the sorting direction on successive clicks on the same column + if (this.PrimarySortColumn != null && e.Column == this.PrimarySortColumn.Index) + this.PrimarySortOrder = (this.PrimarySortOrder == SortOrder.Descending ? SortOrder.Ascending : SortOrder.Descending); + else + this.PrimarySortOrder = SortOrder.Ascending; + + this.BeginUpdate(); + try { + this.Sort(e.Column); + } finally { + this.EndUpdate(); + } + } + + #endregion + + #region Low level Windows Message handling + + /// + /// Override the basic message pump for this control + /// + /// + protected override void WndProc(ref Message m) + { + + // System.Diagnostics.Debug.WriteLine(m.Msg); + switch (m.Msg) { + case 2: // WM_DESTROY + if (!this.HandleDestroy(ref m)) + base.WndProc(ref m); + break; + //case 0x14: // WM_ERASEBKGND + // Can't do anything here since, when the control is double buffered, anything + // done here is immediately over-drawn + // break; + case 0x0F: // WM_PAINT + if (!this.HandlePaint(ref m)) + base.WndProc(ref m); + break; + case 0x46: // WM_WINDOWPOSCHANGING + if (this.PossibleFinishCellEditing() && !this.HandleWindowPosChanging(ref m)) + base.WndProc(ref m); + break; + case 0x4E: // WM_NOTIFY + if (!this.HandleNotify(ref m)) + base.WndProc(ref m); + break; + case 0x0100: // WM_KEY_DOWN + if (!this.HandleKeyDown(ref m)) + base.WndProc(ref m); + break; + case 0x0102: // WM_CHAR + if (!this.HandleChar(ref m)) + base.WndProc(ref m); + break; + case 0x0200: // WM_MOUSEMOVE + if (!this.HandleMouseMove(ref m)) + base.WndProc(ref m); + break; + case 0x0201: // WM_LBUTTONDOWN + // System.Diagnostics.Debug.WriteLine("WM_LBUTTONDOWN"); + if (this.PossibleFinishCellEditing() && !this.HandleLButtonDown(ref m)) + base.WndProc(ref m); + break; + case 0x202: // WM_LBUTTONUP + // System.Diagnostics.Debug.WriteLine("WM_LBUTTONUP"); + if (this.PossibleFinishCellEditing() && !this.HandleLButtonUp(ref m)) + base.WndProc(ref m); + break; + case 0x0203: // WM_LBUTTONDBLCLK + if (this.PossibleFinishCellEditing() && !this.HandleLButtonDoubleClick(ref m)) + base.WndProc(ref m); + break; + case 0x0204: // WM_RBUTTONDOWN + // System.Diagnostics.Debug.WriteLine("WM_RBUTTONDOWN"); + if (this.PossibleFinishCellEditing() && !this.HandleRButtonDown(ref m)) + base.WndProc(ref m); + break; + case 0x0205: // WM_RBUTTONUP + // System.Diagnostics.Debug.WriteLine("WM_RBUTTONUP"); + base.WndProc(ref m); + break; + case 0x0206: // WM_RBUTTONDBLCLK + if (this.PossibleFinishCellEditing() && !this.HandleRButtonDoubleClick(ref m)) + base.WndProc(ref m); + break; + case 0x204E: // WM_REFLECT_NOTIFY + if (!this.HandleReflectNotify(ref m)) + base.WndProc(ref m); + break; + case 0x114: // WM_HSCROLL: + case 0x115: // WM_VSCROLL: + //System.Diagnostics.Debug.WriteLine("WM_VSCROLL"); + if (this.PossibleFinishCellEditing()) + base.WndProc(ref m); + break; + case 0x20A: // WM_MOUSEWHEEL: + case 0x20E: // WM_MOUSEHWHEEL: + if (this.AllowCellEditorsToProcessMouseWheel && this.IsCellEditing) + break; + if (this.PossibleFinishCellEditing()) + base.WndProc(ref m); + break; + case 0x7B: // WM_CONTEXTMENU + if (!this.HandleContextMenu(ref m)) + base.WndProc(ref m); + break; + case 0x1000 + 18: // LVM_HITTEST: + //System.Diagnostics.Debug.WriteLine("LVM_HITTEST"); + if (this.skipNextHitTest) { + //System.Diagnostics.Debug.WriteLine("SKIPPING LVM_HITTEST"); + this.skipNextHitTest = false; + } else { + base.WndProc(ref m); + } + break; + default: + base.WndProc(ref m); + break; + } + } + + /// + /// Handle the search for item m if possible. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleChar(ref Message m) { + + // Trigger a normal KeyPress event, which listeners can handle if they want. + // Handling the event stops ObjectListView's fancy search-by-typing. + if (this.ProcessKeyEventArgs(ref m)) + return true; + + const int MILLISECONDS_BETWEEN_KEYPRESSES = 1000; + + // What character did the user type and was it part of a longer string? + char character = (char)m.WParam.ToInt32(); //TODO: Will this work on 64 bit or MBCS? + if (character == (char)Keys.Back) { + // Backspace forces the next key to be considered the start of a new search + this.timeLastCharEvent = 0; + return true; + } + + if (System.Environment.TickCount < (this.timeLastCharEvent + MILLISECONDS_BETWEEN_KEYPRESSES)) + this.lastSearchString += character; + else + this.lastSearchString = character.ToString(CultureInfo.InvariantCulture); + + // If this control is showing checkboxes, we want to ignore single space presses, + // since they are used to toggle the selected checkboxes. + if (this.CheckBoxes && this.lastSearchString == " ") { + this.timeLastCharEvent = 0; + return true; + } + + // Where should the search start? + int start = 0; + ListViewItem focused = this.FocusedItem; + if (focused != null) { + start = this.GetDisplayOrderOfItemIndex(focused.Index); + + // If the user presses a single key, we search from after the focused item, + // being careful not to march past the end of the list + if (this.lastSearchString.Length == 1) { + start += 1; + if (start == this.GetItemCount()) + start = 0; + } + } + + // Give the world a chance to fiddle with or completely avoid the searching process + BeforeSearchingEventArgs args = new BeforeSearchingEventArgs(this.lastSearchString, start); + this.OnBeforeSearching(args); + if (args.Canceled) + return true; + + // The parameters of the search may have been changed + string searchString = args.StringToFind; + start = args.StartSearchFrom; + + // Do the actual search + int found = this.FindMatchingRow(searchString, start, SearchDirectionHint.Down); + if (found >= 0) { + // Select and focus on the found item + this.BeginUpdate(); + try { + this.SelectedIndices.Clear(); + OLVListItem lvi = this.GetNthItemInDisplayOrder(found); + if (lvi != null) { + if (lvi.Enabled) + lvi.Selected = true; + lvi.Focused = true; + this.EnsureVisible(lvi.Index); + } + } finally { + this.EndUpdate(); + } + } + + // Tell the world that a search has occurred + AfterSearchingEventArgs args2 = new AfterSearchingEventArgs(searchString, found); + this.OnAfterSearching(args2); + if (!args2.Handled) { + if (found < 0) + System.Media.SystemSounds.Beep.Play(); + } + + // When did this event occur? + this.timeLastCharEvent = System.Environment.TickCount; + return true; + } + private int timeLastCharEvent; + private string lastSearchString; + + /// + /// The user wants to see the context menu. + /// + /// The windows m + /// A bool indicating if this m has been handled + /// + /// We want to ignore context menu requests that are triggered by right clicks on the header + /// + protected virtual bool HandleContextMenu(ref Message m) { + // Don't try to handle context menu commands at design time. + if (this.DesignMode) + return false; + + // If the context menu command was generated by the keyboard, LParam will be -1. + // We don't want to process these. + if (m.LParam == this.minusOne) + return false; + + // If the context menu came from somewhere other than the header control, + // we also don't want to ignore it + if (m.WParam != this.HeaderControl.Handle) + return false; + + // OK. Looks like a right click in the header + if (!this.PossibleFinishCellEditing()) + return true; + + int columnIndex = this.HeaderControl.ColumnIndexUnderCursor; + return this.HandleHeaderRightClick(columnIndex); + } + readonly IntPtr minusOne = new IntPtr(-1); + + /// + /// Handle the Custom draw series of notifications + /// + /// The message + /// True if the message has been handled + protected virtual bool HandleCustomDraw(ref Message m) { + const int CDDS_PREPAINT = 1; + const int CDDS_POSTPAINT = 2; + const int CDDS_PREERASE = 3; + const int CDDS_POSTERASE = 4; + //const int CDRF_NEWFONT = 2; + //const int CDRF_SKIPDEFAULT = 4; + const int CDDS_ITEM = 0x00010000; + const int CDDS_SUBITEM = 0x00020000; + const int CDDS_ITEMPREPAINT = (CDDS_ITEM | CDDS_PREPAINT); + const int CDDS_ITEMPOSTPAINT = (CDDS_ITEM | CDDS_POSTPAINT); + const int CDDS_ITEMPREERASE = (CDDS_ITEM | CDDS_PREERASE); + const int CDDS_ITEMPOSTERASE = (CDDS_ITEM | CDDS_POSTERASE); + const int CDDS_SUBITEMPREPAINT = (CDDS_SUBITEM | CDDS_ITEMPREPAINT); + const int CDDS_SUBITEMPOSTPAINT = (CDDS_SUBITEM | CDDS_ITEMPOSTPAINT); + const int CDRF_NOTIFYPOSTPAINT = 0x10; + //const int CDRF_NOTIFYITEMDRAW = 0x20; + //const int CDRF_NOTIFYSUBITEMDRAW = 0x20; // same value as above! + const int CDRF_NOTIFYPOSTERASE = 0x40; + + // There is a bug in owner drawn virtual lists which causes lots of custom draw messages + // to be sent to the control *outside* of a WmPaint event. AFAIK, these custom draw events + // are spurious and only serve to make the control flicker annoyingly. + // So, we ignore messages that are outside of a paint event. + if (!this.isInWmPaintEvent) + return true; + + // One more complication! Sometimes with owner drawn virtual lists, the act of drawing + // the overlays triggers a second attempt to paint the control -- which makes an annoying + // flicker. So, we only do the custom drawing once per WmPaint event. + if (!this.shouldDoCustomDrawing) + return true; + + NativeMethods.NMLVCUSTOMDRAW nmcustomdraw = (NativeMethods.NMLVCUSTOMDRAW)m.GetLParam(typeof(NativeMethods.NMLVCUSTOMDRAW)); + //System.Diagnostics.Debug.WriteLine(String.Format("cd: {0:x}, {1}, {2}", nmcustomdraw.nmcd.dwDrawStage, nmcustomdraw.dwItemType, nmcustomdraw.nmcd.dwItemSpec)); + + // Ignore drawing of group items + if (nmcustomdraw.dwItemType == 1) { + // This is the basis of an idea about how to owner draw group headers + + //nmcustomdraw.clrText = ColorTranslator.ToWin32(Color.DeepPink); + //nmcustomdraw.clrFace = ColorTranslator.ToWin32(Color.DeepPink); + //nmcustomdraw.clrTextBk = ColorTranslator.ToWin32(Color.DeepPink); + //Marshal.StructureToPtr(nmcustomdraw, m.LParam, false); + //using (Graphics g = Graphics.FromHdc(nmcustomdraw.nmcd.hdc)) { + // g.DrawRectangle(Pens.Red, Rectangle.FromLTRB(nmcustomdraw.rcText.left, nmcustomdraw.rcText.top, nmcustomdraw.rcText.right, nmcustomdraw.rcText.bottom)); + //} + //m.Result = (IntPtr)((int)m.Result | CDRF_SKIPDEFAULT); + return true; + } + + switch (nmcustomdraw.nmcd.dwDrawStage) { + case CDDS_PREPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_PREPAINT"); + // Remember which items were drawn during this paint cycle + if (this.prePaintLevel == 0) + this.drawnItems = new List(); + + // If there are any items, we have to wait until at least one has been painted + // before we draw the overlays. If there aren't any items, there will never be any + // item paint events, so we can draw the overlays whenever + this.isAfterItemPaint = (this.GetItemCount() == 0); + this.prePaintLevel++; + base.WndProc(ref m); + + // Make sure that we get postpaint notifications + m.Result = (IntPtr)((int)m.Result | CDRF_NOTIFYPOSTPAINT | CDRF_NOTIFYPOSTERASE); + return true; + + case CDDS_POSTPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_POSTPAINT"); + this.prePaintLevel--; + + // When in group view, we have two problems. On XP, the control sends + // a whole heap of PREPAINT/POSTPAINT messages before drawing any items. + // We have to wait until after the first item paint before we draw overlays. + // On Vista, we have a different problem. On Vista, the control nests calls + // to PREPAINT and POSTPAINT. We only want to draw overlays on the outermost + // POSTPAINT. + if (this.prePaintLevel == 0 && (this.isMarqueSelecting || this.isAfterItemPaint)) { + this.shouldDoCustomDrawing = false; + + // Draw our overlays after everything has been drawn + using (Graphics g = Graphics.FromHdc(nmcustomdraw.nmcd.hdc)) { + this.DrawAllDecorations(g, this.drawnItems); + } + } + break; + + case CDDS_ITEMPREPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPREPAINT"); + + // When in group view on XP, the control send a whole heap of PREPAINT/POSTPAINT + // messages before drawing any items. + // We have to wait until after the first item paint before we draw overlays + this.isAfterItemPaint = true; + + // This scheme of catching custom draw msgs works fine, except + // for Tile view. Something in .NET's handling of Tile view causes lots + // of invalidates and erases. So, we just ignore completely + // .NET's handling of Tile view and let the underlying control + // do its stuff. Strangely, if the Tile view is + // completely owner drawn, those erasures don't happen. + if (this.View == View.Tile) { + if (this.OwnerDraw && this.ItemRenderer != null) + base.WndProc(ref m); + } else { + base.WndProc(ref m); + } + + m.Result = (IntPtr)((int)m.Result | CDRF_NOTIFYPOSTPAINT | CDRF_NOTIFYPOSTERASE); + return true; + + case CDDS_ITEMPOSTPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPOSTPAINT"); + // Remember which items have been drawn so we can draw any decorations for them + // once all other painting is finished + if (this.Columns.Count > 0) { + OLVListItem olvi = this.GetItem((int)nmcustomdraw.nmcd.dwItemSpec); + if (olvi != null) + this.drawnItems.Add(olvi); + } + break; + + case CDDS_SUBITEMPREPAINT: + //System.Diagnostics.Debug.WriteLine(String.Format("CDDS_SUBITEMPREPAINT ({0},{1})", (int)nmcustomdraw.nmcd.dwItemSpec, nmcustomdraw.iSubItem)); + + // There is a bug in the .NET framework which appears when column 0 of an owner drawn listview + // is dragged to another column position. + // The bounds calculation always returns the left edge of column 0 as being 0. + // The effects of this bug become apparent + // when the listview is scrolled horizontally: the control can think that column 0 + // is no longer visible (the horizontal scroll position is subtracted from the bounds, giving a + // rectangle that is offscreen). In those circumstances, column 0 is not redraw because + // the control thinks it is not visible and so does not trigger a DrawSubItem event. + + // To fix this problem, we have to detected the situation -- owner drawing column 0 in any column except 0 -- + // trigger our own DrawSubItem, and then prevent the default processing from occurring. + + // Are we owner drawing column 0 when it's in any column except 0? + if (!this.OwnerDraw) + return false; + + int columnIndex = nmcustomdraw.iSubItem; + if (columnIndex != 0) + return false; + + int displayIndex = this.Columns[0].DisplayIndex; + if (displayIndex == 0) + return false; + + int rowIndex = (int)nmcustomdraw.nmcd.dwItemSpec; + OLVListItem item = this.GetItem(rowIndex); + if (item == null) + return false; + + // OK. We have the error condition, so lets do what the .NET framework should do. + // Trigger an event to draw column 0 when it is not at display index 0 + using (Graphics g = Graphics.FromHdc(nmcustomdraw.nmcd.hdc)) { + + // Correctly calculate the bounds of cell 0 + Rectangle r = item.GetSubItemBounds(0); + + // We can hardcode "0" here since we know we are only doing this for column 0 + DrawListViewSubItemEventArgs args = new DrawListViewSubItemEventArgs(g, r, item, item.SubItems[0], rowIndex, 0, + this.Columns[0], (ListViewItemStates)nmcustomdraw.nmcd.uItemState); + this.OnDrawSubItem(args); + + // If the event handler wants to do the default processing (i.e. DrawDefault = true), we are stuck. + // There is no way we can force the default drawing because of the bug in .NET we are trying to get around. + System.Diagnostics.Trace.Assert(!args.DrawDefault, "Default drawing is impossible in this situation"); + } + m.Result = (IntPtr)4; + + return true; + + case CDDS_SUBITEMPOSTPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_SUBITEMPOSTPAINT"); + break; + + // I have included these stages, but it doesn't seem that they are sent for ListViews. + // http://www.tech-archive.net/Archive/VC/microsoft.public.vc.mfc/2006-08/msg00220.html + + case CDDS_PREERASE: + //System.Diagnostics.Debug.WriteLine("CDDS_PREERASE"); + break; + + case CDDS_POSTERASE: + //System.Diagnostics.Debug.WriteLine("CDDS_POSTERASE"); + break; + + case CDDS_ITEMPREERASE: + //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPREERASE"); + break; + + case CDDS_ITEMPOSTERASE: + //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPOSTERASE"); + break; + } + + return false; + } + bool isAfterItemPaint; + List drawnItems; + + /// + /// Handle the underlying control being destroyed + /// + /// + /// + protected virtual bool HandleDestroy(ref Message m) { + //System.Diagnostics.Debug.WriteLine(String.Format("WM_DESTROY: Disposing={0}, IsDisposed={1}, VirtualMode={2}", Disposing, IsDisposed, VirtualMode)); + + // Recreate the header control when the listview control is destroyed + this.headerControl = null; + + // When the underlying control is destroyed, we need to recreate and reconfigure its tooltip + if (this.cellToolTip != null) { + this.cellToolTip.PushSettings(); + this.BeginInvoke((MethodInvoker)delegate { + this.UpdateCellToolTipHandle(); + this.cellToolTip.PopSettings(); + }); + } + + return false; + } + + /// + /// Handle the search for item m if possible. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleFindItem(ref Message m) { + // NOTE: As far as I can see, this message is never actually sent to the control, making this + // method redundant! + + const int LVFI_STRING = 0x0002; + + NativeMethods.LVFINDINFO findInfo = (NativeMethods.LVFINDINFO)m.GetLParam(typeof(NativeMethods.LVFINDINFO)); + + // We can only handle string searches + if ((findInfo.flags & LVFI_STRING) != LVFI_STRING) + return false; + + int start = m.WParam.ToInt32(); + m.Result = (IntPtr)this.FindMatchingRow(findInfo.psz, start, SearchDirectionHint.Down); + return true; + } + + /// + /// Find the first row after the given start in which the text value in the + /// comparison column begins with the given text. The comparison column is column 0, + /// unless IsSearchOnSortColumn is true, in which case the current sort column is used. + /// + /// The text to be prefix matched + /// The index of the first row to consider + /// Which direction should be searched? + /// The index of the first row that matched, or -1 + /// The text comparison is a case-insensitive, prefix match. The search will + /// search the every row until a match is found, wrapping at the end if needed. + public virtual int FindMatchingRow(string text, int start, SearchDirectionHint direction) { + // We also can't do anything if we don't have data + if (this.Columns.Count == 0) + return -1; + int rowCount = this.GetItemCount(); + if (rowCount == 0) + return -1; + + // Which column are we going to use for our comparing? + OLVColumn column = this.GetColumn(0); + if (this.IsSearchOnSortColumn && this.View == View.Details && this.PrimarySortColumn != null) + column = this.PrimarySortColumn; + + // Do two searches if necessary to find a match. The second search is the wrap-around part of searching + int i; + if (direction == SearchDirectionHint.Down) { + i = this.FindMatchInRange(text, start, rowCount - 1, column); + if (i == -1 && start > 0) + i = this.FindMatchInRange(text, 0, start - 1, column); + } else { + i = this.FindMatchInRange(text, start, 0, column); + if (i == -1 && start != rowCount) + i = this.FindMatchInRange(text, rowCount - 1, start + 1, column); + } + + return i; + } + + /// + /// Find the first row in the given range of rows that prefix matches the string value of the given column. + /// + /// + /// + /// + /// + /// The index of the matched row, or -1 + protected virtual int FindMatchInRange(string text, int first, int last, OLVColumn column) { + if (first <= last) { + for (int i = first; i <= last; i++) { + string data = column.GetStringValue(this.GetNthItemInDisplayOrder(i).RowObject); + if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } else { + for (int i = first; i >= last; i--) { + string data = column.GetStringValue(this.GetNthItemInDisplayOrder(i).RowObject); + if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } + + return -1; + } + + /// + /// Handle the Group Info series of notifications + /// + /// The message + /// True if the message has been handled + protected virtual bool HandleGroupInfo(ref Message m) + { + NativeMethods.NMLVGROUP nmlvgroup = (NativeMethods.NMLVGROUP)m.GetLParam(typeof(NativeMethods.NMLVGROUP)); + + //System.Diagnostics.Debug.WriteLine(String.Format("group: {0}, old state: {1}, new state: {2}", + // nmlvgroup.iGroupId, StateToString(nmlvgroup.uOldState), StateToString(nmlvgroup.uNewState))); + + // Ignore state changes that aren't related to selection, focus or collapsedness + const uint INTERESTING_STATES = (uint) (GroupState.LVGS_COLLAPSED | GroupState.LVGS_FOCUSED | GroupState.LVGS_SELECTED); + if ((nmlvgroup.uOldState & INTERESTING_STATES) == (nmlvgroup.uNewState & INTERESTING_STATES)) + return false; + + foreach (OLVGroup group in this.OLVGroups) { + if (group.GroupId == nmlvgroup.iGroupId) { + GroupStateChangedEventArgs args = new GroupStateChangedEventArgs(group, (GroupState)nmlvgroup.uOldState, (GroupState)nmlvgroup.uNewState); + this.OnGroupStateChanged(args); + break; + } + } + + return false; + } + + //private static string StateToString(uint state) + //{ + // if (state == 0) + // return Enum.GetName(typeof(GroupState), 0); + // + // List names = new List(); + // foreach (int value in Enum.GetValues(typeof(GroupState))) + // { + // if (value != 0 && (state & value) == value) + // { + // names.Add(Enum.GetName(typeof(GroupState), value)); + // } + // } + // return names.Count == 0 ? state.ToString("x") : String.Join("|", names.ToArray()); + //} + + /// + /// Handle a key down message + /// + /// + /// True if the msg has been handled + protected virtual bool HandleKeyDown(ref Message m) { + + // If this is a checkbox list, toggle the selected rows when the user presses Space + if (this.CheckBoxes && m.WParam.ToInt32() == (int)Keys.Space && this.SelectedIndices.Count > 0) { + this.ToggleSelectedRowCheckBoxes(); + return true; + } + + // Remember the scroll position so we can decide if the listview has scrolled in the + // handling of the event. + int scrollPositionH = NativeMethods.GetScrollPosition(this, true); + int scrollPositionV = NativeMethods.GetScrollPosition(this, false); + + base.WndProc(ref m); + + // It's possible that the processing in base.WndProc has actually destroyed this control + if (this.IsDisposed) + return true; + + // If the keydown processing changed the scroll position, trigger a Scroll event + int newScrollPositionH = NativeMethods.GetScrollPosition(this, true); + int newScrollPositionV = NativeMethods.GetScrollPosition(this, false); + + if (scrollPositionH != newScrollPositionH) { + ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, + scrollPositionH, newScrollPositionH, ScrollOrientation.HorizontalScroll); + this.OnScroll(args); + } + if (scrollPositionV != newScrollPositionV) { + ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, + scrollPositionV, newScrollPositionV, ScrollOrientation.VerticalScroll); + this.OnScroll(args); + } + + if (scrollPositionH != newScrollPositionH || scrollPositionV != newScrollPositionV) + this.RefreshHotItem(); + + return true; + } + + /// + /// Toggle the checkedness of the selected rows + /// + /// + /// + /// Actually, this doesn't actually toggle all rows. It toggles the first row, and + /// all other rows get the check state of that first row. This is actually a much + /// more useful behaviour. + /// + /// + /// If no rows are selected, this method does nothing. + /// + /// + public void ToggleSelectedRowCheckBoxes() { + if (this.SelectedIndices.Count == 0) + return; + Object primaryModel = this.GetItem(this.SelectedIndices[0]).RowObject; + this.ToggleCheckObject(primaryModel); + CheckState? state = this.GetCheckState(primaryModel); + if (state.HasValue) { + foreach (Object x in this.SelectedObjects) + this.SetObjectCheckedness(x, state.Value); + } + } + + /// + /// Catch the Left Button down event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleLButtonDown(ref Message m) { + // If there are no columns, the base ListView class can throw OutOfRange exceptions. + if (this.Columns.Count == 0) + return true; + + // We have to intercept this low level message rather than the more natural + // overridding of OnMouseDown, since ListCtrl's internal mouse down behavior + // is to select (or deselect) rows when the mouse is released. We don't + // want the selection to change when the user checks or unchecks a checkbox, so if the + // mouse down event was to check/uncheck, we have to hide this mouse + // down event from the control. + + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + return this.ProcessLButtonDown(this.OlvHitTest(x, y)); + } + + /// + /// Handle a left mouse down at the given hit test location + /// + /// Subclasses can override this to do something unique + /// + /// True if the message has been handled + protected virtual bool ProcessLButtonDown(OlvListViewHitTestInfo hti) { + + if (hti.Item == null) + return false; + + // If the click occurs on a button, ignore it so the row isn't selected + if (hti.HitTestLocation == HitTestLocation.Button) { + this.Invalidate(); + + return true; + } + + // If they didn't click checkbox, we can just return + if (hti.HitTestLocation != HitTestLocation.CheckBox) + return false; + + // Disabled rows cannot change checkboxes + if (!hti.Item.Enabled) + return true; + + // Did they click a sub item checkbox? + if (hti.Column != null && hti.Column.Index > 0) { + if (hti.Column.IsEditable && hti.Item.Enabled) + this.ToggleSubItemCheckBox(hti.RowObject, hti.Column); + return true; + } + + // They must have clicked the primary checkbox + this.ToggleCheckObject(hti.RowObject); + + // If they change the checkbox of a selected row, all the rows in the selection + // should be given the same state + if (hti.Item.Selected) { + CheckState? state = this.GetCheckState(hti.RowObject); + if (state.HasValue) { + foreach (Object x in this.SelectedObjects) + this.SetObjectCheckedness(x, state.Value); + } + } + + return true; + } + + /// + /// Catch the Left Button up event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleLButtonUp(ref Message m) { + // If there are no columns, the base ListView class can throw OutOfRange exceptions. + if (this.Columns.Count == 0) + return true; + + if (this.MouseMoveHitTest == null) + return false; + + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + // Did they click an enabled, non-empty button? + if (this.MouseMoveHitTest.HitTestLocation == HitTestLocation.Button) { + // If a button was hit, Item and Column must be non-null + if (this.MouseMoveHitTest.Item.Enabled || this.MouseMoveHitTest.Column.EnableButtonWhenItemIsDisabled) { + string buttonText = this.MouseMoveHitTest.Column.GetStringValue(this.MouseMoveHitTest.RowObject); + if (!String.IsNullOrEmpty(buttonText)) { + this.Invalidate(); + CellClickEventArgs args = new CellClickEventArgs(); + this.BuildCellEvent(args, new Point(x, y), this.MouseMoveHitTest); + this.OnButtonClick(args); + return true; + } + } + } + + // Are they trying to expand/collapse a group? + if (this.MouseMoveHitTest.HitTestLocation == HitTestLocation.GroupExpander) { + if (this.TriggerGroupExpandCollapse(this.MouseMoveHitTest.Group)) + return true; + } + + if (ObjectListView.IsVistaOrLater && this.HasCollapsibleGroups) + base.DefWndProc(ref m); + + return false; + } + + /// + /// Trigger a GroupExpandCollapse event and return true if the action was cancelled + /// + /// + /// + protected virtual bool TriggerGroupExpandCollapse(OLVGroup group) + { + GroupExpandingCollapsingEventArgs args = new GroupExpandingCollapsingEventArgs(group); + this.OnGroupExpandingCollapsing(args); + return args.Canceled; + } + + /// + /// Catch the Right Button down event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleRButtonDown(ref Message m) { + // If there are no columns, the base ListView class can throw OutOfRange exceptions. + if (this.Columns.Count == 0) + return true; + + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + return this.ProcessRButtonDown(this.OlvHitTest(x, y)); + } + + /// + /// Handle a left mouse down at the given hit test location + /// + /// Subclasses can override this to do something unique + /// + /// True if the message has been handled + protected virtual bool ProcessRButtonDown(OlvListViewHitTestInfo hti) { + if (hti.Item == null) + return false; + + // Ignore clicks on checkboxes + return (hti.HitTestLocation == HitTestLocation.CheckBox); + } + + /// + /// Catch the Left Button double click event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleLButtonDoubleClick(ref Message m) { + // If there are no columns, the base ListView class can throw OutOfRange exceptions. + if (this.Columns.Count == 0) + return true; + + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + return this.ProcessLButtonDoubleClick(this.OlvHitTest(x, y)); + } + + /// + /// Handle a mouse double click at the given hit test location + /// + /// Subclasses can override this to do something unique + /// + /// True if the message has been handled + protected virtual bool ProcessLButtonDoubleClick(OlvListViewHitTestInfo hti) { + // If the user double clicked on a checkbox, ignore it + return (hti.HitTestLocation == HitTestLocation.CheckBox); + } + + /// + /// Catch the right Button double click event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleRButtonDoubleClick(ref Message m) { + + // If there are no columns, the base ListView class can throw OutOfRange exceptions. + if (this.Columns.Count == 0) + return true; + + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + return this.ProcessRButtonDoubleClick(this.OlvHitTest(x, y)); + } + + /// + /// Handle a right mouse double click at the given hit test location + /// + /// Subclasses can override this to do something unique + /// + /// True if the message has been handled + protected virtual bool ProcessRButtonDoubleClick(OlvListViewHitTestInfo hti) { + + // If the user double clicked on a checkbox, ignore it + return (hti.HitTestLocation == HitTestLocation.CheckBox); + } + + /// + /// Catch the MouseMove event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleMouseMove(ref Message m) + { + //int x = m.LParam.ToInt32() & 0xFFFF; + //int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + //this.lastMouseMoveX = x; + //this.lastMouseMoveY = y; + + return false; + } + //private int lastMouseMoveX = -1; + //private int lastMouseMoveY = -1; + + /// + /// Handle notifications that have been reflected back from the parent window + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleReflectNotify(ref Message m) { + const int NM_CLICK = -2; + const int NM_DBLCLK = -3; + const int NM_RDBLCLK = -6; + const int NM_CUSTOMDRAW = -12; + const int NM_RELEASEDCAPTURE = -16; + const int LVN_FIRST = -100; + const int LVN_ITEMCHANGED = LVN_FIRST - 1; + const int LVN_ITEMCHANGING = LVN_FIRST - 0; + const int LVN_HOTTRACK = LVN_FIRST - 21; + const int LVN_MARQUEEBEGIN = LVN_FIRST - 56; + const int LVN_GETINFOTIP = LVN_FIRST - 58; + const int LVN_GETDISPINFO = LVN_FIRST - 77; + const int LVN_BEGINSCROLL = LVN_FIRST - 80; + const int LVN_ENDSCROLL = LVN_FIRST - 81; + const int LVN_LINKCLICK = LVN_FIRST - 84; + const int LVN_GROUPINFO = LVN_FIRST - 88; // undocumented + const int LVIF_STATE = 8; + //const int LVIS_FOCUSED = 1; + const int LVIS_SELECTED = 2; + + bool isMsgHandled = false; + + // TODO: Don't do any logic in this method. Create separate methods for each message + + NativeMethods.NMHDR nmhdr = (NativeMethods.NMHDR)m.GetLParam(typeof(NativeMethods.NMHDR)); + //System.Diagnostics.Debug.WriteLine(String.Format("rn: {0}", nmhdr->code)); + + switch (nmhdr.code) { + case NM_CLICK: + // The standard ListView does some strange stuff here when the list has checkboxes. + // If you shift click on non-primary columns when FullRowSelect is true, the + // checkedness of the selected rows changes. + // We can't just not do the base class stuff because it sets up state that is used to + // determine mouse up events later on. + // So, we sabotage the base class's process of the click event. The base class does a HITTEST + // in order to determine which row was clicked -- if that fails, the base class does nothing. + // So when we get a CLICK, we know that the base class is going to send a HITTEST very soon, + // so we ignore the next HITTEST message, which will cause the click processing to fail. + //System.Diagnostics.Debug.WriteLine("NM_CLICK"); + this.skipNextHitTest = true; + break; + + case LVN_BEGINSCROLL: + //System.Diagnostics.Debug.WriteLine("LVN_BEGINSCROLL"); + isMsgHandled = this.HandleBeginScroll(ref m); + break; + + case LVN_ENDSCROLL: + isMsgHandled = this.HandleEndScroll(ref m); + break; + + case LVN_LINKCLICK: + isMsgHandled = this.HandleLinkClick(ref m); + break; + + case LVN_MARQUEEBEGIN: + //System.Diagnostics.Debug.WriteLine("LVN_MARQUEEBEGIN"); + this.isMarqueSelecting = true; + break; + + case LVN_GETINFOTIP: + //System.Diagnostics.Debug.WriteLine("LVN_GETINFOTIP"); + // When virtual lists are in SmallIcon view, they generates tooltip message with invalid item indices. + NativeMethods.NMLVGETINFOTIP nmGetInfoTip = (NativeMethods.NMLVGETINFOTIP)m.GetLParam(typeof(NativeMethods.NMLVGETINFOTIP)); + isMsgHandled = nmGetInfoTip.iItem >= this.GetItemCount() || this.Columns.Count == 0; + break; + + case NM_RELEASEDCAPTURE: + //System.Diagnostics.Debug.WriteLine("NM_RELEASEDCAPTURE"); + this.isMarqueSelecting = false; + this.Invalidate(); + break; + + case NM_CUSTOMDRAW: + //System.Diagnostics.Debug.WriteLine("NM_CUSTOMDRAW"); + isMsgHandled = this.HandleCustomDraw(ref m); + break; + + case NM_DBLCLK: + // The default behavior of a .NET ListView with checkboxes is to toggle the checkbox on + // double-click. That's just silly, if you ask me :) + if (this.CheckBoxes) { + // How do we make ListView not do that silliness? We could just ignore the message + // but the last part of the base code sets up state information, and without that + // state, the ListView doesn't trigger MouseDoubleClick events. So we fake a + // right button double click event, which sets up the same state, but without + // toggling the checkbox. + nmhdr.code = NM_RDBLCLK; + Marshal.StructureToPtr(nmhdr, m.LParam, false); + } + break; + + case LVN_ITEMCHANGED: + //System.Diagnostics.Debug.WriteLine("LVN_ITEMCHANGED"); + NativeMethods.NMLISTVIEW nmlistviewPtr2 = (NativeMethods.NMLISTVIEW)m.GetLParam(typeof(NativeMethods.NMLISTVIEW)); + if ((nmlistviewPtr2.uChanged & LVIF_STATE) != 0) { + CheckState currentValue = this.CalculateCheckState(nmlistviewPtr2.uOldState); + CheckState newCheckValue = this.CalculateCheckState(nmlistviewPtr2.uNewState); + if (currentValue != newCheckValue) + { + // Remove the state indices so that we don't trigger the OnItemChecked method + // when we call our base method after exiting this method + nmlistviewPtr2.uOldState = (nmlistviewPtr2.uOldState & 0x0FFF); + nmlistviewPtr2.uNewState = (nmlistviewPtr2.uNewState & 0x0FFF); + Marshal.StructureToPtr(nmlistviewPtr2, m.LParam, false); + } + else + { + bool isSelected = (nmlistviewPtr2.uNewState & LVIS_SELECTED) == LVIS_SELECTED; + + if (isSelected) + { + // System.Diagnostics.Debug.WriteLine(String.Format("Selected: {0}", nmlistviewPtr2.iItem)); + bool isShiftDown = (Control.ModifierKeys & Keys.Shift) == Keys.Shift; + + // -1 indicates that all rows are to be selected -- in fact, they already have been. + // We now have to deselect all the disabled objects. + if (nmlistviewPtr2.iItem == -1 || isShiftDown) { + Stopwatch sw = Stopwatch.StartNew(); + foreach (object disabledModel in this.DisabledObjects) + { + int modelIndex = this.IndexOf(disabledModel); + if (modelIndex >= 0) + NativeMethods.DeselectOneItem(this, modelIndex); + } + // System.Diagnostics.Debug.WriteLine(String.Format("PERF - Deselecting took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks)); + } + else + { + // If the object just selected is disabled, explicitly de-select it + OLVListItem olvi = this.GetItem(nmlistviewPtr2.iItem); + if (olvi != null && !olvi.Enabled) + NativeMethods.DeselectOneItem(this, nmlistviewPtr2.iItem); + } + } + } + } + break; + + case LVN_ITEMCHANGING: + //System.Diagnostics.Debug.WriteLine("LVN_ITEMCHANGING"); + NativeMethods.NMLISTVIEW nmlistviewPtr = (NativeMethods.NMLISTVIEW)m.GetLParam(typeof(NativeMethods.NMLISTVIEW)); + if ((nmlistviewPtr.uChanged & LVIF_STATE) != 0) { + CheckState currentValue = this.CalculateCheckState(nmlistviewPtr.uOldState); + CheckState newCheckValue = this.CalculateCheckState(nmlistviewPtr.uNewState); + + if (currentValue != newCheckValue) { + // Prevent the base method from seeing the state change, + // since we handled it elsewhere + nmlistviewPtr.uChanged &= ~LVIF_STATE; + Marshal.StructureToPtr(nmlistviewPtr, m.LParam, false); + } + } + break; + + case LVN_HOTTRACK: + break; + + case LVN_GETDISPINFO: + break; + + case LVN_GROUPINFO: + //System.Diagnostics.Debug.WriteLine("reflect notify: GROUP INFO"); + isMsgHandled = this.HandleGroupInfo(ref m); + break; + + //default: + //System.Diagnostics.Debug.WriteLine(String.Format("reflect notify: {0}", nmhdr.code)); + //break; + } + + return isMsgHandled; + } + private bool skipNextHitTest; + + private CheckState CalculateCheckState(int state) { + switch ((state & 0xf000) >> 12) { + case 1: + return CheckState.Unchecked; + case 2: + return CheckState.Checked; + case 3: + return CheckState.Indeterminate; + default: + return CheckState.Checked; + } + } + + /// + /// In the notification messages, we handle attempts to change the width of our columns + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected bool HandleNotify(ref Message m) { + bool isMsgHandled = false; + + const int NM_CUSTOMDRAW = -12; + + const int HDN_FIRST = (0 - 300); + const int HDN_ITEMCHANGINGA = (HDN_FIRST - 0); + const int HDN_ITEMCHANGINGW = (HDN_FIRST - 20); + const int HDN_ITEMCLICKA = (HDN_FIRST - 2); + const int HDN_ITEMCLICKW = (HDN_FIRST - 22); + const int HDN_DIVIDERDBLCLICKA = (HDN_FIRST - 5); + const int HDN_DIVIDERDBLCLICKW = (HDN_FIRST - 25); + const int HDN_BEGINTRACKA = (HDN_FIRST - 6); + const int HDN_BEGINTRACKW = (HDN_FIRST - 26); + const int HDN_ENDTRACKA = (HDN_FIRST - 7); + const int HDN_ENDTRACKW = (HDN_FIRST - 27); + const int HDN_TRACKA = (HDN_FIRST - 8); + const int HDN_TRACKW = (HDN_FIRST - 28); + + // Handle the notification, remembering to handle both ANSI and Unicode versions + NativeMethods.NMHEADER nmheader = (NativeMethods.NMHEADER)m.GetLParam(typeof(NativeMethods.NMHEADER)); + //System.Diagnostics.Debug.WriteLine(String.Format("not: {0}", nmhdr->code)); + + //if (nmhdr.code < HDN_FIRST) + // System.Diagnostics.Debug.WriteLine(nmhdr.code); + + // In KB Article #183258, MS states that when a header control has the HDS_FULLDRAG style, it will receive + // ITEMCHANGING events rather than TRACK events. Under XP SP2 (at least) this is not always true, which may be + // why MS has withdrawn that particular KB article. It is true that the header is always given the HDS_FULLDRAG + // style. But even while window style set, the control doesn't always received ITEMCHANGING events. + // The controlling setting seems to be the Explorer option "Show Window Contents While Dragging"! + // In the category of "truly bizarre side effects", if the this option is turned on, we will receive + // ITEMCHANGING events instead of TRACK events. But if it is turned off, we receive lots of TRACK events and + // only one ITEMCHANGING event at the very end of the process. + // If we receive HDN_TRACK messages, it's harder to control the resizing process. If we return a result of 1, we + // cancel the whole drag operation, not just that particular track event, which is clearly not what we want. + // If we are willing to compile with unsafe code enabled, we can modify the size of the column in place, using the + // commented out code below. But without unsafe code, the best we can do is allow the user to drag the column to + // any width, and then spring it back to within bounds once they release the mouse button. UI-wise it's very ugly. + switch (nmheader.nhdr.code) { + + case NM_CUSTOMDRAW: + if (!this.OwnerDrawnHeader) + isMsgHandled = this.HeaderControl.HandleHeaderCustomDraw(ref m); + break; + + case HDN_ITEMCLICKA: + case HDN_ITEMCLICKW: + if (!this.PossibleFinishCellEditing()) + { + m.Result = (IntPtr)1; // prevent the change from happening + isMsgHandled = true; + } + break; + + case HDN_DIVIDERDBLCLICKA: + case HDN_DIVIDERDBLCLICKW: + case HDN_BEGINTRACKA: + case HDN_BEGINTRACKW: + if (!this.PossibleFinishCellEditing()) { + m.Result = (IntPtr)1; // prevent the change from happening + isMsgHandled = true; + break; + } + if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) { + OLVColumn column = this.GetColumn(nmheader.iItem); + // Space filling columns can't be dragged or double-click resized + if (column.FillsFreeSpace) { + m.Result = (IntPtr)1; // prevent the change from happening + isMsgHandled = true; + } + } + break; + case HDN_ENDTRACKA: + case HDN_ENDTRACKW: + //if (this.ShowGroups) + // this.ResizeLastGroup(); + break; + case HDN_TRACKA: + case HDN_TRACKW: + if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) { + NativeMethods.HDITEM hditem = (NativeMethods.HDITEM)Marshal.PtrToStructure(nmheader.pHDITEM, typeof(NativeMethods.HDITEM)); + OLVColumn column = this.GetColumn(nmheader.iItem); + if (hditem.cxy < column.MinimumWidth) + hditem.cxy = column.MinimumWidth; + else if (column.MaximumWidth != -1 && hditem.cxy > column.MaximumWidth) + hditem.cxy = column.MaximumWidth; + Marshal.StructureToPtr(hditem, nmheader.pHDITEM, false); + } + break; + + case HDN_ITEMCHANGINGA: + case HDN_ITEMCHANGINGW: + nmheader = (NativeMethods.NMHEADER)m.GetLParam(typeof(NativeMethods.NMHEADER)); + if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) { + NativeMethods.HDITEM hditem = (NativeMethods.HDITEM)Marshal.PtrToStructure(nmheader.pHDITEM, typeof(NativeMethods.HDITEM)); + OLVColumn column = this.GetColumn(nmheader.iItem); + // Check the mask to see if the width field is valid, and if it is, make sure it's within range + if ((hditem.mask & 1) == 1) { + if (hditem.cxy < column.MinimumWidth || + (column.MaximumWidth != -1 && hditem.cxy > column.MaximumWidth)) { + m.Result = (IntPtr)1; // prevent the change from happening + isMsgHandled = true; + } + } + } + break; + + case ToolTipControl.TTN_SHOW: + //System.Diagnostics.Debug.WriteLine("olv TTN_SHOW"); + if (this.CellToolTip.Handle == nmheader.nhdr.hwndFrom) + isMsgHandled = this.CellToolTip.HandleShow(ref m); + break; + + case ToolTipControl.TTN_POP: + //System.Diagnostics.Debug.WriteLine("olv TTN_POP"); + if (this.CellToolTip.Handle == nmheader.nhdr.hwndFrom) + isMsgHandled = this.CellToolTip.HandlePop(ref m); + break; + + case ToolTipControl.TTN_GETDISPINFO: + //System.Diagnostics.Debug.WriteLine("olv TTN_GETDISPINFO"); + if (this.CellToolTip.Handle == nmheader.nhdr.hwndFrom) + isMsgHandled = this.CellToolTip.HandleGetDispInfo(ref m); + break; + +// default: +// System.Diagnostics.Debug.WriteLine(String.Format("notify: {0}", nmheader.nhdr.code)); +// break; + } + + return isMsgHandled; + } + + /// + /// Create a ToolTipControl to manage the tooltip control used by the listview control + /// + protected virtual void CreateCellToolTip() { + this.cellToolTip = new ToolTipControl(); + this.cellToolTip.AssignHandle(NativeMethods.GetTooltipControl(this)); + this.cellToolTip.Showing += new EventHandler(HandleCellToolTipShowing); + this.cellToolTip.SetMaxWidth(); + NativeMethods.MakeTopMost(this.cellToolTip); + } + + /// + /// Update the handle used by our cell tooltip to be the tooltip used by + /// the underlying Windows listview control. + /// + protected virtual void UpdateCellToolTipHandle() { + if (this.cellToolTip != null && this.cellToolTip.Handle == IntPtr.Zero) + this.cellToolTip.AssignHandle(NativeMethods.GetTooltipControl(this)); + } + + /// + /// Handle the WM_PAINT event + /// + /// + /// Return true if the msg has been handled and nothing further should be done + protected virtual bool HandlePaint(ref Message m) { + //System.Diagnostics.Debug.WriteLine("> WMPAINT"); + + // We only want to custom draw the control within WmPaint message and only + // once per paint event. We use these bools to insure this. + this.isInWmPaintEvent = true; + this.shouldDoCustomDrawing = true; + this.prePaintLevel = 0; + + this.ShowOverlays(); + + this.HandlePrePaint(); + base.WndProc(ref m); + this.HandlePostPaint(); + this.isInWmPaintEvent = false; + //System.Diagnostics.Debug.WriteLine("< WMPAINT"); + return true; + } + private int prePaintLevel; + + /// + /// Perform any steps needed before painting the control + /// + protected virtual void HandlePrePaint() { + // When we get a WM_PAINT msg, remember the rectangle that is being updated. + // We can't get this information later, since the BeginPaint call wipes it out. + // this.lastUpdateRectangle = NativeMethods.GetUpdateRect(this); // we no longer need this, but keep the code so we can see it later + + //// When the list is empty, we want to handle the drawing of the control by ourselves. + //// Unfortunately, there is no easy way to tell our superclass that we want to do this. + //// So we resort to guile and deception. We validate the list area of the control, which + //// effectively tells our superclass that this area does not need to be painted. + //// Our superclass will then not paint the control, leaving us free to do so ourselves. + //// Without doing this trickery, the superclass will draw the list as empty, + //// and then moments later, we will draw the empty list msg, giving a nasty flicker + //if (this.GetItemCount() == 0 && this.HasEmptyListMsg) + // NativeMethods.ValidateRect(this, this.ClientRectangle); + } + + /// + /// Perform any steps needed after painting the control + /// + protected virtual void HandlePostPaint() { + // This message is no longer necessary, but we keep it for compatibility + } + + /// + /// Handle the window position changing. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleWindowPosChanging(ref Message m) { + const int SWP_NOSIZE = 1; + + NativeMethods.WINDOWPOS pos = (NativeMethods.WINDOWPOS)m.GetLParam(typeof(NativeMethods.WINDOWPOS)); + if ((pos.flags & SWP_NOSIZE) == 0) { + if (pos.cx < this.Bounds.Width) // only when shrinking + // pos.cx is the window width, not the client area width, so we have to subtract the border widths + this.ResizeFreeSpaceFillingColumns(pos.cx - (this.Bounds.Width - this.ClientSize.Width)); + } + + return false; + } + + #endregion + + #region Column header clicking, column hiding and resizing + + /// + /// The user has right clicked on the column headers. Do whatever is required + /// + /// Return true if this event has been handle + protected virtual bool HandleHeaderRightClick(int columnIndex) { + ToolStripDropDown menu = this.MakeHeaderRightClickMenu(columnIndex); + ColumnRightClickEventArgs eventArgs = new ColumnRightClickEventArgs(columnIndex, menu, Cursor.Position); + this.OnColumnRightClick(eventArgs); + + // Did the event handler stop any further processing? + if (eventArgs.Cancel) + return false; + + return this.ShowHeaderRightClickMenu(columnIndex, eventArgs.MenuStrip, eventArgs.Location); + } + + /// + /// Show a menu that is appropriate when the given column header is clicked. + /// + /// The index of the header that was clicked. This + /// can be -1, indicating that the header was clicked outside of a column + /// Where should the menu be shown + /// True if a menu was displayed + protected virtual bool ShowHeaderRightClickMenu(int columnIndex, ToolStripDropDown menu, Point pt) { + if (menu.Items.Count > 0) { + menu.Show(pt); + return true; + } + + return false; + } + + /// + /// Create the menu that should be displayed when the user right clicks + /// on the given column header. + /// + /// Index of the column that was right clicked. + /// This can be negative, which indicates a click outside of any header. + /// The toolstrip that should be displayed + protected virtual ToolStripDropDown MakeHeaderRightClickMenu(int columnIndex) { + ToolStripDropDown m = new ContextMenuStrip(); + + if (columnIndex >= 0 && this.UseFiltering && this.ShowFilterMenuOnRightClick) + m = this.MakeFilteringMenu(m, columnIndex); + + if (columnIndex >= 0 && this.ShowCommandMenuOnRightClick) + m = this.MakeColumnCommandMenu(m, columnIndex); + + if (this.SelectColumnsOnRightClickBehaviour != ColumnSelectBehaviour.None) { + m = this.MakeColumnSelectMenu(m); + } + + return m; + } + + /// + /// The user has right clicked on the column headers. Do whatever is required + /// + /// Return true if this event has been handle + [Obsolete("Use HandleHeaderRightClick(int) instead")] + protected virtual bool HandleHeaderRightClick() { + return false; + } + + /// + /// Show a popup menu at the given point which will allow the user to choose which columns + /// are visible on this listview + /// + /// Where should the menu be placed + [Obsolete("Use ShowHeaderRightClickMenu instead")] + protected virtual void ShowColumnSelectMenu(Point pt) { + ToolStripDropDown m = this.MakeColumnSelectMenu(new ContextMenuStrip()); + m.Show(pt); + } + + /// + /// Show a popup menu at the given point which will allow the user to choose which columns + /// are visible on this listview + /// + /// + /// Where should the menu be placed + [Obsolete("Use ShowHeaderRightClickMenu instead")] + protected virtual void ShowColumnCommandMenu(int columnIndex, Point pt) { + ToolStripDropDown m = this.MakeColumnCommandMenu(new ContextMenuStrip(), columnIndex); + if (this.SelectColumnsOnRightClick) { + if (m.Items.Count > 0) + m.Items.Add(new ToolStripSeparator()); + this.MakeColumnSelectMenu(m); + } + m.Show(pt); + } + + /// + /// Gets or set the text to be used for the sorting ascending command + /// + [Category("Labels - ObjectListView"), DefaultValue("Sort ascending by '{0}'"), Localizable(true)] + public string MenuLabelSortAscending { + get { return this.menuLabelSortAscending; } + set { this.menuLabelSortAscending = value; } + } + private string menuLabelSortAscending = "Sort ascending by '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Sort descending by '{0}'"), Localizable(true)] + public string MenuLabelSortDescending { + get { return this.menuLabelSortDescending; } + set { this.menuLabelSortDescending = value; } + } + private string menuLabelSortDescending = "Sort descending by '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Group by '{0}'"), Localizable(true)] + public string MenuLabelGroupBy { + get { return this.menuLabelGroupBy; } + set { this.menuLabelGroupBy = value; } + } + private string menuLabelGroupBy = "Group by '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Lock grouping on '{0}'"), Localizable(true)] + public string MenuLabelLockGroupingOn { + get { return this.menuLabelLockGroupingOn; } + set { this.menuLabelLockGroupingOn = value; } + } + private string menuLabelLockGroupingOn = "Lock grouping on '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Unlock grouping from '{0}'"), Localizable(true)] + public string MenuLabelUnlockGroupingOn { + get { return this.menuLabelUnlockGroupingOn; } + set { this.menuLabelUnlockGroupingOn = value; } + } + private string menuLabelUnlockGroupingOn = "Unlock grouping from '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Turn off groups"), Localizable(true)] + public string MenuLabelTurnOffGroups { + get { return this.menuLabelTurnOffGroups; } + set { this.menuLabelTurnOffGroups = value; } + } + private string menuLabelTurnOffGroups = "Turn off groups"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Unsort"), Localizable(true)] + public string MenuLabelUnsort { + get { return this.menuLabelUnsort; } + set { this.menuLabelUnsort = value; } + } + private string menuLabelUnsort = "Unsort"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Columns"), Localizable(true)] + public string MenuLabelColumns { + get { return this.menuLabelColumns; } + set { this.menuLabelColumns = value; } + } + private string menuLabelColumns = "Columns"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Select Columns..."), Localizable(true)] + public string MenuLabelSelectColumns { + get { return this.menuLabelSelectColumns; } + set { this.menuLabelSelectColumns = value; } + } + private string menuLabelSelectColumns = "Select Columns..."; + + /// + /// Gets or sets the image that will be place next to the Sort Ascending command + /// + public static Bitmap SortAscendingImage = BrightIdeasSoftware.Properties.Resources.SortAscending; + + /// + /// Gets or sets the image that will be placed next to the Sort Descending command + /// + public static Bitmap SortDescendingImage = BrightIdeasSoftware.Properties.Resources.SortDescending; + + /// + /// Append the column selection menu items to the given menu strip. + /// + /// The menu to which the items will be added. + /// + /// Return the menu to which the items were added + public virtual ToolStripDropDown MakeColumnCommandMenu(ToolStripDropDown strip, int columnIndex) { + OLVColumn column = this.GetColumn(columnIndex); + if (column == null) + return strip; + + if (strip.Items.Count > 0) + strip.Items.Add(new ToolStripSeparator()); + + string label = String.Format(this.MenuLabelSortAscending, column.Text); + if (column.Sortable && !String.IsNullOrEmpty(label)) { + strip.Items.Add(label, ObjectListView.SortAscendingImage, (EventHandler)delegate(object sender, EventArgs args) { + this.Sort(column, SortOrder.Ascending); + }); + } + label = String.Format(this.MenuLabelSortDescending, column.Text); + if (column.Sortable && !String.IsNullOrEmpty(label)) { + strip.Items.Add(label, ObjectListView.SortDescendingImage, (EventHandler)delegate(object sender, EventArgs args) { + this.Sort(column, SortOrder.Descending); + }); + } + if (this.CanShowGroups) { + label = String.Format(this.MenuLabelGroupBy, column.Text); + if (column.Groupable && !String.IsNullOrEmpty(label)) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.ShowGroups = true; + this.PrimarySortColumn = column; + this.PrimarySortOrder = SortOrder.Ascending; + this.BuildList(); + }); + } + } + if (this.ShowGroups) { + if (this.AlwaysGroupByColumn == column) { + label = String.Format(this.MenuLabelUnlockGroupingOn, column.Text); + if (!String.IsNullOrEmpty(label)) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.AlwaysGroupByColumn = null; + this.AlwaysGroupBySortOrder = SortOrder.None; + this.BuildList(); + }); + } + } else { + label = String.Format(this.MenuLabelLockGroupingOn, column.Text); + if (column.Groupable && !String.IsNullOrEmpty(label)) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.ShowGroups = true; + this.AlwaysGroupByColumn = column; + this.AlwaysGroupBySortOrder = SortOrder.Ascending; + this.BuildList(); + }); + } + } + label = String.Format(this.MenuLabelTurnOffGroups, column.Text); + if (!String.IsNullOrEmpty(label)) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.ShowGroups = false; + this.BuildList(); + }); + } + } else { + label = String.Format(this.MenuLabelUnsort, column.Text); + if (column.Sortable && !String.IsNullOrEmpty(label) && this.PrimarySortOrder != SortOrder.None) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.Unsort(); + }); + } + } + + return strip; + } + + /// + /// Append the column selection menu items to the given menu strip. + /// + /// The menu to which the items will be added. + /// Return the menu to which the items were added + public virtual ToolStripDropDown MakeColumnSelectMenu(ToolStripDropDown strip) { + + System.Diagnostics.Debug.Assert(this.SelectColumnsOnRightClickBehaviour != ColumnSelectBehaviour.None); + + // Append a separator if the menu isn't empty and the last item isn't already a separator + if (strip.Items.Count > 0 && (!(strip.Items[strip.Items.Count-1] is ToolStripSeparator))) + strip.Items.Add(new ToolStripSeparator()); + + if (this.AllColumns.Count > 0 && this.AllColumns[0].LastDisplayIndex == -1) + this.RememberDisplayIndicies(); + + if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.ModelDialog) { + strip.Items.Add(this.MenuLabelSelectColumns, null, delegate(object sender, EventArgs args) { + (new ColumnSelectionForm()).OpenOn(this); + }); + } + + if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.Submenu) { + ToolStripMenuItem menu = new ToolStripMenuItem(this.MenuLabelColumns); + menu.DropDownItemClicked += new ToolStripItemClickedEventHandler(this.ColumnSelectMenuItemClicked); + strip.Items.Add(menu); + this.AddItemsToColumnSelectMenu(menu.DropDownItems); + } + + if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.InlineMenu) { + strip.ItemClicked += new ToolStripItemClickedEventHandler(this.ColumnSelectMenuItemClicked); + strip.Closing += new ToolStripDropDownClosingEventHandler(this.ColumnSelectMenuClosing); + this.AddItemsToColumnSelectMenu(strip.Items); + } + + return strip; + } + + /// + /// Create the menu items that will allow columns to be choosen and add them to the + /// given collection + /// + /// + protected void AddItemsToColumnSelectMenu(ToolStripItemCollection items) { + + // Sort columns by display order + List columns = new List(this.AllColumns); + columns.Sort(delegate(OLVColumn x, OLVColumn y) { return (x.LastDisplayIndex - y.LastDisplayIndex); }); + + // Build menu from sorted columns + foreach (OLVColumn col in columns) { + ToolStripMenuItem mi = new ToolStripMenuItem(col.Text); + mi.Checked = col.IsVisible; + mi.Tag = col; + + // The 'Index' property returns -1 when the column is not visible, so if the + // column isn't visible we have to enable the item. Also the first column can't be turned off + mi.Enabled = !col.IsVisible || col.CanBeHidden; + items.Add(mi); + } + } + + private void ColumnSelectMenuItemClicked(object sender, ToolStripItemClickedEventArgs e) { + this.contextMenuStaysOpen = false; + ToolStripMenuItem menuItemClicked = e.ClickedItem as ToolStripMenuItem; + if (menuItemClicked == null) + return; + OLVColumn col = menuItemClicked.Tag as OLVColumn; + if (col == null) + return; + menuItemClicked.Checked = !menuItemClicked.Checked; + col.IsVisible = menuItemClicked.Checked; + this.contextMenuStaysOpen = this.SelectColumnsMenuStaysOpen; + this.BeginInvoke(new MethodInvoker(this.RebuildColumns)); + } + private bool contextMenuStaysOpen; + + private void ColumnSelectMenuClosing(object sender, ToolStripDropDownClosingEventArgs e) { + e.Cancel = this.contextMenuStaysOpen && e.CloseReason == ToolStripDropDownCloseReason.ItemClicked; + this.contextMenuStaysOpen = false; + } + + /// + /// Create a Filtering menu + /// + /// + /// + /// + public virtual ToolStripDropDown MakeFilteringMenu(ToolStripDropDown strip, int columnIndex) { + OLVColumn column = this.GetColumn(columnIndex); + if (column == null) + return strip; + + FilterMenuBuilder strategy = this.FilterMenuBuildStrategy; + if (strategy == null) + return strip; + + return strategy.MakeFilterMenu(strip, this, column); + } + + /// + /// Override the OnColumnReordered method to do what we want + /// + /// + protected override void OnColumnReordered(ColumnReorderedEventArgs e) { + base.OnColumnReordered(e); + + // The internal logic of the .NET code behind a ENDDRAG event means that, + // at this point, the DisplayIndex's of the columns are not yet as they are + // going to be. So we have to invoke a method to run later that will remember + // what the real DisplayIndex's are. + this.BeginInvoke(new MethodInvoker(this.RememberDisplayIndicies)); + } + + private void RememberDisplayIndicies() { + // Remember the display indexes so we can put them back at a later date + foreach (OLVColumn x in this.AllColumns) + x.LastDisplayIndex = x.DisplayIndex; + } + + /// + /// When the column widths are changing, resize the space filling columns + /// + /// + /// + protected virtual void HandleColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e) { + if (this.UpdateSpaceFillingColumnsWhenDraggingColumnDivider && !this.GetColumn(e.ColumnIndex).FillsFreeSpace) { + // If the width of a column is increasing, resize any space filling columns allowing the extra + // space that the new column width is going to consume + int oldWidth = this.GetColumn(e.ColumnIndex).Width; + if (e.NewWidth > oldWidth) + this.ResizeFreeSpaceFillingColumns(this.ClientSize.Width - (e.NewWidth - oldWidth)); + else + this.ResizeFreeSpaceFillingColumns(); + } + } + + /// + /// When the column widths change, resize the space filling columns + /// + /// + /// + protected virtual void HandleColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e) { + if (!this.GetColumn(e.ColumnIndex).FillsFreeSpace) + this.ResizeFreeSpaceFillingColumns(); + } + + /// + /// When the size of the control changes, we have to resize our space filling columns. + /// + /// + /// + protected virtual void HandleLayout(object sender, LayoutEventArgs e) { + // We have to delay executing the recalculation of the columns, since virtual lists + // get terribly confused if we resize the column widths during this event. + if (!this.hasResizeColumnsHandler) { + this.hasResizeColumnsHandler = true; + this.RunWhenIdle(this.HandleApplicationIdleResizeColumns); + } + } + + private void RunWhenIdle(EventHandler eventHandler) { + Application.Idle += eventHandler; + if (!this.CanUseApplicationIdle) { + SynchronizationContext.Current.Post(delegate(object x) { Application.RaiseIdle(EventArgs.Empty); }, null); + } + } + + /// + /// Resize our space filling columns so they fill any unoccupied width in the control + /// + protected virtual void ResizeFreeSpaceFillingColumns() { + this.ResizeFreeSpaceFillingColumns(this.ClientSize.Width); + } + + /// + /// Resize our space filling columns so they fill any unoccupied width in the control + /// + protected virtual void ResizeFreeSpaceFillingColumns(int freeSpace) { + // It's too confusing to dynamically resize columns at design time. + if (this.DesignMode) + return; + + if (this.Frozen) + return; + + this.BeginUpdate(); + + // Calculate the free space available + int totalProportion = 0; + List spaceFillingColumns = new List(); + for (int i = 0; i < this.Columns.Count; i++) { + OLVColumn col = this.GetColumn(i); + if (col.FillsFreeSpace) { + spaceFillingColumns.Add(col); + totalProportion += col.FreeSpaceProportion; + } else + freeSpace -= col.Width; + } + freeSpace = Math.Max(0, freeSpace); + + // Any space filling column that would hit it's Minimum or Maximum + // width must be treated as a fixed column. + foreach (OLVColumn col in spaceFillingColumns.ToArray()) { + int newWidth = (freeSpace * col.FreeSpaceProportion) / totalProportion; + + if (col.MinimumWidth != -1 && newWidth < col.MinimumWidth) + newWidth = col.MinimumWidth; + else if (col.MaximumWidth != -1 && newWidth > col.MaximumWidth) + newWidth = col.MaximumWidth; + else + newWidth = 0; + + if (newWidth > 0) { + col.Width = newWidth; + freeSpace -= newWidth; + totalProportion -= col.FreeSpaceProportion; + spaceFillingColumns.Remove(col); + } + } + + // Distribute the free space between the columns + foreach (OLVColumn col in spaceFillingColumns) { + col.Width = (freeSpace*col.FreeSpaceProportion)/totalProportion; + } + + this.EndUpdate(); + } + + #endregion + + #region Checkboxes + + /// + /// Check all rows + /// + public virtual void CheckAll() + { + this.CheckedObjects = EnumerableToArray(this.Objects, false); + } + + /// + /// Check the checkbox in the given column header + /// + /// If the given columns header check box is linked to the cell check boxes, + /// then checkboxes in all cells will also be checked. + /// + public virtual void CheckHeaderCheckBox(OLVColumn column) + { + if (column == null) + return; + + ChangeHeaderCheckBoxState(column, CheckState.Checked); + } + + /// + /// Mark the checkbox in the given column header as having an indeterminate value + /// + /// + public virtual void CheckIndeterminateHeaderCheckBox(OLVColumn column) + { + if (column == null) + return; + + ChangeHeaderCheckBoxState(column, CheckState.Indeterminate); + } + + /// + /// Mark the given object as indeterminate check state + /// + /// The model object to be marked indeterminate + public virtual void CheckIndeterminateObject(object modelObject) { + this.SetObjectCheckedness(modelObject, CheckState.Indeterminate); + } + + /// + /// Mark the given object as checked in the list + /// + /// The model object to be checked + public virtual void CheckObject(object modelObject) { + this.SetObjectCheckedness(modelObject, CheckState.Checked); + } + + /// + /// Mark the given objects as checked in the list + /// + /// The model object to be checked + public virtual void CheckObjects(IEnumerable modelObjects) { + foreach (object model in modelObjects) + this.CheckObject(model); + } + + /// + /// Put a check into the check box at the given cell + /// + /// + /// + public virtual void CheckSubItem(object rowObject, OLVColumn column) { + if (column == null || rowObject == null || !column.CheckBoxes) + return; + + column.PutCheckState(rowObject, CheckState.Checked); + this.RefreshObject(rowObject); + } + + /// + /// Put an indeterminate check into the check box at the given cell + /// + /// + /// + public virtual void CheckIndeterminateSubItem(object rowObject, OLVColumn column) { + if (column == null || rowObject == null || !column.CheckBoxes) + return; + + column.PutCheckState(rowObject, CheckState.Indeterminate); + this.RefreshObject(rowObject); + } + + /// + /// Return true of the given object is checked + /// + /// The model object whose checkedness is returned + /// Is the given object checked? + /// If the given object is not in the list, this method returns false. + public virtual bool IsChecked(object modelObject) { + return this.GetCheckState(modelObject) == CheckState.Checked; + } + + /// + /// Return true of the given object is indeterminately checked + /// + /// The model object whose checkedness is returned + /// Is the given object indeterminately checked? + /// If the given object is not in the list, this method returns false. + public virtual bool IsCheckedIndeterminate(object modelObject) { + return this.GetCheckState(modelObject) == CheckState.Indeterminate; + } + + /// + /// Is there a check at the check box at the given cell + /// + /// + /// + public virtual bool IsSubItemChecked(object rowObject, OLVColumn column) { + if (column == null || rowObject == null || !column.CheckBoxes) + return false; + return (column.GetCheckState(rowObject) == CheckState.Checked); + } + + /// + /// Get the checkedness of an object from the model. Returning null means the + /// model does not know and the value from the control will be used. + /// + /// + /// + protected virtual CheckState? GetCheckState(Object modelObject) { + if (this.CheckStateGetter != null) + return this.CheckStateGetter(modelObject); + return this.PersistentCheckBoxes ? this.GetPersistentCheckState(modelObject) : (CheckState?)null; + } + + /// + /// Record the change of checkstate for the given object in the model. + /// This does not update the UI -- only the model + /// + /// + /// + /// The check state that was recorded and that should be used to update + /// the control. + protected virtual CheckState PutCheckState(Object modelObject, CheckState state) { + if (this.CheckStatePutter != null) + return this.CheckStatePutter(modelObject, state); + return this.PersistentCheckBoxes ? this.SetPersistentCheckState(modelObject, state) : state; + } + + /// + /// Change the check state of the given object to be the given state. + /// + /// + /// If the given model object isn't in the list, we still try to remember + /// its state, in case it is referenced in the future. + /// + /// + /// True if the checkedness of the model changed + protected virtual bool SetObjectCheckedness(object modelObject, CheckState state) { + + if (GetCheckState(modelObject) == state) + return false; + + OLVListItem olvi = this.ModelToItem(modelObject); + + // If we didn't find the given, we still try to record the check state. + if (olvi == null) { + this.PutCheckState(modelObject, state); + return true; + } + + // Trigger checkbox changing event + ItemCheckEventArgs ice = new ItemCheckEventArgs(olvi.Index, state, olvi.CheckState); + this.OnItemCheck(ice); + if (ice.NewValue == olvi.CheckState) + return false; + + olvi.CheckState = this.PutCheckState(modelObject, state); + this.RefreshItem(olvi); + + // Trigger check changed event + this.OnItemChecked(new ItemCheckedEventArgs(olvi)); + return true; + } + + /// + /// Toggle the checkedness of the given object. A checked object becomes + /// unchecked; an unchecked or indeterminate object becomes checked. + /// If the list has tristate checkboxes, the order is: + /// unchecked -> checked -> indeterminate -> unchecked ... + /// + /// The model object to be checked + public virtual void ToggleCheckObject(object modelObject) { + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi == null) + return; + + CheckState newState = CheckState.Checked; + + if (olvi.CheckState == CheckState.Checked) { + newState = this.TriStateCheckBoxes ? CheckState.Indeterminate : CheckState.Unchecked; + } else { + if (olvi.CheckState == CheckState.Indeterminate && this.TriStateCheckBoxes) + newState = CheckState.Unchecked; + } + this.SetObjectCheckedness(modelObject, newState); + } + + /// + /// Toggle the checkbox in the header of the given column + /// + /// Obviously, this is only useful if the column actually has a header checkbox. + /// + public virtual void ToggleHeaderCheckBox(OLVColumn column) { + if (column == null) + return; + + CheckState newState = CalculateToggledCheckState(column.HeaderCheckState, column.HeaderTriStateCheckBox, column.HeaderCheckBoxDisabled); + ChangeHeaderCheckBoxState(column, newState); + } + + private void ChangeHeaderCheckBoxState(OLVColumn column, CheckState newState) { + // Tell the world the checkbox was clicked + HeaderCheckBoxChangingEventArgs args = new HeaderCheckBoxChangingEventArgs(); + args.Column = column; + args.NewCheckState = newState; + + this.OnHeaderCheckBoxChanging(args); + if (args.Cancel || column.HeaderCheckState == args.NewCheckState) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + column.HeaderCheckState = args.NewCheckState; + this.HeaderControl.Invalidate(column); + + if (column.HeaderCheckBoxUpdatesRowCheckBoxes) { + if (column.Index == 0) + this.UpdateAllPrimaryCheckBoxes(column); + else + this.UpdateAllSubItemCheckBoxes(column); + } + + // Debug.WriteLine(String.Format("PERF - Changing row checkboxes on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); + } + + private void UpdateAllPrimaryCheckBoxes(OLVColumn column) { + if (!this.CheckBoxes || column.HeaderCheckState == CheckState.Indeterminate) + return; + + if (column.HeaderCheckState == CheckState.Checked) + CheckAll(); + else + UncheckAll(); + } + + private void UpdateAllSubItemCheckBoxes(OLVColumn column) { + if (!column.CheckBoxes || column.HeaderCheckState == CheckState.Indeterminate) + return; + + foreach (object model in this.Objects) + column.PutCheckState(model, column.HeaderCheckState); + this.BuildList(true); + } + + /// + /// Toggle the check at the check box of the given cell + /// + /// + /// + public virtual void ToggleSubItemCheckBox(object rowObject, OLVColumn column) { + CheckState currentState = column.GetCheckState(rowObject); + CheckState newState = CalculateToggledCheckState(currentState, column.TriStateCheckBoxes, false); + + SubItemCheckingEventArgs args = new SubItemCheckingEventArgs(column, this.ModelToItem(rowObject), column.Index, currentState, newState); + this.OnSubItemChecking(args); + if (args.Canceled) + return; + + switch (args.NewValue) { + case CheckState.Checked: + this.CheckSubItem(rowObject, column); + break; + case CheckState.Indeterminate: + this.CheckIndeterminateSubItem(rowObject, column); + break; + case CheckState.Unchecked: + this.UncheckSubItem(rowObject, column); + break; + } + } + + /// + /// Uncheck all rows + /// + public virtual void UncheckAll() + { + this.CheckedObjects = null; + } + + /// + /// Mark the given object as unchecked in the list + /// + /// The model object to be unchecked + public virtual void UncheckObject(object modelObject) { + this.SetObjectCheckedness(modelObject, CheckState.Unchecked); + } + + /// + /// Mark the given objects as unchecked in the list + /// + /// The model object to be checked + public virtual void UncheckObjects(IEnumerable modelObjects) { + foreach (object model in modelObjects) + this.UncheckObject(model); + } + + /// + /// Uncheck the checkbox in the given column header + /// + /// + public virtual void UncheckHeaderCheckBox(OLVColumn column) + { + if (column == null) + return; + + ChangeHeaderCheckBoxState(column, CheckState.Unchecked); + } + + /// + /// Uncheck the check at the given cell + /// + /// + /// + public virtual void UncheckSubItem(object rowObject, OLVColumn column) + { + if (column == null || rowObject == null || !column.CheckBoxes) + return; + + column.PutCheckState(rowObject, CheckState.Unchecked); + this.RefreshObject(rowObject); + } + + #endregion + + #region OLV accessing + + /// + /// Return the column at the given index + /// + /// Index of the column to be returned + /// An OLVColumn, or null if the index is out of bounds + public virtual OLVColumn GetColumn(int index) { + return (index >=0 && index < this.Columns.Count) ? (OLVColumn)this.Columns[index] : null; + } + + /// + /// Return the column at the given title. + /// + /// Name of the column to be returned + /// An OLVColumn + public virtual OLVColumn GetColumn(string name) { + foreach (ColumnHeader column in this.Columns) { + if (column.Text == name) + return (OLVColumn)column; + } + return null; + } + + /// + /// Return a collection of columns that are visible to the given view. + /// Only Tile and Details have columns; all other views have 0 columns. + /// + /// Which view are the columns being calculate for? + /// A list of columns + public virtual List GetFilteredColumns(View view) { + // For both detail and tile view, the first column must be included. Normally, we would + // use the ColumnHeader.Index property, but if the header is not currently part of a ListView + // that property returns -1. So, we track the index of + // the column header, and always include the first header. + + int index = 0; + return this.AllColumns.FindAll(delegate(OLVColumn x) { + return (index++ == 0) || x.IsVisible; + }); + } + + /// + /// Return the number of items in the list + /// + /// the number of items in the list + /// If a filter is installed, this will return the number of items that match the filter. + public virtual int GetItemCount() { + return this.Items.Count; + } + + /// + /// Return the item at the given index + /// + /// Index of the item to be returned + /// An OLVListItem + public virtual OLVListItem GetItem(int index) { + if (index < 0 || index >= this.GetItemCount()) + return null; + + return (OLVListItem)this.Items[index]; + } + + /// + /// Return the model object at the given index + /// + /// Index of the model object to be returned + /// A model object + public virtual object GetModelObject(int index) { + OLVListItem item = this.GetItem(index); + return item == null ? null : item.RowObject; + } + + /// + /// Find the item and column that are under the given co-ords + /// + /// X co-ord + /// Y co-ord + /// The column under the given point + /// The item under the given point. Can be null. + public virtual OLVListItem GetItemAt(int x, int y, out OLVColumn hitColumn) { + hitColumn = null; + ListViewHitTestInfo info = this.HitTest(x, y); + if (info.Item == null) + return null; + + if (info.SubItem != null) { + int subItemIndex = info.Item.SubItems.IndexOf(info.SubItem); + hitColumn = this.GetColumn(subItemIndex); + } + + return (OLVListItem)info.Item; + } + + /// + /// Return the sub item at the given index/column + /// + /// Index of the item to be returned + /// Index of the subitem to be returned + /// An OLVListSubItem + public virtual OLVListSubItem GetSubItem(int index, int columnIndex) { + OLVListItem olvi = this.GetItem(index); + return olvi == null ? null : olvi.GetSubItem(columnIndex); + } + + #endregion + + #region Object manipulation + + /// + /// Scroll the listview so that the given group is at the top. + /// + /// The group to be revealed + /// + /// If the group is already visible, the list will still be scrolled to move + /// the group to the top, if that is possible. + /// + /// This only works when the list is showing groups (obviously). + /// This does not work on virtual lists, since virtual lists don't use ListViewGroups + /// for grouping. Use instead. + /// + public virtual void EnsureGroupVisible(ListViewGroup lvg) { + if (!this.ShowGroups || lvg == null) + return; + + int groupIndex = this.Groups.IndexOf(lvg); + if (groupIndex <= 0) { + // There is no easy way to scroll back to the beginning of the list + int delta = 0 - NativeMethods.GetScrollPosition(this, false); + NativeMethods.Scroll(this, 0, delta); + } else { + // Find the display rectangle of the last item in the previous group + ListViewGroup previousGroup = this.Groups[groupIndex - 1]; + ListViewItem lastItemInGroup = previousGroup.Items[previousGroup.Items.Count - 1]; + Rectangle r = this.GetItemRect(lastItemInGroup.Index); + + // Scroll so that the last item of the previous group is just out of sight, + // which will make the desired group header visible. + int delta = r.Y + r.Height / 2; + NativeMethods.Scroll(this, 0, delta); + } + } + + /// + /// Ensure that the given model object is visible + /// + /// The model object to be revealed + public virtual void EnsureModelVisible(Object modelObject) { + int index = this.IndexOf(modelObject); + if (index >= 0) + this.EnsureVisible(index); + } + + /// + /// Return the model object of the row that is selected or null if there is no selection or more than one selection + /// + /// Model object or null + [Obsolete("Use SelectedObject property instead of this method")] + public virtual object GetSelectedObject() { + return this.SelectedObject; + } + + /// + /// Return the model objects of the rows that are selected or an empty collection if there is no selection + /// + /// ArrayList + [Obsolete("Use SelectedObjects property instead of this method")] + public virtual ArrayList GetSelectedObjects() { + return ObjectListView.EnumerableToArray(this.SelectedObjects, false); + } + + /// + /// Return the model object of the row that is checked or null if no row is checked + /// or more than one row is checked + /// + /// Model object or null + /// Use CheckedObject property instead of this method + [Obsolete("Use CheckedObject property instead of this method")] + public virtual object GetCheckedObject() { + return this.CheckedObject; + } + + /// + /// Get the collection of model objects that are checked. + /// + /// Use CheckedObjects property instead of this method + [Obsolete("Use CheckedObjects property instead of this method")] + public virtual ArrayList GetCheckedObjects() { + return ObjectListView.EnumerableToArray(this.CheckedObjects, false); + } + + /// + /// Find the given model object within the listview and return its index + /// + /// The model object to be found + /// The index of the object. -1 means the object was not present + public virtual int IndexOf(Object modelObject) { + for (int i = 0; i < this.GetItemCount(); i++) { + if (this.GetModelObject(i).Equals(modelObject)) + return i; + } + return -1; + } + + /// + /// Rebuild the given ListViewItem with the data from its associated model. + /// + /// This method does not resort or regroup the view. It simply updates + /// the displayed data of the given item + public virtual void RefreshItem(OLVListItem olvi) { + olvi.UseItemStyleForSubItems = true; + olvi.SubItems.Clear(); + this.FillInValues(olvi, olvi.RowObject); + this.PostProcessOneRow(olvi.Index, this.GetDisplayOrderOfItemIndex(olvi.Index), olvi); + } + + /// + /// Rebuild the data on the row that is showing the given object. + /// + /// + /// + /// This method does not resort or regroup the view. + /// + /// + /// The given object is *not* used as the source of data for the rebuild. + /// It is only used to locate the matching model in the collection, + /// then that matching model is used as the data source. This distinction is + /// only important in model classes that have overridden the Equals() method. + /// + /// + /// If you want the given model object to replace the pre-existing model, + /// use . + /// + /// + public virtual void RefreshObject(object modelObject) { + this.RefreshObjects(new object[] { modelObject }); + } + + /// + /// Update the rows that are showing the given objects + /// + /// + /// This method does not resort or regroup the view. + /// This method can safely be called from background threads. + /// + public virtual void RefreshObjects(IList modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.RefreshObjects(modelObjects); }); + return; + } + foreach (object modelObject in modelObjects) { + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi != null) { + this.ReplaceModel(olvi, modelObject); + this.RefreshItem(olvi); + } + } + } + + private void ReplaceModel(OLVListItem olvi, object newModel) { + if (ReferenceEquals(olvi.RowObject, newModel)) + return; + + this.TakeOwnershipOfObjects(); + ArrayList array = ObjectListView.EnumerableToArray(this.Objects, false); + int i = array.IndexOf(olvi.RowObject); + if (i >= 0) + array[i] = newModel; + + olvi.RowObject = newModel; + } + + /// + /// Update the rows that are selected + /// + /// This method does not resort or regroup the view. + public virtual void RefreshSelectedObjects() { + foreach (ListViewItem lvi in this.SelectedItems) + this.RefreshItem((OLVListItem)lvi); + } + + /// + /// Select the row that is displaying the given model object, in addition to any current selection. + /// + /// The object to be selected + /// Use the property to deselect all other rows + public virtual void SelectObject(object modelObject) { + this.SelectObject(modelObject, false); + } + + /// + /// Select the row that is displaying the given model object, in addition to any current selection. + /// + /// The object to be selected + /// Should the object be focused as well? + /// Use the property to deselect all other rows + public virtual void SelectObject(object modelObject, bool setFocus) { + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi != null && olvi.Enabled) { + olvi.Selected = true; + if (setFocus) + olvi.Focused = true; + } + } + + /// + /// Select the rows that is displaying any of the given model object. All other rows are deselected. + /// + /// A collection of model objects + public virtual void SelectObjects(IList modelObjects) { + this.SelectedIndices.Clear(); + + if (modelObjects == null) + return; + + foreach (object modelObject in modelObjects) { + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi != null && olvi.Enabled) + olvi.Selected = true; + } + } + + #endregion + + #region Freezing/Suspending + + /// + /// Get or set whether or not the listview is frozen. When the listview is + /// frozen, it will not update itself. + /// + /// The Frozen property is similar to the methods Freeze()/Unfreeze() + /// except that setting Frozen property to false immediately unfreezes the control + /// regardless of the number of Freeze() calls outstanding. + /// objectListView1.Frozen = false; // unfreeze the control now! + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual bool Frozen { + get { return freezeCount > 0; } + set { + if (value) + Freeze(); + else if (freezeCount > 0) { + freezeCount = 1; + Unfreeze(); + } + } + } + private int freezeCount; + + /// + /// Freeze the listview so that it no longer updates itself. + /// + /// Freeze()/Unfreeze() calls nest correctly + public virtual void Freeze() { + if (freezeCount == 0) + DoFreeze(); + + freezeCount++; + this.OnFreezing(new FreezeEventArgs(freezeCount)); + } + + /// + /// Unfreeze the listview. If this call is the outermost Unfreeze(), + /// the contents of the listview will be rebuilt. + /// + /// Freeze()/Unfreeze() calls nest correctly + public virtual void Unfreeze() { + if (freezeCount <= 0) + return; + + freezeCount--; + if (freezeCount == 0) + DoUnfreeze(); + + this.OnFreezing(new FreezeEventArgs(freezeCount)); + } + + /// + /// Do the actual work required when the listview is frozen + /// + protected virtual void DoFreeze() { + this.BeginUpdate(); + } + + /// + /// Do the actual work required when the listview is unfrozen + /// + protected virtual void DoUnfreeze() + { + this.EndUpdate(); + this.ResizeFreeSpaceFillingColumns(); + this.BuildList(); + } + + /// + /// Returns true if selection events are currently suspended. + /// While selection events are suspended, neither SelectedIndexChanged + /// or SelectionChanged events will be raised. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + protected bool SelectionEventsSuspended { + get { return this.suspendSelectionEventCount > 0; } + } + + /// + /// Suspend selection events until a matching ResumeSelectionEvents() + /// is called. + /// + /// Calls to this method nest correctly. Every call to SuspendSelectionEvents() + /// must have a matching ResumeSelectionEvents(). + protected void SuspendSelectionEvents() { + this.suspendSelectionEventCount++; + } + + /// + /// Resume raising selection events. + /// + protected void ResumeSelectionEvents() { + Debug.Assert(this.SelectionEventsSuspended, "Mismatched called to ResumeSelectionEvents()"); + this.suspendSelectionEventCount--; + } + + /// + /// Returns a disposable that will disable selection events + /// during a using() block. + /// + /// + protected IDisposable SuspendSelectionEventsDuring() { + return new SuspendSelectionDisposable(this); + } + + /// + /// Implementation only class that suspends and resumes selection + /// events on instance creation and disposal. + /// + private class SuspendSelectionDisposable : IDisposable { + public SuspendSelectionDisposable(ObjectListView objectListView) { + this.objectListView = objectListView; + this.objectListView.SuspendSelectionEvents(); + } + + public void Dispose() { + this.objectListView.ResumeSelectionEvents(); + } + + private readonly ObjectListView objectListView; + } + + #endregion + + #region Column sorting + + /// + /// Sort the items by the last sort column and order + /// + public new void Sort() { + this.Sort(this.PrimarySortColumn, this.PrimarySortOrder); + } + + /// + /// Sort the items in the list view by the values in the given column and the last sort order + /// + /// The name of the column whose values will be used for the sorting + public virtual void Sort(string columnToSortName) { + this.Sort(this.GetColumn(columnToSortName), this.PrimarySortOrder); + } + + /// + /// Sort the items in the list view by the values in the given column and the last sort order + /// + /// The index of the column whose values will be used for the sorting + public virtual void Sort(int columnToSortIndex) { + if (columnToSortIndex >= 0 && columnToSortIndex < this.Columns.Count) + this.Sort(this.GetColumn(columnToSortIndex), this.PrimarySortOrder); + } + + /// + /// Sort the items in the list view by the values in the given column and the last sort order + /// + /// The column whose values will be used for the sorting + public virtual void Sort(OLVColumn columnToSort) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.Sort(columnToSort); }); + } else { + this.Sort(columnToSort, this.PrimarySortOrder); + } + } + + /// + /// Sort the items in the list view by the values in the given column and by the given order. + /// + /// The column whose values will be used for the sorting. + /// If null, the first column will be used. + /// The ordering to be used for sorting. If this is None, + /// this.Sorting and then SortOrder.Ascending will be used + /// If ShowGroups is true, the rows will be grouped by the given column. + /// If AlwaysGroupsByColumn is not null, the rows will be grouped by that column, + /// and the rows within each group will be sorted by the given column. + public virtual void Sort(OLVColumn columnToSort, SortOrder order) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.Sort(columnToSort, order); }); + } else { + this.DoSort(columnToSort, order); + this.PostProcessRows(); + } + } + + private void DoSort(OLVColumn columnToSort, SortOrder order) { + // Sanity checks + if (this.GetItemCount() == 0 || this.Columns.Count == 0) + return; + + // Fill in default values, if the parameters don't make sense + if (this.ShowGroups) { + columnToSort = columnToSort ?? this.GetColumn(0); + if (order == SortOrder.None) { + order = this.Sorting; + if (order == SortOrder.None) + order = SortOrder.Ascending; + } + } + + // Give the world a chance to fiddle with or completely avoid the sorting process + BeforeSortingEventArgs args = this.BuildBeforeSortingEventArgs(columnToSort, order); + this.OnBeforeSorting(args); + if (args.Canceled) + return; + + // Virtual lists don't preserve selection, so we have to do it specifically + // THINK: Do we need to preserve focus too? + IList selection = this.VirtualMode ? this.SelectedObjects : null; + this.SuspendSelectionEvents(); + + this.ClearHotItem(); + + // Finally, do the work of sorting, unless an event handler has already done the sorting for us + if (!args.Handled) { + // Sanity checks + if (args.ColumnToSort != null && args.SortOrder != SortOrder.None) { + if (this.ShowGroups) + this.BuildGroups(args.ColumnToGroupBy, args.GroupByOrder, args.ColumnToSort, args.SortOrder, + args.SecondaryColumnToSort, args.SecondarySortOrder); + else if (this.CustomSorter != null) + this.CustomSorter(args.ColumnToSort, args.SortOrder); + else + this.ListViewItemSorter = new ColumnComparer(args.ColumnToSort, args.SortOrder, + args.SecondaryColumnToSort, args.SecondarySortOrder); + } + } + + if (this.ShowSortIndicators) + this.ShowSortIndicator(args.ColumnToSort, args.SortOrder); + + this.PrimarySortColumn = args.ColumnToSort; + this.PrimarySortOrder = args.SortOrder; + + if (selection != null && selection.Count > 0) + this.SelectedObjects = selection; + this.ResumeSelectionEvents(); + + this.RefreshHotItem(); + + this.OnAfterSorting(new AfterSortingEventArgs(args)); + } + + /// + /// Put a sort indicator next to the text of the sort column + /// + public virtual void ShowSortIndicator() { + if (this.ShowSortIndicators && this.PrimarySortOrder != SortOrder.None) + this.ShowSortIndicator(this.PrimarySortColumn, this.PrimarySortOrder); + } + + /// + /// Put a sort indicator next to the text of the given column + /// + /// The column to be marked + /// The sort order in effect on that column + protected virtual void ShowSortIndicator(OLVColumn columnToSort, SortOrder sortOrder) { + int imageIndex = -1; + + if (!NativeMethods.HasBuiltinSortIndicators()) { + // If we can't use builtin image, we have to make and then locate the index of the + // sort indicator we want to use. SortOrder.None doesn't show an image. + if (this.SmallImageList == null || !this.SmallImageList.Images.ContainsKey(SORT_INDICATOR_UP_KEY)) + MakeSortIndicatorImages(); + + if (this.SmallImageList != null) + { + string key = sortOrder == SortOrder.Ascending ? SORT_INDICATOR_UP_KEY : SORT_INDICATOR_DOWN_KEY; + imageIndex = this.SmallImageList.Images.IndexOfKey(key); + } + } + + // Set the image for each column + for (int i = 0; i < this.Columns.Count; i++) { + if (columnToSort != null && i == columnToSort.Index) + NativeMethods.SetColumnImage(this, i, sortOrder, imageIndex); + else + NativeMethods.SetColumnImage(this, i, SortOrder.None, -1); + } + } + + /// + /// The name of the image used when a column is sorted ascending + /// + /// This image is only used on pre-XP systems. System images are used for XP and later + public const string SORT_INDICATOR_UP_KEY = "sort-indicator-up"; + + /// + /// The name of the image used when a column is sorted descending + /// + /// This image is only used on pre-XP systems. System images are used for XP and later + public const string SORT_INDICATOR_DOWN_KEY = "sort-indicator-down"; + + /// + /// If the sort indicator images don't already exist, this method will make and install them + /// + protected virtual void MakeSortIndicatorImages() { + // Don't mess with the image list in design mode + if (this.DesignMode) + return; + + ImageList il = this.SmallImageList; + if (il == null) { + il = new ImageList(); + il.ImageSize = new Size(16, 16); + il.ColorDepth = ColorDepth.Depth32Bit; + } + + // This arrangement of points works well with (16,16) images, and OK with others + int midX = il.ImageSize.Width / 2; + int midY = (il.ImageSize.Height / 2) - 1; + int deltaX = midX - 2; + int deltaY = deltaX / 2; + + if (il.Images.IndexOfKey(SORT_INDICATOR_UP_KEY) == -1) { + Point pt1 = new Point(midX - deltaX, midY + deltaY); + Point pt2 = new Point(midX, midY - deltaY - 1); + Point pt3 = new Point(midX + deltaX, midY + deltaY); + il.Images.Add(SORT_INDICATOR_UP_KEY, this.MakeTriangleBitmap(il.ImageSize, new Point[] { pt1, pt2, pt3 })); + } + + if (il.Images.IndexOfKey(SORT_INDICATOR_DOWN_KEY) == -1) { + Point pt1 = new Point(midX - deltaX, midY - deltaY); + Point pt2 = new Point(midX, midY + deltaY); + Point pt3 = new Point(midX + deltaX, midY - deltaY); + il.Images.Add(SORT_INDICATOR_DOWN_KEY, this.MakeTriangleBitmap(il.ImageSize, new Point[] { pt1, pt2, pt3 })); + } + + this.SmallImageList = il; + } + + private Bitmap MakeTriangleBitmap(Size sz, Point[] pts) { + Bitmap bm = new Bitmap(sz.Width, sz.Height); + Graphics g = Graphics.FromImage(bm); + g.FillPolygon(new SolidBrush(Color.Gray), pts); + return bm; + } + + /// + /// Remove any sorting and revert to the given order of the model objects + /// + public virtual void Unsort() { + this.ShowGroups = false; + this.PrimarySortColumn = null; + this.PrimarySortOrder = SortOrder.None; + this.BuildList(); + } + + #endregion + + #region Utilities + + private static CheckState CalculateToggledCheckState(CheckState currentState, bool isTriState, bool isDisabled) + { + if (isDisabled) + return currentState; + switch (currentState) + { + case CheckState.Checked: return isTriState ? CheckState.Indeterminate : CheckState.Unchecked; + case CheckState.Indeterminate: return CheckState.Unchecked; + default: return CheckState.Checked; + } + } + + /// + /// Do the actual work of creating the given list of groups + /// + /// + protected virtual void CreateGroups(IEnumerable groups) { + this.Groups.Clear(); + // The group must be added before it is given items, otherwise an exception is thrown (is this documented?) + foreach (OLVGroup group in groups) { + group.InsertGroupOldStyle(this); + group.SetItemsOldStyle(); + } + } + + /// + /// For some reason, UseItemStyleForSubItems doesn't work for the colors + /// when owner drawing the list, so we have to specifically give each subitem + /// the desired colors + /// + /// The item whose subitems are to be corrected + /// Cells drawn via BaseRenderer don't need this, but it is needed + /// when an owner drawn cell uses DrawDefault=true + protected virtual void CorrectSubItemColors(ListViewItem olvi) { + } + + /// + /// Fill in the given OLVListItem with values of the given row + /// + /// the OLVListItem that is to be stuff with values + /// the model object from which values will be taken + protected virtual void FillInValues(OLVListItem lvi, object rowObject) { + if (this.Columns.Count == 0) + return; + + OLVListSubItem subItem = this.MakeSubItem(rowObject, this.GetColumn(0)); + lvi.SubItems[0] = subItem; + lvi.ImageSelector = subItem.ImageSelector; + + // Give the item the same font/colors as the control + lvi.Font = this.Font; + lvi.BackColor = this.BackColor; + lvi.ForeColor = this.ForeColor; + + // Should the row be selectable? + lvi.Enabled = !this.IsDisabled(rowObject); + + // Only Details and Tile views have subitems + switch (this.View) { + case View.Details: + for (int i = 1; i < this.Columns.Count; i++) { + lvi.SubItems.Add(this.MakeSubItem(rowObject, this.GetColumn(i))); + } + break; + case View.Tile: + for (int i = 1; i < this.Columns.Count; i++) { + OLVColumn column = this.GetColumn(i); + if (column.IsTileViewColumn) + lvi.SubItems.Add(this.MakeSubItem(rowObject, column)); + } + break; + } + + // Should the row be selectable? + if (!lvi.Enabled) { + lvi.UseItemStyleForSubItems = false; + ApplyRowStyle(lvi, this.DisabledItemStyle ?? ObjectListView.DefaultDisabledItemStyle); + } + + // Set the check state of the row, if we are showing check boxes + if (this.CheckBoxes) { + CheckState? state = this.GetCheckState(lvi.RowObject); + if (state.HasValue) + lvi.CheckState = state.Value; + } + + // Give the RowFormatter a chance to mess with the item + if (this.RowFormatter != null) { + this.RowFormatter(lvi); + } + } + + private OLVListSubItem MakeSubItem(object rowObject, OLVColumn column) { + object cellValue = column.GetValue(rowObject); + OLVListSubItem subItem = new OLVListSubItem(cellValue, + column.ValueToString(cellValue), + column.GetImage(rowObject)); + if (this.UseHyperlinks && column.Hyperlink) { + IsHyperlinkEventArgs args = new IsHyperlinkEventArgs(); + args.ListView = this; + args.Model = rowObject; + args.Column = column; + args.Text = subItem.Text; + args.Url = subItem.Text; + args.IsHyperlink = !this.IsDisabled(rowObject); + this.OnIsHyperlink(args); + subItem.Url = args.IsHyperlink ? args.Url : null; + } + + return subItem; + } + + private void ApplyHyperlinkStyle(OLVListItem olvi) { + + for (int i = 0; i < this.Columns.Count; i++) { + OLVListSubItem subItem = olvi.GetSubItem(i); + if (subItem == null) + continue; + OLVColumn column = this.GetColumn(i); + if (column.Hyperlink && !String.IsNullOrEmpty(subItem.Url)) + this.ApplyCellStyle(olvi, i, this.IsUrlVisited(subItem.Url) ? this.HyperlinkStyle.Visited : this.HyperlinkStyle.Normal); + } + } + + + /// + /// Make sure the ListView has the extended style that says to display subitem images. + /// + /// This method must be called after any .NET call that update the extended styles + /// since they seem to erase this setting. + protected virtual void ForceSubItemImagesExStyle() { + // Virtual lists can't show subitem images natively, so don't turn on this flag + if (!this.VirtualMode) + NativeMethods.ForceSubItemImagesExStyle(this); + } + + /// + /// Convert the given image selector to an index into our image list. + /// Return -1 if that's not possible + /// + /// + /// Index of the image in the imageList, or -1 + protected virtual int GetActualImageIndex(Object imageSelector) { + if (imageSelector == null) + return -1; + + if (imageSelector is Int32) + return (int)imageSelector; + + String imageSelectorAsString = imageSelector as String; + if (imageSelectorAsString != null && this.SmallImageList != null) + return this.SmallImageList.Images.IndexOfKey(imageSelectorAsString); + + return -1; + } + + /// + /// Return the tooltip that should be shown when the mouse is hovered over the given column + /// + /// The column index whose tool tip is to be fetched + /// A string or null if no tool tip is to be shown + public virtual String GetHeaderToolTip(int columnIndex) { + OLVColumn column = this.GetColumn(columnIndex); + if (column == null) + return null; + String tooltip = column.ToolTipText; + if (this.HeaderToolTipGetter != null) + tooltip = this.HeaderToolTipGetter(column); + return tooltip; + } + + /// + /// Return the tooltip that should be shown when the mouse is hovered over the given cell + /// + /// The column index whose tool tip is to be fetched + /// The row index whose tool tip is to be fetched + /// A string or null if no tool tip is to be shown + public virtual String GetCellToolTip(int columnIndex, int rowIndex) { + if (this.CellToolTipGetter != null) + return this.CellToolTipGetter(this.GetColumn(columnIndex), this.GetModelObject(rowIndex)); + + // Show the URL in the tooltip if it's different to the text + if (columnIndex >= 0) { + OLVListSubItem subItem = this.GetSubItem(rowIndex, columnIndex); + if (subItem != null && !String.IsNullOrEmpty(subItem.Url) && subItem.Url != subItem.Text && + this.HotCellHitLocation == HitTestLocation.Text) + return subItem.Url; + } + + return null; + } + + /// + /// Return the OLVListItem that displays the given model object + /// + /// The modelObject whose item is to be found + /// The OLVListItem that displays the model, or null + /// This method has O(n) performance. + public virtual OLVListItem ModelToItem(object modelObject) { + if (modelObject == null) + return null; + + foreach (OLVListItem olvi in this.Items) { + if (olvi.RowObject != null && olvi.RowObject.Equals(modelObject)) + return olvi; + } + return null; + } + + /// + /// Do the work required after the items in a listview have been created + /// + protected virtual void PostProcessRows() { + // If this method is called during a BeginUpdate/EndUpdate pair, changes to the + // Items collection are cached. Getting the Count flushes that cache. +#pragma warning disable 168 +// ReSharper disable once UnusedVariable + int count = this.Items.Count; +#pragma warning restore 168 + + int i = 0; + if (this.ShowGroups) { + foreach (ListViewGroup group in this.Groups) { + foreach (OLVListItem olvi in group.Items) { + this.PostProcessOneRow(olvi.Index, i, olvi); + i++; + } + } + } else { + foreach (OLVListItem olvi in this.Items) { + this.PostProcessOneRow(olvi.Index, i, olvi); + i++; + } + } + } + + /// + /// Do the work required after one item in a listview have been created + /// + protected virtual void PostProcessOneRow(int rowIndex, int displayIndex, OLVListItem olvi) { + if (this.Columns.Count == 0) + return; + if (this.UseAlternatingBackColors && this.View == View.Details && olvi.Enabled) { + olvi.UseItemStyleForSubItems = true; + olvi.BackColor = displayIndex % 2 == 1 ? this.AlternateRowBackColorOrDefault : this.BackColor; + } + if (this.ShowImagesOnSubItems && !this.VirtualMode) + this.SetSubItemImages(rowIndex, olvi); + + bool needToTriggerFormatCellEvents = this.TriggerFormatRowEvent(rowIndex, displayIndex, olvi); + + // We only need cell level events if we are in details view + if (this.View != View.Details) + return; + + // If we're going to have per cell formatting, we need to copy the formatting + // of the item into each cell, before triggering the cell format events + if (needToTriggerFormatCellEvents) { + PropagateFormatFromRowToCells(olvi); + this.TriggerFormatCellEvents(rowIndex, displayIndex, olvi); + } + + // Similarly, if any cell in the row has hyperlinks, we have to copy formatting + // from the item into each cell before applying the hyperlink style + if (this.UseHyperlinks && olvi.HasAnyHyperlinks) { + PropagateFormatFromRowToCells(olvi); + this.ApplyHyperlinkStyle(olvi); + } + } + + /// + /// Prepare the listview to show alternate row backcolors + /// + /// We cannot rely on lvi.Index in this method. + /// In a straight list, lvi.Index is the display index, and can be used to determine + /// whether the row should be colored. But when organised by groups, lvi.Index is not + /// usable because it still refers to the position in the overall list, not the display order. + /// + [Obsolete("This method is no longer used. Override PostProcessOneRow() to achieve a similar result")] + protected virtual void PrepareAlternateBackColors() { + } + + /// + /// Setup all subitem images on all rows + /// + [Obsolete("This method is not longer maintained and will be removed", false)] + protected virtual void SetAllSubItemImages() { + //if (!this.ShowImagesOnSubItems || this.OwnerDraw) + // return; + + //this.ForceSubItemImagesExStyle(); + + //for (int rowIndex = 0; rowIndex < this.GetItemCount(); rowIndex++) + // SetSubItemImages(rowIndex, this.GetItem(rowIndex)); + } + + /// + /// Tell the underlying list control which images to show against the subitems + /// + /// the index at which the item occurs + /// the item whose subitems are to be set + protected virtual void SetSubItemImages(int rowIndex, OLVListItem item) { + this.SetSubItemImages(rowIndex, item, false); + } + + /// + /// Tell the underlying list control which images to show against the subitems + /// + /// the index at which the item occurs + /// the item whose subitems are to be set + /// will existing images be cleared if no new image is provided? + protected virtual void SetSubItemImages(int rowIndex, OLVListItem item, bool shouldClearImages) { + if (!this.ShowImagesOnSubItems || this.OwnerDraw) + return; + + for (int i = 1; i < item.SubItems.Count; i++) { + this.SetSubItemImage(rowIndex, i, item.GetSubItem(i), shouldClearImages); + } + } + + /// + /// Set the subitem image natively + /// + /// + /// + /// + /// + public virtual void SetSubItemImage(int rowIndex, int subItemIndex, OLVListSubItem subItem, bool shouldClearImages) { + int imageIndex = this.GetActualImageIndex(subItem.ImageSelector); + if (shouldClearImages || imageIndex != -1) + NativeMethods.SetSubItemImage(this, rowIndex, subItemIndex, imageIndex); + } + + /// + /// Take ownership of the 'objects' collection. This separates our collection from the source. + /// + /// + /// + /// This method + /// separates the 'objects' instance variable from its source, so that any AddObject/RemoveObject + /// calls will modify our collection and not the original collection. + /// + /// + /// This method has the intentional side-effect of converting our list of objects to an ArrayList. + /// + /// + protected virtual void TakeOwnershipOfObjects() { + if (this.isOwnerOfObjects) + return; + + this.isOwnerOfObjects = true; + + this.objects = ObjectListView.EnumerableToArray(this.objects, true); + } + + /// + /// Trigger FormatRow and possibly FormatCell events for the given item + /// + /// + /// + /// + protected virtual bool TriggerFormatRowEvent(int rowIndex, int displayIndex, OLVListItem olvi) { + FormatRowEventArgs args = new FormatRowEventArgs(); + args.ListView = this; + args.RowIndex = rowIndex; + args.DisplayIndex = displayIndex; + args.Item = olvi; + args.UseCellFormatEvents = this.UseCellFormatEvents; + this.OnFormatRow(args); + return args.UseCellFormatEvents; + } + + /// + /// Trigger FormatCell events for the given item + /// + /// + /// + /// + protected virtual void TriggerFormatCellEvents(int rowIndex, int displayIndex, OLVListItem olvi) { + + PropagateFormatFromRowToCells(olvi); + + // Fire one event per cell + FormatCellEventArgs args2 = new FormatCellEventArgs(); + args2.ListView = this; + args2.RowIndex = rowIndex; + args2.DisplayIndex = displayIndex; + args2.Item = olvi; + for (int i = 0; i < this.Columns.Count; i++) { + args2.ColumnIndex = i; + args2.Column = this.GetColumn(i); + args2.SubItem = olvi.GetSubItem(i); + this.OnFormatCell(args2); + } + } + + private static void PropagateFormatFromRowToCells(OLVListItem olvi) { + // If a cell isn't given its own colors, it *should* use the colors of the item. + // However, there is a bug in the .NET framework where the cell are given + // the colors of the ListView instead of the colors of the row. + + // If we've already done this, don't do it again + if (olvi.UseItemStyleForSubItems == false) + return; + + // So we have to explicitly give each cell the fore and back colors and the font that it should have. + olvi.UseItemStyleForSubItems = false; + Color backColor = olvi.BackColor; + Color foreColor = olvi.ForeColor; + Font font = olvi.Font; + foreach (ListViewItem.ListViewSubItem subitem in olvi.SubItems) { + subitem.BackColor = backColor; + subitem.ForeColor = foreColor; + subitem.Font = font; + } + } + + /// + /// Make the list forget everything -- all rows and all columns + /// + /// Use if you want to remove just the rows. + public virtual void Reset() { + this.Clear(); + this.AllColumns.Clear(); + this.ClearObjects(); + this.PrimarySortColumn = null; + this.SecondarySortColumn = null; + this.ClearDisabledObjects(); + this.ClearPersistentCheckState(); + this.ClearUrlVisited(); + this.ClearHotItem(); + } + + + #endregion + + #region ISupportInitialize Members + + void ISupportInitialize.BeginInit() { + this.Frozen = true; + } + + void ISupportInitialize.EndInit() { + if (this.RowHeight != -1) { + this.SmallImageList = this.SmallImageList; + if (this.CheckBoxes) + this.InitializeStateImageList(); + } + + if (this.UseSubItemCheckBoxes || (this.VirtualMode && this.CheckBoxes)) + this.SetupSubItemCheckBoxes(); + + this.Frozen = false; + } + + #endregion + + #region Image list manipulation + + /// + /// Update our externally visible image list so it holds the same images as our shadow list, but sized correctly + /// + private void SetupBaseImageList() { + // If a row height hasn't been set, or an image list has been give which is the required size, just assign it + if (rowHeight == -1 || + this.View != View.Details || + (this.shadowedImageList != null && this.shadowedImageList.ImageSize.Height == rowHeight)) + this.BaseSmallImageList = this.shadowedImageList; + else { + int width = (this.shadowedImageList == null ? 16 : this.shadowedImageList.ImageSize.Width); + this.BaseSmallImageList = this.MakeResizedImageList(width, rowHeight, shadowedImageList); + } + } + + /// + /// Return a copy of the given source image list, where each image has been resized to be height x height in size. + /// If source is null, an empty image list of the given size is returned + /// + /// Height and width of the new images + /// Height and width of the new images + /// Source of the images (can be null) + /// A new image list + private ImageList MakeResizedImageList(int width, int height, ImageList source) { + ImageList il = new ImageList(); + il.ImageSize = new Size(width, height); + + // If there's nothing to copy, just return the new list + if (source == null) + return il; + + il.TransparentColor = source.TransparentColor; + il.ColorDepth = source.ColorDepth; + + // Fill the imagelist with resized copies from the source + for (int i = 0; i < source.Images.Count; i++) { + Bitmap bm = this.MakeResizedImage(width, height, source.Images[i], source.TransparentColor); + il.Images.Add(bm); + } + + // Give each image the same key it has in the original + foreach (String key in source.Images.Keys) { + il.Images.SetKeyName(source.Images.IndexOfKey(key), key); + } + + return il; + } + + /// + /// Return a bitmap of the given height x height, which shows the given image, centred. + /// + /// Height and width of new bitmap + /// Height and width of new bitmap + /// Image to be centred + /// The background color + /// A new bitmap + private Bitmap MakeResizedImage(int width, int height, Image image, Color transparent) { + Bitmap bm = new Bitmap(width, height); + Graphics g = Graphics.FromImage(bm); + g.Clear(transparent); + int x = Math.Max(0, (bm.Size.Width - image.Size.Width) / 2); + int y = Math.Max(0, (bm.Size.Height - image.Size.Height) / 2); + g.DrawImage(image, x, y, image.Size.Width, image.Size.Height); + return bm; + } + + /// + /// Initialize the state image list with the required checkbox images + /// + protected virtual void InitializeStateImageList() { + if (this.DesignMode) + return; + + if (!this.CheckBoxes) + return; + + if (this.StateImageList == null) { + this.StateImageList = new ImageList(); + this.StateImageList.ImageSize = new Size(16, this.RowHeight == -1 ? 16 : this.RowHeight); + this.StateImageList.ColorDepth = ColorDepth.Depth32Bit; + } + + if (this.RowHeight != -1 && + this.View == View.Details && + this.StateImageList.ImageSize.Height != this.RowHeight) { + this.StateImageList = new ImageList(); + this.StateImageList.ImageSize = new Size(16, this.RowHeight); + this.StateImageList.ColorDepth = ColorDepth.Depth32Bit; + } + + // The internal logic of ListView cycles through the state images when the primary + // checkbox is clicked. So we have to get exactly the right number of images in the + // image list. + if (this.StateImageList.Images.Count == 0) + this.AddCheckStateBitmap(this.StateImageList, UNCHECKED_KEY, CheckBoxState.UncheckedNormal); + if (this.StateImageList.Images.Count <= 1) + this.AddCheckStateBitmap(this.StateImageList, CHECKED_KEY, CheckBoxState.CheckedNormal); + if (this.TriStateCheckBoxes && this.StateImageList.Images.Count <= 2) + this.AddCheckStateBitmap(this.StateImageList, INDETERMINATE_KEY, CheckBoxState.MixedNormal); + else { + if (this.StateImageList.Images.ContainsKey(INDETERMINATE_KEY)) + this.StateImageList.Images.RemoveByKey(INDETERMINATE_KEY); + } + } + + /// + /// The name of the image used when a check box is checked + /// + public const string CHECKED_KEY = "checkbox-checked"; + + /// + /// The name of the image used when a check box is unchecked + /// + public const string UNCHECKED_KEY = "checkbox-unchecked"; + + /// + /// The name of the image used when a check box is Indeterminate + /// + public const string INDETERMINATE_KEY = "checkbox-indeterminate"; + + /// + /// Setup this control so it can display check boxes on subitems + /// (or primary checkboxes in virtual mode) + /// + /// This gives the ListView a small image list, if it doesn't already have one. + public virtual void SetupSubItemCheckBoxes() { + this.ShowImagesOnSubItems = true; + if (this.SmallImageList == null || !this.SmallImageList.Images.ContainsKey(CHECKED_KEY)) + this.InitializeSubItemCheckBoxImages(); + } + + /// + /// Make sure the small image list for this control has checkbox images + /// (used for sub-item checkboxes). + /// + /// + /// + /// This gives the ListView a small image list, if it doesn't already have one. + /// + /// + /// ObjectListView has to manage checkboxes on subitems separate from the checkboxes on each row. + /// The underlying ListView knows about the per-row checkboxes, and to make them work, OLV has to + /// correctly configure the StateImageList. However, the ListView cannot do checkboxes in subitems, + /// so ObjectListView has to handle them in a different fashion. So, per-row checkboxes are controlled + /// by images in the StateImageList, but per-cell checkboxes are handled by images in the SmallImageList. + /// + /// + protected virtual void InitializeSubItemCheckBoxImages() { + // Don't mess with the image list in design mode + if (this.DesignMode) + return; + + ImageList il = this.SmallImageList; + if (il == null) { + il = new ImageList(); + il.ImageSize = new Size(16, 16); + il.ColorDepth = ColorDepth.Depth32Bit; + } + + this.AddCheckStateBitmap(il, CHECKED_KEY, CheckBoxState.CheckedNormal); + this.AddCheckStateBitmap(il, UNCHECKED_KEY, CheckBoxState.UncheckedNormal); + this.AddCheckStateBitmap(il, INDETERMINATE_KEY, CheckBoxState.MixedNormal); + + this.SmallImageList = il; + } + + private void AddCheckStateBitmap(ImageList il, string key, CheckBoxState boxState) { + Bitmap b = new Bitmap(il.ImageSize.Width, il.ImageSize.Height); + Graphics g = Graphics.FromImage(b); + g.Clear(il.TransparentColor); + Point location = new Point(b.Width / 2 - 5, b.Height / 2 - 6); + CheckBoxRenderer.DrawCheckBox(g, location, boxState); + il.Images.Add(key, b); + } + + #endregion + + #region Owner drawing + + /// + /// Owner draw the column header + /// + /// + protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e) { + e.DrawDefault = true; + base.OnDrawColumnHeader(e); + } + + /// + /// Owner draw the item + /// + /// + protected override void OnDrawItem(DrawListViewItemEventArgs e) { + if (this.View == View.Details) + e.DrawDefault = false; + else { + if (this.ItemRenderer == null) + e.DrawDefault = true; + else { + Object row = ((OLVListItem)e.Item).RowObject; + e.DrawDefault = !this.ItemRenderer.RenderItem(e, e.Graphics, e.Bounds, row); + } + } + + if (e.DrawDefault) + base.OnDrawItem(e); + } + + /// + /// Owner draw a single subitem + /// + /// + protected override void OnDrawSubItem(DrawListViewSubItemEventArgs e) { + //System.Diagnostics.Debug.WriteLine(String.Format("OnDrawSubItem ({0}, {1})", e.ItemIndex, e.ColumnIndex)); + // Don't try to do owner drawing at design time + if (this.DesignMode) { + e.DrawDefault = true; + return; + } + + object rowObject = ((OLVListItem)e.Item).RowObject; + + // Calculate where the subitem should be drawn + Rectangle r = e.Bounds; + + // Get the special renderer for this column. If there isn't one, use the default draw mechanism. + OLVColumn column = this.GetColumn(e.ColumnIndex); + IRenderer renderer = this.GetCellRenderer(rowObject, column); + + // Get a graphics context for the renderer to use. + // But we have more complications. Virtual lists have a nasty habit of drawing column 0 + // whenever there is any mouse move events over a row, and doing it in an un-double-buffered manner, + // which results in nasty flickers! There are also some unbuffered draw when a mouse is first + // hovered over column 0 of a normal row. So, to avoid all complications, + // we always manually double-buffer the drawing. + // Except with Mono, which doesn't seem to handle double buffering at all :-( + BufferedGraphics buffer = BufferedGraphicsManager.Current.Allocate(e.Graphics, r); + Graphics g = buffer.Graphics; + + g.TextRenderingHint = ObjectListView.TextRenderingHint; + g.SmoothingMode = ObjectListView.SmoothingMode; + + // Finally, give the renderer a chance to draw something + e.DrawDefault = !renderer.RenderSubItem(e, g, r, rowObject); + + if (!e.DrawDefault) + buffer.Render(); + buffer.Dispose(); + } + + #endregion + + #region OnEvent Handling + + /// + /// We need the click count in the mouse up event, but that is always 1. + /// So we have to remember the click count from the preceding mouse down event. + /// + /// + protected override void OnMouseDown(MouseEventArgs e) { + //System.Diagnostics.Debug.WriteLine(String.Format("OnMouseDown: {0}, {1}", e.Button, e.Clicks)); + this.lastMouseDownClickCount = e.Clicks; + this.lastMouseDownButton = e.Button; + base.OnMouseDown(e); + } + private int lastMouseDownClickCount; + private MouseButtons lastMouseDownButton; + + /// + /// When the mouse leaves the control, remove any hot item highlighting + /// + /// + protected override void OnMouseLeave(EventArgs e) { + base.OnMouseLeave(e); + + if (!this.Created) + return; + + this.UpdateHotItem(new Point(-1,-1)); + } + + // We could change the hot item on the mouse hover event, but it looks wrong. + + //protected override void OnMouseHover(EventArgs e) { + // System.Diagnostics.Debug.WriteLine(String.Format("OnMouseHover")); + // base.OnMouseHover(e); + // this.UpdateHotItem(this.PointToClient(Cursor.Position)); + //} + + /// + /// When the mouse moves, we might need to change the hot item. + /// + /// + protected override void OnMouseMove(MouseEventArgs e) { + base.OnMouseMove(e); + + if (this.Created) + HandleMouseMove(e.Location); + } + + internal void HandleMouseMove(Point pt) { + //System.Diagnostics.Debug.WriteLine(String.Format("HandleMouseMove: {0}", pt)); + + CellOverEventArgs args = new CellOverEventArgs(); + this.BuildCellEvent(args, pt); + this.OnCellOver(args); + this.MouseMoveHitTest = args.HitTest; + + if (!args.Handled) + this.UpdateHotItem(args.HitTest); + } + + /// + /// Check to see if we need to start editing a cell + /// + /// + protected override void OnMouseUp(MouseEventArgs e) { + //System.Diagnostics.Debug.WriteLine(String.Format("OnMouseUp: {0}, {1}", e.Button, e.Clicks)); + + base.OnMouseUp(e); + + if (!this.Created) + return; + + // Sigh! More complexity. e.Button is not reliable when clicking on group headers. + // The mouse up event for first click on a group header always reports e.Button as None. + // Subsequent mouse up events report the button from the previous event. + // However, mouse down events are correctly reported, so we use the button value from + // the last mouse down event. + if (this.lastMouseDownButton == MouseButtons.Right) { + this.OnRightMouseUp(e); + return; + } + + // What event should we listen for to start cell editing? + // ------------------------------------------------------ + // + // We can't use OnMouseClick, OnMouseDoubleClick, OnClick, or OnDoubleClick + // since they are not triggered for clicks on subitems without Full Row Select. + // + // We could use OnMouseDown, but selecting rows is done in OnMouseUp. This means + // that if we start the editing during OnMouseDown, the editor will automatically + // lose focus when mouse up happens. + // + + // Tell the world about a cell click. If someone handles it, don't do anything else + CellClickEventArgs args = new CellClickEventArgs(); + this.BuildCellEvent(args, e.Location); + args.ClickCount = this.lastMouseDownClickCount; + this.OnCellClick(args); + if (args.Handled) + return; + + // Did the user click a hyperlink? + if (this.UseHyperlinks && + args.HitTest.HitTestLocation == HitTestLocation.Text && + args.SubItem != null && + !String.IsNullOrEmpty(args.SubItem.Url)) { + // We have to delay the running of this process otherwise we can generate + // a series of MouseUp events (don't ask me why) + this.BeginInvoke((MethodInvoker)delegate { this.ProcessHyperlinkClicked(args); }); + } + + // No one handled it so check to see if we should start editing. + if (!this.ShouldStartCellEdit(e)) + return; + + // We only start the edit if the user clicked on the image or text. + if (args.HitTest.HitTestLocation == HitTestLocation.Nothing) + return; + + // We don't edit the primary column by single clicks -- only subitems. + if (this.CellEditActivation == CellEditActivateMode.SingleClick && args.ColumnIndex <= 0) + return; + + // Don't start a cell edit operation when the user clicks on the background of a checkbox column -- it just looks wrong. + // If the user clicks on the actual checkbox, changing the checkbox state is handled elsewhere. + if (args.Column != null && args.Column.CheckBoxes) + return; + + this.EditSubItem(args.Item, args.ColumnIndex); + } + + /// + /// Tell the world that a hyperlink was clicked and if the event isn't handled, + /// do the default processing. + /// + /// + protected virtual void ProcessHyperlinkClicked(CellClickEventArgs e) { + HyperlinkClickedEventArgs args = new HyperlinkClickedEventArgs(); + args.HitTest = e.HitTest; + args.ListView = this; + args.Location = new Point(-1, -1); + args.Item = e.Item; + args.SubItem = e.SubItem; + args.Model = e.Model; + args.ColumnIndex = e.ColumnIndex; + args.Column = e.Column; + args.RowIndex = e.RowIndex; + args.ModifierKeys = Control.ModifierKeys; + args.Url = e.SubItem.Url; + this.OnHyperlinkClicked(args); + if (!args.Handled) { + this.StandardHyperlinkClickedProcessing(args); + } + } + + /// + /// Do the default processing for a hyperlink clicked event, which + /// is to try and open the url. + /// + /// + protected virtual void StandardHyperlinkClickedProcessing(HyperlinkClickedEventArgs args) { + Cursor originalCursor = this.Cursor; + try { + this.Cursor = Cursors.WaitCursor; + System.Diagnostics.Process.Start(args.Url); + } catch (Win32Exception) { + System.Media.SystemSounds.Beep.Play(); + // ignore it + } finally { + this.Cursor = originalCursor; + } + this.MarkUrlVisited(args.Url); + this.RefreshHotItem(); + } + + /// + /// The user right clicked on the control + /// + /// + protected virtual void OnRightMouseUp(MouseEventArgs e) { + CellRightClickEventArgs args = new CellRightClickEventArgs(); + this.BuildCellEvent(args, e.Location); + this.OnCellRightClick(args); + if (!args.Handled) { + if (args.MenuStrip != null) { + args.MenuStrip.Show(this, args.Location); + } + } + } + + internal void BuildCellEvent(CellEventArgs args, Point location) { + BuildCellEvent(args, location, this.OlvHitTest(location.X, location.Y)); + } + + internal void BuildCellEvent(CellEventArgs args, Point location, OlvListViewHitTestInfo hitTest) { + args.HitTest = hitTest; + args.ListView = this; + args.Location = location; + args.Item = hitTest.Item; + args.SubItem = hitTest.SubItem; + args.Model = hitTest.RowObject; + args.ColumnIndex = hitTest.ColumnIndex; + args.Column = hitTest.Column; + if (hitTest.Item != null) + args.RowIndex = hitTest.Item.Index; + args.ModifierKeys = Control.ModifierKeys; + + // In non-details view, we want any hit on an item to act as if it was a hit + // on column 0 -- which, effectively, it was. + if (args.Item != null && args.ListView.View != View.Details) { + args.ColumnIndex = 0; + args.Column = args.ListView.GetColumn(0); + args.SubItem = args.Item.GetSubItem(0); + } + } + + /// + /// This method is called every time a row is selected or deselected. This can be + /// a pain if the user shift-clicks 100 rows. We override this method so we can + /// trigger one event for any number of select/deselects that come from one user action + /// + /// + protected override void OnSelectedIndexChanged(EventArgs e) { + if (this.SelectionEventsSuspended) + return; + + base.OnSelectedIndexChanged(e); + + this.TriggerDeferredSelectionChangedEvent(); + } + + /// + /// Schedule a SelectionChanged event to happen after the next idle event, + /// unless we've already scheduled that to happen. + /// + protected virtual void TriggerDeferredSelectionChangedEvent() { + if (this.SelectionEventsSuspended) + return; + + // If we haven't already scheduled an event, schedule it to be triggered + // By using idle time, we will wait until all select events for the same + // user action have finished before triggering the event. + if (!this.hasIdleHandler) { + this.hasIdleHandler = true; + this.RunWhenIdle(HandleApplicationIdle); + } + } + + /// + /// Called when the handle of the underlying control is created + /// + /// + protected override void OnHandleCreated(EventArgs e) { + //Debug.WriteLine("OnHandleCreated"); + base.OnHandleCreated(e); + + this.Invoke((MethodInvoker)this.OnControlCreated); + } + + /// + /// This method is called after the control has been fully created. + /// + protected virtual void OnControlCreated() { + + //Debug.WriteLine("OnControlCreated"); + + // Force the header control to be created when the listview handle is + HeaderControl hc = this.HeaderControl; + hc.WordWrap = this.HeaderWordWrap; + + // Make sure any overlays that are set on the hot item style take effect + this.HotItemStyle = this.HotItemStyle; + + // Arrange for any group images to be installed after the control is created + NativeMethods.SetGroupImageList(this, this.GroupImageList); + + this.UseExplorerTheme = this.UseExplorerTheme; + + this.RememberDisplayIndicies(); + this.SetGroupSpacing(); + + if (this.VirtualMode) + this.ApplyExtendedStyles(); + } + + #endregion + + #region Cell editing + + /// + /// Should we start editing the cell in response to the given mouse button event? + /// + /// + /// + protected virtual bool ShouldStartCellEdit(MouseEventArgs e) { + if (this.IsCellEditing) + return false; + + if (e.Button != MouseButtons.Left && e.Button != MouseButtons.Right) + return false; + + if ((Control.ModifierKeys & (Keys.Shift | Keys.Control | Keys.Alt)) != 0) + return false; + + if (this.lastMouseDownClickCount == 1 && ( + this.CellEditActivation == CellEditActivateMode.SingleClick || + this.CellEditActivation == CellEditActivateMode.SingleClickAlways)) + return true; + + return (this.lastMouseDownClickCount == 2 && this.CellEditActivation == CellEditActivateMode.DoubleClick); + } + + /// + /// Handle a key press on this control. We specifically look for F2 which edits the primary column, + /// or a Tab character during an edit operation, which tries to start editing on the next (or previous) cell. + /// + /// + /// + protected override bool ProcessDialogKey(Keys keyData) { + + if (this.IsCellEditing) + return this.CellEditKeyEngine.HandleKey(this, keyData); + + // Treat F2 as a request to edit the primary column + if (keyData == Keys.F2) { + this.EditSubItem((OLVListItem)this.FocusedItem, 0); + return base.ProcessDialogKey(keyData); + } + + // Treat Ctrl-C as Copy To Clipboard. + if (this.CopySelectionOnControlC && keyData == (Keys.C | Keys.Control)) { + this.CopySelectionToClipboard(); + return true; + } + + // Treat Ctrl-A as Select All. + if (this.SelectAllOnControlA && keyData == (Keys.A | Keys.Control)) { + this.SelectAll(); + return true; + } + + return base.ProcessDialogKey(keyData); + } + + /// + /// Start an editing operation on the first editable column of the given model. + /// + /// + /// + /// + /// If the model doesn't exist, or there are no editable columns, this method + /// will do nothing. + /// + /// This will start an edit operation regardless of CellActivationMode. + /// + /// + public virtual void EditModel(object rowModel) { + OLVListItem olvItem = this.ModelToItem(rowModel); + if (olvItem == null) + return; + + for (int i = 0; i < olvItem.SubItems.Count; i++) { + var olvColumn = this.GetColumn(i); + if (olvColumn != null && olvColumn.IsEditable) { + this.StartCellEdit(olvItem, i); + return; + } + } + } + + /// + /// Begin an edit operation on the given cell. + /// + /// This performs various sanity checks and passes off the real work to StartCellEdit(). + /// The row to be edited + /// The index of the cell to be edited + public virtual void EditSubItem(OLVListItem item, int subItemIndex) { + if (item == null) + return; + + if (this.CellEditActivation == CellEditActivateMode.None) + return; + + OLVColumn olvColumn = this.GetColumn(subItemIndex); + if (olvColumn == null || !olvColumn.IsEditable) + return; + + if (!item.Enabled) + return; + + this.StartCellEdit(item, subItemIndex); + } + + /// + /// Really start an edit operation on a given cell. The parameters are assumed to be sane. + /// + /// The row to be edited + /// The index of the cell to be edited + public virtual void StartCellEdit(OLVListItem item, int subItemIndex) { + OLVColumn column = this.GetColumn(subItemIndex); + if (column == null) + return; + Control c = this.GetCellEditor(item, subItemIndex); + Rectangle cellBounds = this.CalculateCellBounds(item, subItemIndex); + c.Bounds = this.CalculateCellEditorBounds(item, subItemIndex, c.PreferredSize); + + // Try to align the control as the column is aligned. Not all controls support this property + Munger.PutProperty(c, "TextAlign", column.TextAlign); + + // Give the control the value from the model + this.SetControlValue(c, column.GetValue(item.RowObject), column.GetStringValue(item.RowObject)); + + // Give the outside world the chance to munge with the process + this.CellEditEventArgs = new CellEditEventArgs(column, c, cellBounds, item, subItemIndex); + this.OnCellEditStarting(this.CellEditEventArgs); + if (this.CellEditEventArgs.Cancel) + return; + + // The event handler may have completely changed the control, so we need to remember it + this.cellEditor = this.CellEditEventArgs.Control; + + this.Invalidate(); + this.Controls.Add(this.cellEditor); + this.ConfigureControl(); + this.PauseAnimations(true); + } + private Control cellEditor; + internal CellEditEventArgs CellEditEventArgs; + + /// + /// Calculate the bounds of the edit control for the given item/column + /// + /// + /// + /// + /// + public Rectangle CalculateCellEditorBounds(OLVListItem item, int subItemIndex, Size preferredSize) { + Rectangle r = CalculateCellBounds(item, subItemIndex); + + // Calculate the width of the cell's current contents + return this.OwnerDraw + ? CalculateCellEditorBoundsOwnerDrawn(item, subItemIndex, r, preferredSize) + : CalculateCellEditorBoundsStandard(item, subItemIndex, r, preferredSize); + } + + /// + /// Calculate the bounds of the edit control for the given item/column, when the listview + /// is being owner drawn. + /// + /// + /// + /// + /// + /// A rectangle that is the bounds of the cell editor + protected Rectangle CalculateCellEditorBoundsOwnerDrawn(OLVListItem item, int subItemIndex, Rectangle r, Size preferredSize) { + IRenderer renderer = this.View == View.Details + ? this.GetCellRenderer(item.RowObject, this.GetColumn(subItemIndex)) + : this.ItemRenderer; + + if (renderer == null) + return r; + + using (Graphics g = this.CreateGraphics()) { + return renderer.GetEditRectangle(g, r, item, subItemIndex, preferredSize); + } + } + + /// + /// Calculate the bounds of the edit control for the given item/column, when the listview + /// is not being owner drawn. + /// + /// + /// + /// + /// + /// A rectangle that is the bounds of the cell editor + protected Rectangle CalculateCellEditorBoundsStandard(OLVListItem item, int subItemIndex, Rectangle cellBounds, Size preferredSize) { + if (this.View == View.Tile) + return cellBounds; + + // Center the editor vertically + if (cellBounds.Height != preferredSize.Height) + cellBounds.Y += (cellBounds.Height - preferredSize.Height) / 2; + + // Only Details view needs more processing + if (this.View != View.Details) + return cellBounds; + + // Allow for image (if there is one). + int offset = 0; + object imageSelector = null; + if (subItemIndex == 0) + imageSelector = item.ImageSelector; + else { + // We only check for subitem images if we are owner drawn or showing subitem images + if (this.OwnerDraw || this.ShowImagesOnSubItems) + imageSelector = item.GetSubItem(subItemIndex).ImageSelector; + } + if (this.GetActualImageIndex(imageSelector) != -1) { + offset += this.SmallImageSize.Width + 2; + } + + // Allow for checkbox + if (this.CheckBoxes && this.StateImageList != null && subItemIndex == 0) { + offset += this.StateImageList.ImageSize.Width + 2; + } + + // Allow for indent (first column only) + if (subItemIndex == 0 && item.IndentCount > 0) { + offset += (this.SmallImageSize.Width * item.IndentCount); + } + + // Do the adjustment + if (offset > 0) { + cellBounds.X += offset; + cellBounds.Width -= offset; + } + + return cellBounds; + } + + /// + /// Try to give the given value to the provided control. Fall back to assigning a string + /// if the value assignment fails. + /// + /// A control + /// The value to be given to the control + /// The string to be given if the value doesn't work + protected virtual void SetControlValue(Control control, Object value, String stringValue) { + // Does the control implement our custom interface? + IOlvEditor olvEditor = control as IOlvEditor; + if (olvEditor != null) { + olvEditor.Value = value; + return; + } + + // Handle combobox explicitly + ComboBox cb = control as ComboBox; + if (cb != null) { + if (cb.Created) + cb.SelectedValue = value; + else + this.BeginInvoke(new MethodInvoker(delegate { + cb.SelectedValue = value; + })); + return; + } + + if (Munger.PutProperty(control, "Value", value)) + return; + + // There wasn't a Value property, or we couldn't set it, so set the text instead + try + { + String valueAsString = value as String; + control.Text = valueAsString ?? stringValue; + } + catch (ArgumentOutOfRangeException) { + // The value couldn't be set via the Text property. + } + } + + /// + /// Setup the given control to be a cell editor + /// + protected virtual void ConfigureControl() { + this.cellEditor.Validating += new CancelEventHandler(CellEditor_Validating); + this.cellEditor.Select(); + } + + /// + /// Return the value that the given control is showing + /// + /// + /// + protected virtual Object GetControlValue(Control control) { + if (control == null) + return null; + + IOlvEditor olvEditor = control as IOlvEditor; + if (olvEditor != null) + return olvEditor.Value; + + TextBox box = control as TextBox; + if (box != null) + return box.Text; + + ComboBox comboBox = control as ComboBox; + if (comboBox != null) + return comboBox.SelectedValue; + + CheckBox checkBox = control as CheckBox; + if (checkBox != null) + return checkBox.Checked; + + try { + return control.GetType().InvokeMember("Value", BindingFlags.GetProperty, null, control, null); + } catch (MissingMethodException) { // Microsoft throws this + return control.Text; + } catch (MissingFieldException) { // Mono throws this + return control.Text; + } + } + + /// + /// Called when the cell editor could be about to lose focus. Time to commit the change + /// + /// + /// + protected virtual void CellEditor_Validating(object sender, CancelEventArgs e) { + this.CellEditEventArgs.Cancel = false; + this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); + this.OnCellEditorValidating(this.CellEditEventArgs); + + if (this.CellEditEventArgs.Cancel) { + this.CellEditEventArgs.Control.Select(); + e.Cancel = true; + } else + FinishCellEdit(); + } + + /// + /// Return the bounds of the given cell + /// + /// The row to be edited + /// The index of the cell to be edited + /// A Rectangle + public virtual Rectangle CalculateCellBounds(OLVListItem item, int subItemIndex) { + + // It seems on Win7, GetSubItemBounds() does not have the same problems with + // column 0 that it did previously. + + // TODO - Check on XP + + if (this.View != View.Details) + return this.GetItemRect(item.Index, ItemBoundsPortion.Label); + + Rectangle r = item.GetSubItemBounds(subItemIndex); + r.Width -= 1; + r.Height -= 1; + return r; + + // We use ItemBoundsPortion.Label rather than ItemBoundsPortion.Item + // since Label extends to the right edge of the cell, whereas Item gives just the + // current text width. + //return this.CalculateCellBounds(item, subItemIndex, ItemBoundsPortion.Label); + } + + /// + /// Return the bounds of the given cell only until the edge of the current text + /// + /// The row to be edited + /// The index of the cell to be edited + /// A Rectangle + public virtual Rectangle CalculateCellTextBounds(OLVListItem item, int subItemIndex) { + return this.CalculateCellBounds(item, subItemIndex, ItemBoundsPortion.ItemOnly); + } + + private Rectangle CalculateCellBounds(OLVListItem item, int subItemIndex, ItemBoundsPortion portion) { + // SubItem.Bounds works for every subitem, except the first. + if (subItemIndex > 0) + return item.GetSubItemBounds(subItemIndex); + + // For non detail views, we just use the requested portion + Rectangle r = this.GetItemRect(item.Index, portion); + if (r.Y < -10000000 || r.Y > 10000000) { + r.Y = item.Bounds.Y; + } + if (this.View != View.Details) + return r; + + // Finding the bounds of cell 0 should not be a difficult task, but it is. Problems: + // 1) item.SubItem[0].Bounds is always the full bounds of the entire row, not just cell 0. + // 2) if column 0 has been dragged to some other position, the bounds always has a left edge of 0. + + // We avoid both these problems by using the position of sides the column header to calculate + // the sides of the cell + Point sides = NativeMethods.GetScrolledColumnSides(this, 0); + r.X = sides.X + 4; + r.Width = sides.Y - sides.X - 5; + + return r; + } + + /// + /// Calculate the visible bounds of the given column. The column's bottom edge is + /// either the bottom of the last row or the bottom of the control. + /// + /// The bounds of the control itself + /// The column + /// A Rectangle + /// This returns an empty rectangle if the control isn't in Details mode, + /// OR has doesn't have any rows, OR if the given column is hidden. + public virtual Rectangle CalculateColumnVisibleBounds(Rectangle bounds, OLVColumn column) + { + // Sanity checks + if (column == null || + this.View != System.Windows.Forms.View.Details || + this.GetItemCount() == 0 || + !column.IsVisible) + return Rectangle.Empty; + + Point sides = NativeMethods.GetScrolledColumnSides(this, column.Index); + if (sides.X == -1) + return Rectangle.Empty; + + Rectangle columnBounds = new Rectangle(sides.X, bounds.Top, sides.Y - sides.X, bounds.Bottom); + + // Find the bottom of the last item. The column only extends to there. + OLVListItem lastItem = this.GetLastItemInDisplayOrder(); + if (lastItem != null) + { + Rectangle lastItemBounds = lastItem.Bounds; + if (!lastItemBounds.IsEmpty && lastItemBounds.Bottom < columnBounds.Bottom) + columnBounds.Height = lastItemBounds.Bottom - columnBounds.Top; + } + + return columnBounds; + } + + /// + /// Return a control that can be used to edit the value of the given cell. + /// + /// The row to be edited + /// The index of the cell to be edited + /// A Control to edit the given cell + protected virtual Control GetCellEditor(OLVListItem item, int subItemIndex) { + OLVColumn column = this.GetColumn(subItemIndex); + Object value = column.GetValue(item.RowObject); + + // Does the column have its own special way of creating cell editors? + if (column.EditorCreator != null) { + Control customEditor = column.EditorCreator(item.RowObject, column, value); + if (customEditor != null) + return customEditor; + } + + // Ask the registry for an instance of the appropriate editor. + // Use a default editor if the registry can't create one for us. + Control editor = ObjectListView.EditorRegistry.GetEditor(item.RowObject, column, value ?? this.GetFirstNonNullValue(column)); + return editor ?? this.MakeDefaultCellEditor(column); + } + + /// + /// Get the first non-null value of the given column. + /// At most 1000 rows will be considered. + /// + /// + /// The first non-null value, or null if no non-null values were found + internal object GetFirstNonNullValue(OLVColumn column) { + for (int i = 0; i < Math.Min(this.GetItemCount(), 1000); i++) { + object value = column.GetValue(this.GetModelObject(i)); + if (value != null) + return value; + } + return null; + } + + /// + /// Return a TextBox that can be used as a default cell editor. + /// + /// What column does the cell belong to? + /// + protected virtual Control MakeDefaultCellEditor(OLVColumn column) { + TextBox tb = new TextBox(); + if (column.AutoCompleteEditor) + this.ConfigureAutoComplete(tb, column); + return tb; + } + + /// + /// Configure the given text box to autocomplete unique values + /// from the given column. At most 1000 rows will be considered. + /// + /// The textbox to configure + /// The column used to calculate values + public void ConfigureAutoComplete(TextBox tb, OLVColumn column) { + this.ConfigureAutoComplete(tb, column, 1000); + } + + + /// + /// Configure the given text box to autocomplete unique values + /// from the given column. At most 1000 rows will be considered. + /// + /// The textbox to configure + /// The column used to calculate values + /// Consider only this many rows + public void ConfigureAutoComplete(TextBox tb, OLVColumn column, int maxRows) { + // Don't consider more rows than we actually have + maxRows = Math.Min(this.GetItemCount(), maxRows); + + // Reset any existing autocomplete + tb.AutoCompleteCustomSource.Clear(); + + // CONSIDER: Should we use ClusteringStrategy here? + + // Build a list of unique values, to be used as autocomplete on the editor + Dictionary alreadySeen = new Dictionary(); + List values = new List(); + for (int i = 0; i < maxRows; i++) { + string valueAsString = column.GetStringValue(this.GetModelObject(i)); + if (!String.IsNullOrEmpty(valueAsString) && !alreadySeen.ContainsKey(valueAsString)) { + values.Add(valueAsString); + alreadySeen[valueAsString] = true; + } + } + + tb.AutoCompleteCustomSource.AddRange(values.ToArray()); + tb.AutoCompleteSource = AutoCompleteSource.CustomSource; + tb.AutoCompleteMode = column.AutoCompleteEditorMode; + } + + /// + /// Stop editing a cell and throw away any changes. + /// + public virtual void CancelCellEdit() { + if (!this.IsCellEditing) + return; + + // Let the world know that the user has cancelled the edit operation + this.CellEditEventArgs.Cancel = true; + this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); + this.OnCellEditFinishing(this.CellEditEventArgs); + + // Now cleanup the editing process + this.CleanupCellEdit(false, this.CellEditEventArgs.AutoDispose); + } + + /// + /// If a cell edit is in progress, finish the edit. + /// + /// Returns false if the finishing process was cancelled + /// (i.e. the cell editor is still on screen) + /// This method does not guarantee that the editing will finish. The validation + /// process can cause the finishing to be aborted. Developers should check the return value + /// or use IsCellEditing property after calling this method to see if the user is still + /// editing a cell. + public virtual bool PossibleFinishCellEditing() { + return this.PossibleFinishCellEditing(false); + } + + /// + /// If a cell edit is in progress, finish the edit. + /// + /// Returns false if the finishing process was cancelled + /// (i.e. the cell editor is still on screen) + /// This method does not guarantee that the editing will finish. The validation + /// process can cause the finishing to be aborted. Developers should check the return value + /// or use IsCellEditing property after calling this method to see if the user is still + /// editing a cell. + /// True if it is likely that another cell is going to be + /// edited immediately after this cell finishes editing + public virtual bool PossibleFinishCellEditing(bool expectingCellEdit) { + if (!this.IsCellEditing) + return true; + + this.CellEditEventArgs.Cancel = false; + this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); + this.OnCellEditorValidating(this.CellEditEventArgs); + + if (this.CellEditEventArgs.Cancel) + return false; + + this.FinishCellEdit(expectingCellEdit); + + return true; + } + + /// + /// Finish the cell edit operation, writing changed data back to the model object + /// + /// This method does not trigger a Validating event, so it always finishes + /// the cell edit. + public virtual void FinishCellEdit() { + this.FinishCellEdit(false); + } + + /// + /// Finish the cell edit operation, writing changed data back to the model object + /// + /// This method does not trigger a Validating event, so it always finishes + /// the cell edit. + /// True if it is likely that another cell is going to be + /// edited immediately after this cell finishes editing + public virtual void FinishCellEdit(bool expectingCellEdit) { + if (!this.IsCellEditing) + return; + + this.CellEditEventArgs.Cancel = false; + this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); + this.OnCellEditFinishing(this.CellEditEventArgs); + + // If someone doesn't cancel the editing process, write the value back into the model + if (!this.CellEditEventArgs.Cancel) { + this.CellEditEventArgs.Column.PutValue(this.CellEditEventArgs.RowObject, this.CellEditEventArgs.NewValue); + this.RefreshItem(this.CellEditEventArgs.ListViewItem); + } + + this.CleanupCellEdit(expectingCellEdit, this.CellEditEventArgs.AutoDispose); + + // Tell the world that the cell has been edited + this.OnCellEditFinished(this.CellEditEventArgs); + } + + /// + /// Remove all trace of any existing cell edit operation + /// + /// True if it is likely that another cell is going to be + /// edited immediately after this cell finishes editing + /// True if the cell editor should be disposed + protected virtual void CleanupCellEdit(bool expectingCellEdit, bool disposeOfCellEditor) { + if (this.cellEditor == null) + return; + + this.cellEditor.Validating -= new CancelEventHandler(CellEditor_Validating); + + Control soonToBeOldCellEditor = this.cellEditor; + this.cellEditor = null; + + // Delay cleaning up the cell editor so that if we are immediately going to + // start a new cell edit (because the user pressed Tab) the new cell editor + // has a chance to grab the focus. Without this, the ListView gains focus + // momentarily (after the cell editor is remove and before the new one is created) + // causing the list's selection to flash momentarily. + EventHandler toBeRun = null; + toBeRun = delegate(object sender, EventArgs e) { + Application.Idle -= toBeRun; + this.Controls.Remove(soonToBeOldCellEditor); + if (disposeOfCellEditor) + soonToBeOldCellEditor.Dispose(); + this.Invalidate(); + + if (!this.IsCellEditing) { + if (this.Focused) + this.Select(); + this.PauseAnimations(false); + } + }; + + // We only want to delay the removal of the control if we are expecting another cell + // to be edited. Otherwise, we remove the control immediately. + if (expectingCellEdit) + this.RunWhenIdle(toBeRun); + else + toBeRun(null, null); + } + + #endregion + + #region Hot row and cell handling + + /// + /// Force the hot item to be recalculated + /// + public virtual void ClearHotItem() { + this.UpdateHotItem(new Point(-1, -1)); + } + + /// + /// Force the hot item to be recalculated + /// + public virtual void RefreshHotItem() { + this.UpdateHotItem(this.PointToClient(Cursor.Position)); + } + + /// + /// The mouse has moved to the given pt. See if the hot item needs to be updated + /// + /// Where is the mouse? + /// This is the main entry point for hot item handling + protected virtual void UpdateHotItem(Point pt) { + this.UpdateHotItem(this.OlvHitTest(pt.X, pt.Y)); + } + + /// + /// The mouse has moved to the given pt. See if the hot item needs to be updated + /// + /// + /// This is the main entry point for hot item handling + protected virtual void UpdateHotItem(OlvListViewHitTestInfo hti) { + + // We only need to do the work of this method when the list has hot parts + // (i.e. some element whose visual appearance changes when under the mouse)? + // Hot item decorations and hyperlinks are obvious, but if we have checkboxes + // or buttons, those are also "hot". It's difficult to quickly detect if there are any + // columns that have checkboxes or buttons, so we just abdicate responsibility and + // provide a property (UseHotControls) which lets the programmer say whether to do + // the hot processing or not. + if (!this.UseHotItem && !this.UseHyperlinks && !this.UseHotControls) + return; + + int newHotRow = hti.RowIndex; + int newHotColumn = hti.ColumnIndex; + HitTestLocation newHotCellHitLocation = hti.HitTestLocation; + HitTestLocationEx newHotCellHitLocationEx = hti.HitTestLocationEx; + OLVGroup newHotGroup = hti.Group; + + // In non-details view, we treat any hit on a row as if it were a hit + // on column 0 -- which (effectively) it is! + if (newHotRow >= 0 && this.View != View.Details) + newHotColumn = 0; + + if (this.HotRowIndex == newHotRow && + this.HotColumnIndex == newHotColumn && + this.HotCellHitLocation == newHotCellHitLocation && + this.HotCellHitLocationEx == newHotCellHitLocationEx && + this.HotGroup == newHotGroup) { + return; + } + + // Trigger the hotitem changed event + HotItemChangedEventArgs args = new HotItemChangedEventArgs(); + args.HotCellHitLocation = newHotCellHitLocation; + args.HotCellHitLocationEx = newHotCellHitLocationEx; + args.HotColumnIndex = newHotColumn; + args.HotRowIndex = newHotRow; + args.HotGroup = newHotGroup; + args.OldHotCellHitLocation = this.HotCellHitLocation; + args.OldHotCellHitLocationEx = this.HotCellHitLocationEx; + args.OldHotColumnIndex = this.HotColumnIndex; + args.OldHotRowIndex = this.HotRowIndex; + args.OldHotGroup = this.HotGroup; + this.OnHotItemChanged(args); + + // Update the state of the control + this.HotRowIndex = newHotRow; + this.HotColumnIndex = newHotColumn; + this.HotCellHitLocation = newHotCellHitLocation; + this.HotCellHitLocationEx = newHotCellHitLocationEx; + this.HotGroup = newHotGroup; + + // If the event handler handled it complete, don't do anything else + if (args.Handled) + return; + +// System.Diagnostics.Debug.WriteLine(String.Format("Changed hot item: {0}", args)); + + this.BeginUpdate(); + try { + this.Invalidate(); + if (args.OldHotRowIndex != -1) + this.UnapplyHotItem(args.OldHotRowIndex); + + if (this.HotRowIndex != -1) { + // Virtual lists apply hot item style when fetching their rows + if (this.VirtualMode) { + this.ClearCachedInfo(); + this.RedrawItems(this.HotRowIndex, this.HotRowIndex, true); + } else { + this.UpdateHotRow(this.HotRowIndex, this.HotColumnIndex, this.HotCellHitLocation, hti.Item); + } + } + + if (this.UseHotItem && this.HotItemStyleOrDefault.Overlay != null) { + this.RefreshOverlays(); + } + } + finally { + this.EndUpdate(); + } + } + + /// + /// Update the given row using the current hot item information + /// + /// + protected virtual void UpdateHotRow(OLVListItem olvi) { + this.UpdateHotRow(this.HotRowIndex, this.HotColumnIndex, this.HotCellHitLocation, olvi); + } + + /// + /// Update the given row using the given hot item information + /// + /// + /// + /// + /// + protected virtual void UpdateHotRow(int rowIndex, int columnIndex, HitTestLocation hitLocation, OLVListItem olvi) { + if (rowIndex < 0 || columnIndex < 0) + return; + + // System.Diagnostics.Debug.WriteLine(String.Format("UpdateHotRow: {0}, {1}, {2}", rowIndex, columnIndex, hitLocation)); + + if (this.UseHyperlinks) { + OLVColumn column = this.GetColumn(columnIndex); + OLVListSubItem subItem = olvi.GetSubItem(columnIndex); + if (column != null && column.Hyperlink && hitLocation == HitTestLocation.Text && !String.IsNullOrEmpty(subItem.Url)) { + this.ApplyCellStyle(olvi, columnIndex, this.HyperlinkStyle.Over); + this.Cursor = this.HyperlinkStyle.OverCursor ?? Cursors.Default; + } else { + this.Cursor = Cursors.Default; + } + } + + if (this.UseHotItem) { + if (!olvi.Selected && olvi.Enabled) { + this.ApplyRowStyle(olvi, this.HotItemStyleOrDefault); + } + } + } + + /// + /// Apply a style to the given row + /// + /// + /// + public virtual void ApplyRowStyle(OLVListItem olvi, IItemStyle style) { + if (style == null) + return; + + Font font = style.Font ?? olvi.Font; + + if (style.FontStyle != FontStyle.Regular) + font = new Font(font ?? this.Font, style.FontStyle); + + if (!Equals(font, olvi.Font)) { + if (olvi.UseItemStyleForSubItems) + olvi.Font = font; + else { + foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) + x.Font = font; + } + } + + if (!style.ForeColor.IsEmpty) { + if (olvi.UseItemStyleForSubItems) + olvi.ForeColor = style.ForeColor; + else { + foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) + x.ForeColor = style.ForeColor; + } + } + + if (!style.BackColor.IsEmpty) { + if (olvi.UseItemStyleForSubItems) + olvi.BackColor = style.BackColor; + else { + foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) + x.BackColor = style.BackColor; + } + } + } + + /// + /// Apply a style to a cell + /// + /// + /// + /// + protected virtual void ApplyCellStyle(OLVListItem olvi, int columnIndex, IItemStyle style) { + if (style == null) + return; + + // Don't apply formatting to subitems when not in Details view + if (this.View != View.Details && columnIndex > 0) + return; + + olvi.UseItemStyleForSubItems = false; + + ListViewItem.ListViewSubItem subItem = olvi.SubItems[columnIndex]; + if (style.Font != null) + subItem.Font = style.Font; + + if (style.FontStyle != FontStyle.Regular) + subItem.Font = new Font(subItem.Font ?? olvi.Font ?? this.Font, style.FontStyle); + + if (!style.ForeColor.IsEmpty) + subItem.ForeColor = style.ForeColor; + + if (!style.BackColor.IsEmpty) + subItem.BackColor = style.BackColor; + } + + /// + /// Remove hot item styling from the given row + /// + /// + protected virtual void UnapplyHotItem(int index) { + this.Cursor = Cursors.Default; + // Virtual lists will apply the appropriate formatting when the row is fetched + if (this.VirtualMode) { + if (index < this.VirtualListSize) + this.RedrawItems(index, index, true); + } else { + OLVListItem olvi = this.GetItem(index); + if (olvi != null) { + //this.PostProcessOneRow(index, index, olvi); + this.RefreshItem(olvi); + } + } + } + + + #endregion + + #region Drag and drop + + /// + /// + /// + /// + protected override void OnItemDrag(ItemDragEventArgs e) { + base.OnItemDrag(e); + + if (this.DragSource == null) + return; + + Object data = this.DragSource.StartDrag(this, e.Button, (OLVListItem)e.Item); + if (data != null) { + DragDropEffects effect = this.DoDragDrop(data, this.DragSource.GetAllowedEffects(data)); + this.DragSource.EndDrag(data, effect); + } + } + + /// + /// + /// + /// + protected override void OnDragEnter(DragEventArgs args) { + base.OnDragEnter(args); + + if (this.DropSink != null) + this.DropSink.Enter(args); + } + + /// + /// + /// + /// + protected override void OnDragOver(DragEventArgs args) { + base.OnDragOver(args); + + if (this.DropSink != null) + this.DropSink.Over(args); + } + + /// + /// + /// + /// + protected override void OnDragDrop(DragEventArgs args) { + base.OnDragDrop(args); + + this.lastMouseDownClickCount = 0; // prevent drop events from becoming cell edits + + if (this.DropSink != null) + this.DropSink.Drop(args); + } + + /// + /// + /// + /// + protected override void OnDragLeave(EventArgs e) { + base.OnDragLeave(e); + + if (this.DropSink != null) + this.DropSink.Leave(); + } + + /// + /// + /// + /// + protected override void OnGiveFeedback(GiveFeedbackEventArgs args) { + base.OnGiveFeedback(args); + + if (this.DropSink != null) + this.DropSink.GiveFeedback(args); + } + + /// + /// + /// + /// + protected override void OnQueryContinueDrag(QueryContinueDragEventArgs args) { + base.OnQueryContinueDrag(args); + + if (this.DropSink != null) + this.DropSink.QueryContinue(args); + } + + #endregion + + #region Decorations and Overlays + + /// + /// Add the given decoration to those on this list and make it appear + /// + /// The decoration + /// + /// A decoration scrolls with the listview. An overlay stays fixed in place. + /// + public virtual void AddDecoration(IDecoration decoration) { + if (decoration == null) + return; + this.Decorations.Add(decoration); + this.Invalidate(); + } + + /// + /// Add the given overlay to those on this list and make it appear + /// + /// The overlay + public virtual void AddOverlay(IOverlay overlay) { + if (overlay == null) + return; + this.Overlays.Add(overlay); + this.Invalidate(); + } + + /// + /// Draw all the decorations + /// + /// A Graphics + /// The items that were redrawn and whose decorations should also be redrawn + protected virtual void DrawAllDecorations(Graphics g, List itemsThatWereRedrawn) { + g.TextRenderingHint = ObjectListView.TextRenderingHint; + g.SmoothingMode = ObjectListView.SmoothingMode; + + Rectangle contentRectangle = this.ContentRectangle; + + if (this.HasEmptyListMsg && this.GetItemCount() == 0) { + this.EmptyListMsgOverlay.Draw(this, g, contentRectangle); + } + + // Let the drop sink draw whatever feedback it likes + if (this.DropSink != null) { + this.DropSink.DrawFeedback(g, contentRectangle); + } + + // Draw our item and subitem decorations + foreach (OLVListItem olvi in itemsThatWereRedrawn) { + if (olvi.HasDecoration) { + foreach (IDecoration d in olvi.Decorations) { + d.ListItem = olvi; + d.SubItem = null; + d.Draw(this, g, contentRectangle); + } + } + foreach (OLVListSubItem subItem in olvi.SubItems) { + if (subItem.HasDecoration) { + foreach (IDecoration d in subItem.Decorations) { + d.ListItem = olvi; + d.SubItem = subItem; + d.Draw(this, g, contentRectangle); + } + } + } + if (this.SelectedRowDecoration != null && olvi.Selected && olvi.Enabled) { + this.SelectedRowDecoration.ListItem = olvi; + this.SelectedRowDecoration.SubItem = null; + this.SelectedRowDecoration.Draw(this, g, contentRectangle); + } + } + + // Now draw the specifically registered decorations + foreach (IDecoration decoration in this.Decorations) { + decoration.ListItem = null; + decoration.SubItem = null; + decoration.Draw(this, g, contentRectangle); + } + + // Finally, draw any hot item decoration + if (this.UseHotItem) { + IDecoration hotItemDecoration = this.HotItemStyleOrDefault.Decoration; + if (hotItemDecoration != null) { + hotItemDecoration.ListItem = this.GetItem(this.HotRowIndex); + if (hotItemDecoration.ListItem == null || hotItemDecoration.ListItem.Enabled) { + hotItemDecoration.SubItem = hotItemDecoration.ListItem == null ? null : hotItemDecoration.ListItem.GetSubItem(this.HotColumnIndex); + hotItemDecoration.Draw(this, g, contentRectangle); + } + } + } + + // If we are in design mode, we don't want to use the glass panels, + // so we draw the background overlays here + if (this.DesignMode) { + foreach (IOverlay overlay in this.Overlays) { + overlay.Draw(this, g, contentRectangle); + } + } + } + + /// + /// Is the given decoration shown on this list + /// + /// The overlay + public virtual bool HasDecoration(IDecoration decoration) { + return this.Decorations.Contains(decoration); + } + + /// + /// Is the given overlay shown on this list? + /// + /// The overlay + public virtual bool HasOverlay(IOverlay overlay) { + return this.Overlays.Contains(overlay); + } + + /// + /// Hide any overlays. + /// + /// + /// This is only a temporary hiding -- the overlays will be shown + /// the next time the ObjectListView redraws. + /// + public virtual void HideOverlays() { + foreach (GlassPanelForm glassPanel in this.glassPanels) { + glassPanel.HideGlass(); + } + } + + /// + /// Create and configure the empty list msg overlay + /// + protected virtual void InitializeEmptyListMsgOverlay() { + TextOverlay overlay = new TextOverlay(); + overlay.Alignment = System.Drawing.ContentAlignment.MiddleCenter; + overlay.TextColor = SystemColors.ControlDarkDark; + overlay.BackColor = Color.BlanchedAlmond; + overlay.BorderColor = SystemColors.ControlDark; + overlay.BorderWidth = 2.0f; + this.EmptyListMsgOverlay = overlay; + } + + /// + /// Initialize the standard image and text overlays + /// + protected virtual void InitializeStandardOverlays() { + this.OverlayImage = new ImageOverlay(); + this.AddOverlay(this.OverlayImage); + this.OverlayText = new TextOverlay(); + this.AddOverlay(this.OverlayText); + } + + /// + /// Make sure that any overlays are visible. + /// + public virtual void ShowOverlays() { + // If we shouldn't show overlays, then don't create glass panels + if (!this.ShouldShowOverlays()) + return; + + // Make sure that each overlay has its own glass panels + if (this.Overlays.Count != this.glassPanels.Count) { + foreach (IOverlay overlay in this.Overlays) { + GlassPanelForm glassPanel = this.FindGlassPanelForOverlay(overlay); + if (glassPanel == null) { + glassPanel = new GlassPanelForm(); + glassPanel.Bind(this, overlay); + this.glassPanels.Add(glassPanel); + } + } + } + foreach (GlassPanelForm glassPanel in this.glassPanels) { + glassPanel.ShowGlass(); + } + } + + private bool ShouldShowOverlays() { + // If we are in design mode, we don’t show the overlays + if (this.DesignMode) + return false; + + // If we are explicitly not using overlays, also don't show them + if (!this.UseOverlays) + return false; + + // If there are no overlays, guess... + if (!this.HasOverlays) + return false; + + // If we don't have 32-bit display, alpha blending doesn't work, so again, no overlays + // TODO: This should actually figure out which screen(s) the control is on, and make sure + // that each one is 32-bit. + if (Screen.PrimaryScreen.BitsPerPixel < 32) + return false; + + // Finally, we can show the overlays + return true; + } + + private GlassPanelForm FindGlassPanelForOverlay(IOverlay overlay) { + return this.glassPanels.Find(delegate(GlassPanelForm x) { return x.Overlay == overlay; }); + } + + /// + /// Refresh the display of the overlays + /// + public virtual void RefreshOverlays() { + foreach (GlassPanelForm glassPanel in this.glassPanels) { + glassPanel.Invalidate(); + } + } + + /// + /// Refresh the display of just one overlays + /// + public virtual void RefreshOverlay(IOverlay overlay) { + GlassPanelForm glassPanel = this.FindGlassPanelForOverlay(overlay); + if (glassPanel != null) + glassPanel.Invalidate(); + } + + /// + /// Remove the given decoration from this list + /// + /// The decoration to remove + public virtual void RemoveDecoration(IDecoration decoration) { + if (decoration == null) + return; + this.Decorations.Remove(decoration); + this.Invalidate(); + } + + /// + /// Remove the given overlay to those on this list + /// + /// The overlay + public virtual void RemoveOverlay(IOverlay overlay) { + if (overlay == null) + return; + this.Overlays.Remove(overlay); + GlassPanelForm glassPanel = this.FindGlassPanelForOverlay(overlay); + if (glassPanel != null) { + this.glassPanels.Remove(glassPanel); + glassPanel.Unbind(); + glassPanel.Dispose(); + } + } + + #endregion + + #region Filtering + + /// + /// Create a filter that will enact all the filtering currently installed + /// on the visible columns. + /// + public virtual IModelFilter CreateColumnFilter() { + List filters = new List(); + foreach (OLVColumn column in this.Columns) { + IModelFilter filter = column.ValueBasedFilter; + if (filter != null) + filters.Add(filter); + } + return (filters.Count == 0) ? null : new CompositeAllFilter(filters); + } + + /// + /// Do the actual work of filtering + /// + /// + /// + /// + /// + protected virtual IEnumerable FilterObjects(IEnumerable originalObjects, IModelFilter aModelFilter, IListFilter aListFilter) { + // Being cautious + originalObjects = originalObjects ?? new ArrayList(); + + // Tell the world to filter the objects. If they do so, don't do anything else +// ReSharper disable PossibleMultipleEnumeration + FilterEventArgs args = new FilterEventArgs(originalObjects); + this.OnFilter(args); + if (args.FilteredObjects != null) + return args.FilteredObjects; + + // Apply a filter to the list as a whole + if (aListFilter != null) + originalObjects = aListFilter.Filter(originalObjects); + + // Apply the object filter if there is one + if (aModelFilter != null) { + ArrayList filteredObjects = new ArrayList(); + foreach (object model in originalObjects) { + if (aModelFilter.Filter(model)) + filteredObjects.Add(model); + } + originalObjects = filteredObjects; + } + + return originalObjects; +// ReSharper restore PossibleMultipleEnumeration + } + + /// + /// Remove all column filtering. + /// + public virtual void ResetColumnFiltering() { + foreach (OLVColumn column in this.Columns) { + column.ValuesChosenForFiltering.Clear(); + } + this.UpdateColumnFiltering(); + } + + /// + /// Update the filtering of this ObjectListView based on the value filtering + /// defined in each column + /// + public virtual void UpdateColumnFiltering() { + //List filters = new List(); + //IModelFilter columnFilter = this.CreateColumnFilter(); + //if (columnFilter != null) + // filters.Add(columnFilter); + //if (this.AdditionalFilter != null) + // filters.Add(this.AdditionalFilter); + //this.ModelFilter = filters.Count == 0 ? null : new CompositeAllFilter(filters); + + if (this.AdditionalFilter == null) + this.ModelFilter = this.CreateColumnFilter(); + else { + IModelFilter columnFilter = this.CreateColumnFilter(); + if (columnFilter == null) + this.ModelFilter = this.AdditionalFilter; + else { + List filters = new List(); + filters.Add(columnFilter); + filters.Add(this.AdditionalFilter); + this.ModelFilter = new CompositeAllFilter(filters); + } + } + } + + /// + /// When some setting related to filtering changes, this method is called. + /// + protected virtual void UpdateFiltering() { + this.BuildList(true); + } + + /// + /// Update all renderers with the currently installed model filter + /// + protected virtual void NotifyNewModelFilter() { + IFilterAwareRenderer filterAware = this.DefaultRenderer as IFilterAwareRenderer; + if (filterAware != null) + filterAware.Filter = this.ModelFilter; + + foreach (OLVColumn column in this.AllColumns) { + filterAware = column.Renderer as IFilterAwareRenderer; + if (filterAware != null) + filterAware.Filter = this.ModelFilter; + } + } + + #endregion + + #region Persistent check state + + /// + /// Gets the checkedness of the given model. + /// + /// The model + /// The checkedness of the model. Defaults to unchecked. + protected virtual CheckState GetPersistentCheckState(object model) { + CheckState state; + if (model != null && this.CheckStateMap.TryGetValue(model, out state)) + return state; + return CheckState.Unchecked; + } + + /// + /// Remember the check state of the given model object + /// + /// The model to be remembered + /// The model's checkedness + /// The state given to the method + protected virtual CheckState SetPersistentCheckState(object model, CheckState state) { + if (model == null) + return CheckState.Unchecked; + + this.CheckStateMap[model] = state; + return state; + } + + /// + /// Forget any persistent checkbox state + /// + protected virtual void ClearPersistentCheckState() { + this.CheckStateMap = null; + } + + #endregion + + #region Implementation variables + + private bool isOwnerOfObjects; // does this ObjectListView own the Objects collection? + private bool hasIdleHandler; // has an Idle handler already been installed? + private bool hasResizeColumnsHandler; // has an idle handler been installed which will handle column resizing? + private bool isInWmPaintEvent; // is a WmPaint event currently being handled? + private bool shouldDoCustomDrawing; // should the list do its custom drawing? + private bool isMarqueSelecting; // Is a marque selection in progress? + private int suspendSelectionEventCount; // How many unmatched SuspendSelectionEvents() calls have been made? + + private readonly List glassPanels = new List(); // The transparent panel that draws overlays + private Dictionary visitedUrlMap = new Dictionary(); // Which urls have been visited? + + // TODO + //private CheckBoxSettings checkBoxSettings = new CheckBoxSettings(); + + #endregion + } +} diff --git a/ObjectListView/ObjectListView.shfb b/ObjectListView/ObjectListView.shfb new file mode 100644 index 00000000..514d58bc --- /dev/null +++ b/ObjectListView/ObjectListView.shfb @@ -0,0 +1,47 @@ + + + + + + + All ObjectListView appears in this namespace + + + ObjectListViewDemo demonstrates helpful techniques when using an ObjectListView + Summary, Parameter, Returns, AutoDocumentCtors, Namespace + InheritedMembers, Protected, SealedProtected + + + .\Help\ + + + True + True + HtmlHelp1x + True + False + 2.0.50727 + True + False + True + False + + ObjectListView Reference + Documentation + en-US + + (c) Copyright 2006-2008 Phillip Piper All Rights Reserved + phillip.piper@gmail.com + + + Local + Msdn + Blank + Prototype + Guid + CSharp + False + AboveNamespaces + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2005.csproj b/ObjectListView/ObjectListView2005.csproj new file mode 100644 index 00000000..02c0c006 --- /dev/null +++ b/ObjectListView/ObjectListView2005.csproj @@ -0,0 +1,182 @@ + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {18FEDA0C-D147-4286-B39A-01204808106A} + Library + Properties + BrightIdeasSoftware + ObjectListView + true + olv-keyfile.snk + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + DEBUG;TRACE + prompt + 4 + false + + + + + + + + + + + + + + Component + + + + + + + + + Component + + + + + + + + + Component + + + True + True + Resources.resx + + + + + Component + + + + Component + + + + + + + Component + + + Component + + + + + Component + + + + + + + Component + + + Component + + + Form + + + ColumnSelectionForm.cs + + + + Form + + + + Code + + + + + + + + Component + + + + + + Component + + + Component + + + + Component + + + + + + + Component + + + + + + + Designer + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ColumnSelectionForm.cs + Designer + + + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2008.csproj b/ObjectListView/ObjectListView2008.csproj new file mode 100644 index 00000000..fb038d21 --- /dev/null +++ b/ObjectListView/ObjectListView2008.csproj @@ -0,0 +1,188 @@ + + + Debug + AnyCPU + 9.0.21022 + 2.0 + {18FEDA0C-D147-4286-B39A-01204808106A} + Library + Properties + BrightIdeasSoftware + ObjectListView + + + + + 2.0 + v2.0 + true + olv-keyfile.snk + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + false + + + + + pdbonly + true + bin\Release\ + DEBUG;TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + Component + + + + + + + + + Component + + + + + + + + + Component + + + True + True + Resources.resx + + + + + Component + + + + Component + + + + + + + Component + + + Component + + + + + Component + + + + + + + Component + + + Component + + + Form + + + ColumnSelectionForm.cs + + + + Form + + + + Code + + + + + + + + Component + + + + + + Component + + + Component + + + + Component + + + + + + + Component + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ColumnSelectionForm.cs + Designer + + + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2008.ncrunchproject b/ObjectListView/ObjectListView2008.ncrunchproject new file mode 100644 index 00000000..17f8118f --- /dev/null +++ b/ObjectListView/ObjectListView2008.ncrunchproject @@ -0,0 +1,16 @@ + + false + false + false + false + false + true + true + false + true + true + 60000 + + + AutoDetect + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2010.csproj b/ObjectListView/ObjectListView2010.csproj new file mode 100644 index 00000000..f478fa79 --- /dev/null +++ b/ObjectListView/ObjectListView2010.csproj @@ -0,0 +1,188 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {18FEDA0C-D147-4286-B39A-01204808106A} + Library + Properties + BrightIdeasSoftware + ObjectListView + + + + + 3.5 + v2.0 + true + olv-keyfile.snk + + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + false + bin\Debug\ObjectListView.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + Component + + + + + + + + Component + + + + + + + + + Component + + + True + True + Resources.resx + + + + + Component + + + + Component + + + + + + + Component + + + Component + + + + + + Component + + + + + + + Component + + + Component + + + Form + + + ColumnSelectionForm.cs + + + + Form + + + + Code + + + + + + + + Component + + + + + + Component + + + Component + + + + Component + + + + + + + Component + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ColumnSelectionForm.cs + Designer + + + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2010.ncrunchproject b/ObjectListView/ObjectListView2010.ncrunchproject new file mode 100644 index 00000000..b4ca6711 --- /dev/null +++ b/ObjectListView/ObjectListView2010.ncrunchproject @@ -0,0 +1,27 @@ + + false + false + false + true + false + false + false + false + true + true + false + true + true + 60000 + + + + AutoDetect + STA + x86 + + + .* + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.csproj b/ObjectListView/ObjectListView2012.csproj new file mode 100644 index 00000000..3ff032a1 --- /dev/null +++ b/ObjectListView/ObjectListView2012.csproj @@ -0,0 +1,194 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {18FEDA0C-D147-4286-B39A-01204808106A} + Library + Properties + BrightIdeasSoftware + ObjectListView + + + + + 3.5 + v2.0 + true + olv-keyfile.snk + + %24/ObjectListView/trunk/ObjectListView + . + https://grammarian.visualstudio.com + {4CA58AB2-18FA-4F8D-95D4-32DDF27D184C} + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 1 + false + bin\Debug\ObjectListView.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + bin\Release\ObjectListView.XML + + + + + + + + + + + + + + Component + + + + + + + + Component + + + + + + + + + Component + + + True + True + Resources.resx + + + + + + + Component + + + + + + + Component + + + Component + + + + + + Component + + + + + + + Component + + + Component + + + Form + + + ColumnSelectionForm.cs + + + + Form + + + + Code + + + + + + + + Component + + + + + + Component + + + Component + + + + Component + + + + + + + Component + + + + + + + Designer + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ColumnSelectionForm.cs + Designer + + + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.ncrunchproject b/ObjectListView/ObjectListView2012.ncrunchproject new file mode 100644 index 00000000..896f2193 --- /dev/null +++ b/ObjectListView/ObjectListView2012.ncrunchproject @@ -0,0 +1,22 @@ + + false + false + false + true + false + false + false + false + true + true + false + true + true + 60000 + + + + AutoDetect + STA + x86 + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.nuspec b/ObjectListView/ObjectListView2012.nuspec new file mode 100644 index 00000000..3e883c68 --- /dev/null +++ b/ObjectListView/ObjectListView2012.nuspec @@ -0,0 +1,22 @@ + + + + ObjectListView.Official + ObjectListView (Official) + 2.9.2-alpha2 + Phillip Piper + Phillip Piper + http://www.gnu.org/licenses/gpl.html + http://objectlistview.sourceforge.net + http://objectlistview.sourceforge.net/cs/_static/index-icon.png + true + ObjectListView is a .NET ListView wired on caffeine, guarana and steroids. + ObjectListView is a .NET ListView wired on caffeine, guarana and steroids. + More calmly, it is a C# wrapper around a .NET ListView, which makes the ListView much easier to use and teaches it lots of neat new tricks. + v2.9.2 Fixed cell edit bounds problem in TreeListView, plus other small issues. + v2.9.1 Added CellRendererGetter to allow each cell to have a different renderer, plus fixes a few small bugs. + v2.9 adds buttons to cells, fixed some formatting bugs, and completely rewrote the demo to be much easier to understand. + Copyright 2006-2016 Bright Ideas Software + .Net WinForms Net20 Net40 ListView Controls + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.sln.DotSettings b/ObjectListView/ObjectListView2012.sln.DotSettings new file mode 100644 index 00000000..a35bee7d --- /dev/null +++ b/ObjectListView/ObjectListView2012.sln.DotSettings @@ -0,0 +1,7 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy> \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.v2.ncrunchproject b/ObjectListView/ObjectListView2012.v2.ncrunchproject new file mode 100644 index 00000000..896f2193 --- /dev/null +++ b/ObjectListView/ObjectListView2012.v2.ncrunchproject @@ -0,0 +1,22 @@ + + false + false + false + true + false + false + false + false + true + true + false + true + true + 60000 + + + + AutoDetect + STA + x86 + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2019.csproj b/ObjectListView/ObjectListView2019.csproj new file mode 100644 index 00000000..2abfb267 --- /dev/null +++ b/ObjectListView/ObjectListView2019.csproj @@ -0,0 +1,54 @@ + + + net5.0-windows + Library + BrightIdeasSoftware + ObjectListView + true + olv-keyfile.snk + %24/ObjectListView/trunk/ObjectListView + . + https://grammarian.visualstudio.com + {4CA58AB2-18FA-4F8D-95D4-32DDF27D184C} + false + true + true + + + 1 + bin\Debug\ObjectListView.XML + + + bin\Release\ObjectListView.XML + + + + + + + + + + + + + + Component + + + Component + + + + + + + Designer + + + + + + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2019.nuspec b/ObjectListView/ObjectListView2019.nuspec new file mode 100644 index 00000000..3e883c68 --- /dev/null +++ b/ObjectListView/ObjectListView2019.nuspec @@ -0,0 +1,22 @@ + + + + ObjectListView.Official + ObjectListView (Official) + 2.9.2-alpha2 + Phillip Piper + Phillip Piper + http://www.gnu.org/licenses/gpl.html + http://objectlistview.sourceforge.net + http://objectlistview.sourceforge.net/cs/_static/index-icon.png + true + ObjectListView is a .NET ListView wired on caffeine, guarana and steroids. + ObjectListView is a .NET ListView wired on caffeine, guarana and steroids. + More calmly, it is a C# wrapper around a .NET ListView, which makes the ListView much easier to use and teaches it lots of neat new tricks. + v2.9.2 Fixed cell edit bounds problem in TreeListView, plus other small issues. + v2.9.1 Added CellRendererGetter to allow each cell to have a different renderer, plus fixes a few small bugs. + v2.9 adds buttons to cells, fixed some formatting bugs, and completely rewrote the demo to be much easier to understand. + Copyright 2006-2016 Bright Ideas Software + .Net WinForms Net20 Net40 ListView Controls + + \ No newline at end of file diff --git a/ObjectListView/Properties/AssemblyInfo.cs b/ObjectListView/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..515899d6 --- /dev/null +++ b/ObjectListView/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ObjectListView")] +[assembly: AssemblyDescription("A much easier to use ListView and friends")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Bright Ideas Software")] +[assembly: AssemblyProduct("ObjectListView")] +[assembly: AssemblyCopyright("Copyright © 2006-2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ef28c7a8-77ae-442d-abc3-bb023fa31e57")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("2.9.1.*")] +[assembly: AssemblyFileVersion("2.9.1.0")] +[assembly: AssemblyInformationalVersion("2.9.1")] +[assembly: System.CLSCompliant(true)] diff --git a/ObjectListView/Properties/Resources.Designer.cs b/ObjectListView/Properties/Resources.Designer.cs new file mode 100644 index 00000000..1b86d072 --- /dev/null +++ b/ObjectListView/Properties/Resources.Designer.cs @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace BrightIdeasSoftware.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BrightIdeasSoftware.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ClearFiltering { + get { + object obj = ResourceManager.GetObject("ClearFiltering", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ColumnFilterIndicator { + get { + object obj = ResourceManager.GetObject("ColumnFilterIndicator", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Filtering { + get { + object obj = ResourceManager.GetObject("Filtering", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap SortAscending { + get { + object obj = ResourceManager.GetObject("SortAscending", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap SortDescending { + get { + object obj = ResourceManager.GetObject("SortDescending", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/ObjectListView/Properties/Resources.resx b/ObjectListView/Properties/Resources.resx new file mode 100644 index 00000000..b017d6ab --- /dev/null +++ b/ObjectListView/Properties/Resources.resx @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\clear-filter.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + + ..\Resources\filter-icons3.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\filter.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\sort-ascending.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\sort-descending.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/ObjectListView/Rendering/Adornments.cs b/ObjectListView/Rendering/Adornments.cs new file mode 100644 index 00000000..a159cfad --- /dev/null +++ b/ObjectListView/Rendering/Adornments.cs @@ -0,0 +1,743 @@ +/* + * Adornments - Adornments are the basis for overlays and decorations -- things that can be rendered over the top of a ListView + * + * Author: Phillip Piper + * Date: 16/08/2009 1:02 AM + * + * Change log: + * v2.6 + * 2012-08-18 JPP - Correctly dispose of brush and pen resources + * v2.3 + * 2009-09-22 JPP - Added Wrap property to TextAdornment, to allow text wrapping to be disabled + * - Added ShrinkToWidth property to ImageAdornment + * 2009-08-17 JPP - Initial version + * + * To do: + * - Use IPointLocator rather than Corners + * - Add RotationCenter property rather than always using middle center + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; + +namespace BrightIdeasSoftware +{ + /// + /// An adornment is the common base for overlays and decorations. + /// + public class GraphicAdornment + { + #region Public properties + + /// + /// Gets or sets the corner of the adornment that will be positioned at the reference corner + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public System.Drawing.ContentAlignment AdornmentCorner { + get { return this.adornmentCorner; } + set { this.adornmentCorner = value; } + } + private System.Drawing.ContentAlignment adornmentCorner = System.Drawing.ContentAlignment.MiddleCenter; + + /// + /// Gets or sets location within the reference rectangle where the adornment will be drawn + /// + /// This is a simplified interface to ReferenceCorner and AdornmentCorner + [Category("ObjectListView"), + Description("How will the adornment be aligned"), + DefaultValue(System.Drawing.ContentAlignment.BottomRight), + NotifyParentProperty(true)] + public System.Drawing.ContentAlignment Alignment { + get { return this.alignment; } + set { + this.alignment = value; + this.ReferenceCorner = value; + this.AdornmentCorner = value; + } + } + private System.Drawing.ContentAlignment alignment = System.Drawing.ContentAlignment.BottomRight; + + /// + /// Gets or sets the offset by which the position of the adornment will be adjusted + /// + [Category("ObjectListView"), + Description("The offset by which the position of the adornment will be adjusted"), + DefaultValue(typeof(Size), "0,0")] + public Size Offset { + get { return this.offset; } + set { this.offset = value; } + } + private Size offset = new Size(); + + /// + /// Gets or sets the point of the reference rectangle to which the adornment will be aligned. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public System.Drawing.ContentAlignment ReferenceCorner { + get { return this.referenceCorner; } + set { this.referenceCorner = value; } + } + private System.Drawing.ContentAlignment referenceCorner = System.Drawing.ContentAlignment.MiddleCenter; + + /// + /// Gets or sets the degree of rotation by which the adornment will be transformed. + /// The centre of rotation will be the center point of the adornment. + /// + [Category("ObjectListView"), + Description("The degree of rotation that will be applied to the adornment."), + DefaultValue(0), + NotifyParentProperty(true)] + public int Rotation { + get { return this.rotation; } + set { this.rotation = value; } + } + private int rotation; + + /// + /// Gets or sets the transparency of the overlay. + /// 0 is completely transparent, 255 is completely opaque. + /// + [Category("ObjectListView"), + Description("The transparency of this adornment. 0 is completely transparent, 255 is completely opaque."), + DefaultValue(128)] + public int Transparency { + get { return this.transparency; } + set { this.transparency = Math.Min(255, Math.Max(0, value)); } + } + private int transparency = 128; + + #endregion + + #region Calculations + + /// + /// Calculate the location of rectangle of the given size, + /// so that it's indicated corner would be at the given point. + /// + /// The point + /// + /// Which corner will be positioned at the reference point + /// + /// CalculateAlignedPosition(new Point(50, 100), new Size(10, 20), System.Drawing.ContentAlignment.TopLeft) -> Point(50, 100) + /// CalculateAlignedPosition(new Point(50, 100), new Size(10, 20), System.Drawing.ContentAlignment.MiddleCenter) -> Point(45, 90) + /// CalculateAlignedPosition(new Point(50, 100), new Size(10, 20), System.Drawing.ContentAlignment.BottomRight) -> Point(40, 80) + public virtual Point CalculateAlignedPosition(Point pt, Size size, System.Drawing.ContentAlignment corner) { + switch (corner) { + case System.Drawing.ContentAlignment.TopLeft: + return pt; + case System.Drawing.ContentAlignment.TopCenter: + return new Point(pt.X - (size.Width / 2), pt.Y); + case System.Drawing.ContentAlignment.TopRight: + return new Point(pt.X - size.Width, pt.Y); + case System.Drawing.ContentAlignment.MiddleLeft: + return new Point(pt.X, pt.Y - (size.Height / 2)); + case System.Drawing.ContentAlignment.MiddleCenter: + return new Point(pt.X - (size.Width / 2), pt.Y - (size.Height / 2)); + case System.Drawing.ContentAlignment.MiddleRight: + return new Point(pt.X - size.Width, pt.Y - (size.Height / 2)); + case System.Drawing.ContentAlignment.BottomLeft: + return new Point(pt.X, pt.Y - size.Height); + case System.Drawing.ContentAlignment.BottomCenter: + return new Point(pt.X - (size.Width / 2), pt.Y - size.Height); + case System.Drawing.ContentAlignment.BottomRight: + return new Point(pt.X - size.Width, pt.Y - size.Height); + } + + // Should never reach here + return pt; + } + + /// + /// Calculate a rectangle that has the given size which is positioned so that + /// its alignment point is at the reference location of the given rect. + /// + /// + /// + /// + public virtual Rectangle CreateAlignedRectangle(Rectangle r, Size sz) { + return this.CreateAlignedRectangle(r, sz, this.ReferenceCorner, this.AdornmentCorner, this.Offset); + } + + /// + /// Create a rectangle of the given size which is positioned so that + /// its indicated corner is at the indicated corner of the reference rect. + /// + /// + /// + /// + /// + /// + /// + /// + /// Creates a rectangle so that its bottom left is at the centre of the reference: + /// corner=BottomLeft, referenceCorner=MiddleCenter + /// This is a powerful concept that takes some getting used to, but is + /// very neat once you understand it. + /// + public virtual Rectangle CreateAlignedRectangle(Rectangle r, Size sz, + System.Drawing.ContentAlignment corner, System.Drawing.ContentAlignment referenceCorner, Size offset) { + Point referencePt = this.CalculateCorner(r, referenceCorner); + Point topLeft = this.CalculateAlignedPosition(referencePt, sz, corner); + return new Rectangle(topLeft + offset, sz); + } + + /// + /// Return the point at the indicated corner of the given rectangle (it doesn't + /// have to be a corner, but a named location) + /// + /// The reference rectangle + /// Which point of the rectangle should be returned? + /// A point + /// CalculateReferenceLocation(new Rectangle(0, 0, 50, 100), System.Drawing.ContentAlignment.TopLeft) -> Point(0, 0) + /// CalculateReferenceLocation(new Rectangle(0, 0, 50, 100), System.Drawing.ContentAlignment.MiddleCenter) -> Point(25, 50) + /// CalculateReferenceLocation(new Rectangle(0, 0, 50, 100), System.Drawing.ContentAlignment.BottomRight) -> Point(50, 100) + public virtual Point CalculateCorner(Rectangle r, System.Drawing.ContentAlignment corner) { + switch (corner) { + case System.Drawing.ContentAlignment.TopLeft: + return new Point(r.Left, r.Top); + case System.Drawing.ContentAlignment.TopCenter: + return new Point(r.X + (r.Width / 2), r.Top); + case System.Drawing.ContentAlignment.TopRight: + return new Point(r.Right, r.Top); + case System.Drawing.ContentAlignment.MiddleLeft: + return new Point(r.Left, r.Top + (r.Height / 2)); + case System.Drawing.ContentAlignment.MiddleCenter: + return new Point(r.X + (r.Width / 2), r.Top + (r.Height / 2)); + case System.Drawing.ContentAlignment.MiddleRight: + return new Point(r.Right, r.Top + (r.Height / 2)); + case System.Drawing.ContentAlignment.BottomLeft: + return new Point(r.Left, r.Bottom); + case System.Drawing.ContentAlignment.BottomCenter: + return new Point(r.X + (r.Width / 2), r.Bottom); + case System.Drawing.ContentAlignment.BottomRight: + return new Point(r.Right, r.Bottom); + } + + // Should never reach here + return r.Location; + } + + /// + /// Given the item and the subitem, calculate its bounds. + /// + /// + /// + /// + public virtual Rectangle CalculateItemBounds(OLVListItem item, OLVListSubItem subItem) { + if (item == null) + return Rectangle.Empty; + + if (subItem == null) + return item.Bounds; + + return item.GetSubItemBounds(item.SubItems.IndexOf(subItem)); + } + + #endregion + + #region Commands + + /// + /// Apply any specified rotation to the Graphic content. + /// + /// The Graphics to be transformed + /// The rotation will be around the centre of this rect + protected virtual void ApplyRotation(Graphics g, Rectangle r) { + if (this.Rotation == 0) + return; + + // THINK: Do we want to reset the transform? I think we want to push a new transform + g.ResetTransform(); + Matrix m = new Matrix(); + m.RotateAt(this.Rotation, new Point(r.Left + r.Width / 2, r.Top + r.Height / 2)); + g.Transform = m; + } + + /// + /// Reverse the rotation created by ApplyRotation() + /// + /// + protected virtual void UnapplyRotation(Graphics g) { + if (this.Rotation != 0) + g.ResetTransform(); + } + + #endregion + } + + /// + /// An overlay that will draw an image over the top of the ObjectListView + /// + public class ImageAdornment : GraphicAdornment + { + #region Public properties + + /// + /// Gets or sets the image that will be drawn + /// + [Category("ObjectListView"), + Description("The image that will be drawn"), + DefaultValue(null), + NotifyParentProperty(true)] + public Image Image { + get { return this.image; } + set { this.image = value; } + } + private Image image; + + /// + /// Gets or sets if the image will be shrunk to fit with its horizontal bounds + /// + [Category("ObjectListView"), + Description("Will the image be shrunk to fit within its width?"), + DefaultValue(false)] + public bool ShrinkToWidth { + get { return this.shrinkToWidth; } + set { this.shrinkToWidth = value; } + } + private bool shrinkToWidth; + + #endregion + + #region Commands + + /// + /// Draw the image in its specified location + /// + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void DrawImage(Graphics g, Rectangle r) { + if (this.ShrinkToWidth) + this.DrawScaledImage(g, r, this.Image, this.Transparency); + else + this.DrawImage(g, r, this.Image, this.Transparency); + } + + /// + /// Draw the image in its specified location + /// + /// The image to be drawn + /// The Graphics used for drawing + /// The bounds of the rendering + /// How transparent should the image be (0 is completely transparent, 255 is opaque) + public virtual void DrawImage(Graphics g, Rectangle r, Image image, int transparency) { + if (image != null) + this.DrawImage(g, r, image, image.Size, transparency); + } + + /// + /// Draw the image in its specified location + /// + /// The image to be drawn + /// The Graphics used for drawing + /// The bounds of the rendering + /// How big should the image be? + /// How transparent should the image be (0 is completely transparent, 255 is opaque) + public virtual void DrawImage(Graphics g, Rectangle r, Image image, Size sz, int transparency) { + if (image == null) + return; + + Rectangle adornmentBounds = this.CreateAlignedRectangle(r, sz); + try { + this.ApplyRotation(g, adornmentBounds); + this.DrawTransparentBitmap(g, adornmentBounds, image, transparency); + } + finally { + this.UnapplyRotation(g); + } + } + + /// + /// Draw the image in its specified location, scaled so that it is not wider + /// than the given rectangle. Height is scaled proportional to the width. + /// + /// The image to be drawn + /// The Graphics used for drawing + /// The bounds of the rendering + /// How transparent should the image be (0 is completely transparent, 255 is opaque) + public virtual void DrawScaledImage(Graphics g, Rectangle r, Image image, int transparency) { + if (image == null) + return; + + // If the image is too wide to be drawn in the space provided, proportionally scale it down. + // Too tall images are not scaled. + Size size = image.Size; + if (image.Width > r.Width) { + float scaleRatio = (float)r.Width / (float)image.Width; + size.Height = (int)((float)image.Height * scaleRatio); + size.Width = r.Width - 1; + } + + this.DrawImage(g, r, image, size, transparency); + } + + /// + /// Utility to draw a bitmap transparently. + /// + /// + /// + /// + /// + protected virtual void DrawTransparentBitmap(Graphics g, Rectangle r, Image image, int transparency) { + ImageAttributes imageAttributes = null; + if (transparency != 255) { + imageAttributes = new ImageAttributes(); + float a = (float)transparency / 255.0f; + float[][] colorMatrixElements = { + new float[] {1, 0, 0, 0, 0}, + new float[] {0, 1, 0, 0, 0}, + new float[] {0, 0, 1, 0, 0}, + new float[] {0, 0, 0, a, 0}, + new float[] {0, 0, 0, 0, 1}}; + + imageAttributes.SetColorMatrix(new ColorMatrix(colorMatrixElements)); + } + + g.DrawImage(image, + r, // destination rectangle + 0, 0, image.Size.Width, image.Size.Height, // source rectangle + GraphicsUnit.Pixel, + imageAttributes); + } + + #endregion + } + + /// + /// An adornment that will draw text + /// + public class TextAdornment : GraphicAdornment + { + #region Public properties + + /// + /// Gets or sets the background color of the text + /// Set this to Color.Empty to not draw a background + /// + [Category("ObjectListView"), + Description("The background color of the text"), + DefaultValue(typeof(Color), "")] + public Color BackColor { + get { return this.backColor; } + set { this.backColor = value; } + } + private Color backColor = Color.Empty; + + /// + /// Gets the brush that will be used to paint the text + /// + [Browsable(false)] + public Brush BackgroundBrush { + get { + return new SolidBrush(Color.FromArgb(this.workingTransparency, this.BackColor)); + } + } + + /// + /// Gets or sets the color of the border around the billboard. + /// Set this to Color.Empty to remove the border + /// + [Category("ObjectListView"), + Description("The color of the border around the text"), + DefaultValue(typeof(Color), "")] + public Color BorderColor { + get { return this.borderColor; } + set { this.borderColor = value; } + } + private Color borderColor = Color.Empty; + + /// + /// Gets the brush that will be used to paint the text + /// + [Browsable(false)] + public Pen BorderPen { + get { + return new Pen(Color.FromArgb(this.workingTransparency, this.BorderColor), this.BorderWidth); + } + } + + /// + /// Gets or sets the width of the border around the text + /// + [Category("ObjectListView"), + Description("The width of the border around the text"), + DefaultValue(0.0f)] + public float BorderWidth { + get { return this.borderWidth; } + set { this.borderWidth = value; } + } + private float borderWidth; + + /// + /// How rounded should the corners of the border be? 0 means no rounding. + /// + /// If this value is too large, the edges of the border will appear odd. + [Category("ObjectListView"), + Description("How rounded should the corners of the border be? 0 means no rounding."), + DefaultValue(16.0f), + NotifyParentProperty(true)] + public float CornerRounding { + get { return this.cornerRounding; } + set { this.cornerRounding = value; } + } + private float cornerRounding = 16.0f; + + /// + /// Gets or sets the font that will be used to draw the text + /// + [Category("ObjectListView"), + Description("The font that will be used to draw the text"), + DefaultValue(null), + NotifyParentProperty(true)] + public Font Font { + get { return this.font; } + set { this.font = value; } + } + private Font font; + + /// + /// Gets the font that will be used to draw the text or a reasonable default + /// + [Browsable(false)] + public Font FontOrDefault { + get { + return this.Font ?? new Font("Tahoma", 16); + } + } + + /// + /// Does this text have a background? + /// + [Browsable(false)] + public bool HasBackground { + get { + return this.BackColor != Color.Empty; + } + } + + /// + /// Does this overlay have a border? + /// + [Browsable(false)] + public bool HasBorder { + get { + return this.BorderColor != Color.Empty && this.BorderWidth > 0; + } + } + + /// + /// Gets or sets the maximum width of the text. Text longer than this will wrap. + /// 0 means no maximum. + /// + [Category("ObjectListView"), + Description("The maximum width the text (0 means no maximum). Text longer than this will wrap"), + DefaultValue(0)] + public int MaximumTextWidth { + get { return this.maximumTextWidth; } + set { this.maximumTextWidth = value; } + } + private int maximumTextWidth; + + /// + /// Gets or sets the formatting that should be used on the text + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual StringFormat StringFormat { + get { + if (this.stringFormat == null) { + this.stringFormat = new StringFormat(); + this.stringFormat.Alignment = StringAlignment.Center; + this.stringFormat.LineAlignment = StringAlignment.Center; + this.stringFormat.Trimming = StringTrimming.EllipsisCharacter; + if (!this.Wrap) + this.stringFormat.FormatFlags = StringFormatFlags.NoWrap; + } + return this.stringFormat; + } + set { this.stringFormat = value; } + } + private StringFormat stringFormat; + + /// + /// Gets or sets the text that will be drawn + /// + [Category("ObjectListView"), + Description("The text that will be drawn over the top of the ListView"), + DefaultValue(null), + NotifyParentProperty(true), + Localizable(true)] + public string Text { + get { return this.text; } + set { this.text = value; } + } + private string text; + + /// + /// Gets the brush that will be used to paint the text + /// + [Browsable(false)] + public Brush TextBrush { + get { + return new SolidBrush(Color.FromArgb(this.workingTransparency, this.TextColor)); + } + } + + /// + /// Gets or sets the color of the text + /// + [Category("ObjectListView"), + Description("The color of the text"), + DefaultValue(typeof(Color), "DarkBlue"), + NotifyParentProperty(true)] + public Color TextColor { + get { return this.textColor; } + set { this.textColor = value; } + } + private Color textColor = Color.DarkBlue; + + /// + /// Gets or sets whether the text will wrap when it exceeds its bounds + /// + [Category("ObjectListView"), + Description("Will the text wrap?"), + DefaultValue(true)] + public bool Wrap { + get { return this.wrap; } + set { this.wrap = value; } + } + private bool wrap = true; + + #endregion + + #region Implementation + + /// + /// Draw our text with our stored configuration in relation to the given + /// reference rectangle + /// + /// The Graphics used for drawing + /// The reference rectangle in relation to which the text will be drawn + public virtual void DrawText(Graphics g, Rectangle r) { + this.DrawText(g, r, this.Text, this.Transparency); + } + + /// + /// Draw the given text with our stored configuration + /// + /// The Graphics used for drawing + /// The reference rectangle in relation to which the text will be drawn + /// The text to draw + /// How opaque should be text be + public virtual void DrawText(Graphics g, Rectangle r, string s, int transparency) { + if (String.IsNullOrEmpty(s)) + return; + + Rectangle textRect = this.CalculateTextBounds(g, r, s); + this.DrawBorderedText(g, textRect, s, transparency); + } + + /// + /// Draw the text with a border + /// + /// The Graphics used for drawing + /// The bounds within which the text should be drawn + /// The text to draw + /// How opaque should be text be + protected virtual void DrawBorderedText(Graphics g, Rectangle textRect, string text, int transparency) { + Rectangle borderRect = textRect; + borderRect.Inflate((int)this.BorderWidth / 2, (int)this.BorderWidth / 2); + borderRect.Y -= 1; // Looker better a little higher + + try { + this.ApplyRotation(g, textRect); + using (GraphicsPath path = this.GetRoundedRect(borderRect, this.CornerRounding)) { + this.workingTransparency = transparency; + if (this.HasBackground) { + using (Brush b = this.BackgroundBrush) + g.FillPath(b, path); + } + + using (Brush b = this.TextBrush) + g.DrawString(text, this.FontOrDefault, b, textRect, this.StringFormat); + + if (this.HasBorder) { + using (Pen p = this.BorderPen) + g.DrawPath(p, path); + } + } + } + finally { + this.UnapplyRotation(g); + } + } + + /// + /// Return the rectangle that will be the precise bounds of the displayed text + /// + /// + /// + /// + /// The bounds of the text + protected virtual Rectangle CalculateTextBounds(Graphics g, Rectangle r, string s) { + int maxWidth = this.MaximumTextWidth <= 0 ? r.Width : this.MaximumTextWidth; + SizeF sizeF = g.MeasureString(s, this.FontOrDefault, maxWidth, this.StringFormat); + Size size = new Size(1 + (int)sizeF.Width, 1 + (int)sizeF.Height); + return this.CreateAlignedRectangle(r, size); + } + + /// + /// Return a GraphicPath that is a round cornered rectangle + /// + /// The rectangle + /// The diameter of the corners + /// A round cornered rectangle path + /// If I could rely on people using C# 3.0+, this should be + /// an extension method of GraphicsPath. + protected virtual GraphicsPath GetRoundedRect(Rectangle rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + if (diameter > 0) { + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + } else { + path.AddRectangle(rect); + } + + return path; + } + + #endregion + + private int workingTransparency; + } +} diff --git a/ObjectListView/Rendering/Decorations.cs b/ObjectListView/Rendering/Decorations.cs new file mode 100644 index 00000000..91f21d69 --- /dev/null +++ b/ObjectListView/Rendering/Decorations.cs @@ -0,0 +1,973 @@ +/* + * Decorations - Images, text or other things that can be rendered onto an ObjectListView + * + * Author: Phillip Piper + * Date: 19/08/2009 10:56 PM + * + * Change log: + * 2018-04-30 JPP - Added ColumnEdgeDecoration. + * TintedColumnDecoration now uses common base class, ColumnDecoration. + * v2.5 + * 2011-04-04 JPP - Added ability to have a gradient background on BorderDecoration + * v2.4 + * 2010-04-15 JPP - Tweaked LightBoxDecoration a little + * v2.3 + * 2009-09-23 JPP - Added LeftColumn and RightColumn to RowBorderDecoration + * 2009-08-23 JPP - Added LightBoxDecoration + * 2009-08-19 JPP - Initial version. Separated from Overlays.cs + * + * To do: + * + * Copyright (C) 2009-2018 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A decoration is an overlay that draws itself in relation to a given row or cell. + /// Decorations scroll when the listview scrolls. + /// + public interface IDecoration : IOverlay + { + /// + /// Gets or sets the row that is to be decorated + /// + OLVListItem ListItem { get; set; } + + /// + /// Gets or sets the subitem that is to be decorated + /// + OLVListSubItem SubItem { get; set; } + } + + /// + /// An AbstractDecoration is a safe do-nothing implementation of the IDecoration interface + /// + public class AbstractDecoration : IDecoration + { + #region IDecoration Members + + /// + /// Gets or sets the row that is to be decorated + /// + public OLVListItem ListItem { + get { return listItem; } + set { listItem = value; } + } + private OLVListItem listItem; + + /// + /// Gets or sets the subitem that is to be decorated + /// + public OLVListSubItem SubItem { + get { return subItem; } + set { subItem = value; } + } + private OLVListSubItem subItem; + + #endregion + + #region Public properties + + /// + /// Gets the bounds of the decorations row + /// + public Rectangle RowBounds { + get { + if (this.ListItem == null) + return Rectangle.Empty; + else + return this.ListItem.Bounds; + } + } + + /// + /// Get the bounds of the decorations cell + /// + public Rectangle CellBounds { + get { + if (this.ListItem == null || this.SubItem == null) + return Rectangle.Empty; + else + return this.ListItem.GetSubItemBounds(this.ListItem.SubItems.IndexOf(this.SubItem)); + } + } + + #endregion + + #region IOverlay Members + + /// + /// Draw the decoration + /// + /// + /// + /// + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + } + + #endregion + } + + + /// + /// This decoration draws something over a given column. + /// Subclasses must override DrawDecoration() + /// + public class ColumnDecoration : AbstractDecoration { + #region Constructors + + /// + /// Create a ColumnDecoration + /// + public ColumnDecoration() { + } + + /// + /// Create a ColumnDecoration + /// + /// + public ColumnDecoration(OLVColumn column) + : this() { + this.ColumnToDecorate = column ?? throw new ArgumentNullException("column"); + } + + #endregion + + #region Properties + + /// + /// Gets or sets the column that will be decorated + /// + public OLVColumn ColumnToDecorate { + get { return this.columnToDecorate; } + set { this.columnToDecorate = value; } + } + private OLVColumn columnToDecorate; + + /// + /// Gets or sets the pen that will be used to draw the column decoration + /// + public Pen Pen { + get { + return this.pen ?? Pens.DarkSlateBlue; + } + set { + if (this.pen == value) + return; + + if (this.pen != null) { + this.pen.Dispose(); + } + + this.pen = value; + } + } + private Pen pen; + + #endregion + + #region IOverlay Members + + /// + /// Draw a decoration over our column + /// + /// + /// This overlay only works when: + /// - the list is in Details view + /// - there is at least one row + /// - there is a selected column (or a specified tint column) + /// + /// + /// + /// + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + + if (olv.View != System.Windows.Forms.View.Details) + return; + + if (olv.GetItemCount() == 0) + return; + + if (this.ColumnToDecorate == null) + return; + + Point sides = NativeMethods.GetScrolledColumnSides(olv, this.ColumnToDecorate.Index); + if (sides.X == -1) + return; + + Rectangle columnBounds = new Rectangle(sides.X, r.Top, sides.Y - sides.X, r.Bottom); + + // Find the bottom of the last item. The decoration should extend only to there. + OLVListItem lastItem = olv.GetLastItemInDisplayOrder(); + if (lastItem != null) { + Rectangle lastItemBounds = lastItem.Bounds; + if (!lastItemBounds.IsEmpty && lastItemBounds.Bottom < columnBounds.Bottom) + columnBounds.Height = lastItemBounds.Bottom - columnBounds.Top; + } + + // Delegate the drawing of the actual decoration + this.DrawDecoration(olv, g, r, columnBounds); + } + + /// + /// Subclasses should override this to draw exactly what they want + /// + /// + /// + /// + /// + public virtual void DrawDecoration(ObjectListView olv, Graphics g, Rectangle r, Rectangle columnBounds) { + g.DrawRectangle(this.Pen, columnBounds); + } + + #endregion + } + + /// + /// This decoration draws a slight tint over a column of the + /// owning listview. If no column is explicitly set, the selected + /// column in the listview will be used. + /// The selected column is normally the sort column, but does not have to be. + /// + public class TintedColumnDecoration : ColumnDecoration { + #region Constructors + + /// + /// Create a TintedColumnDecoration + /// + public TintedColumnDecoration() { + this.Tint = Color.FromArgb(15, Color.Blue); + } + + /// + /// Create a TintedColumnDecoration + /// + /// + public TintedColumnDecoration(OLVColumn column) + : this() { + this.ColumnToDecorate = column; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the color that will be 'tinted' over the selected column + /// + public Color Tint { + get { return this.tint; } + set { + if (this.tint == value) + return; + + if (this.tintBrush != null) { + this.tintBrush.Dispose(); + this.tintBrush = null; + } + + this.tint = value; + this.tintBrush = new SolidBrush(this.tint); + } + } + private Color tint; + private SolidBrush tintBrush; + + #endregion + + #region IOverlay Members + + public override void DrawDecoration(ObjectListView olv, Graphics g, Rectangle r, Rectangle columnBounds) { + g.FillRectangle(this.tintBrush, columnBounds); + } + + #endregion + } + + /// + /// Specify on which side edge the decoration will be drawn + /// + public enum ColumnEdge { + Left, + Right + } + + /// + /// This decoration draws a line on the edge(s) of its given column + /// + /// + /// Like all decorations, this draws over the contents of list view. + /// If you set the Pen too wide enough, you may overwrite the contents + /// of the column (if alignment is Inside) or the surrounding columns (if alignment is Outside) + /// + public class ColumnEdgeDecoration : ColumnDecoration { + #region Constructors + + /// + /// Create a ColumnEdgeDecoration + /// + public ColumnEdgeDecoration() { + } + + /// + /// Create a ColumnEdgeDecoration which draws a line over the right edges of the column (by default) + /// + /// + /// + /// + /// + public ColumnEdgeDecoration(OLVColumn column, Pen pen = null, ColumnEdge edge = ColumnEdge.Right, float xOffset = 0) + : this() { + this.ColumnToDecorate = column; + this.Pen = pen; + this.Edge = edge; + this.XOffset = xOffset; + } + + #endregion + + #region Properties + + /// + /// Gets or sets whether this decoration will draw a line on the left or right edge of the column + /// + public ColumnEdge Edge { + get { return edge; } + set { edge = value; } + } + private ColumnEdge edge = ColumnEdge.Right; + + /// + /// Gets or sets the horizontal offset from centered at which the line will be drawn + /// + public float XOffset { + get { return xOffset; } + set { xOffset = value; } + } + private float xOffset; + + #endregion + + #region IOverlay Members + + public override void DrawDecoration(ObjectListView olv, Graphics g, Rectangle r, Rectangle columnBounds) { + float left = CalculateEdge(columnBounds); + g.DrawLine(this.Pen, left, columnBounds.Top, left, columnBounds.Bottom); + + } + + private float CalculateEdge(Rectangle columnBounds) { + float tweak = this.XOffset + (this.Pen.Width <= 2 ? 0 : 1); + int x = this.Edge == ColumnEdge.Left ? columnBounds.Left : columnBounds.Right; + return tweak + x - this.Pen.Width / 2; + } + + #endregion + } + + /// + /// This decoration draws an optionally filled border around a rectangle. + /// Subclasses must override CalculateBounds(). + /// + public class BorderDecoration : AbstractDecoration + { + #region Constructors + + /// + /// Create a BorderDecoration + /// + public BorderDecoration() + : this(new Pen(Color.FromArgb(64, Color.Blue), 1)) { + } + + /// + /// Create a BorderDecoration + /// + /// The pen used to draw the border + public BorderDecoration(Pen borderPen) { + this.BorderPen = borderPen; + } + + /// + /// Create a BorderDecoration + /// + /// The pen used to draw the border + /// The brush used to fill the rectangle + public BorderDecoration(Pen borderPen, Brush fill) { + this.BorderPen = borderPen; + this.FillBrush = fill; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the pen that will be used to draw the border + /// + public Pen BorderPen { + get { return this.borderPen; } + set { this.borderPen = value; } + } + private Pen borderPen; + + /// + /// Gets or sets the padding that will be added to the bounds of the item + /// before drawing the border and fill. + /// + public Size BoundsPadding { + get { return this.boundsPadding; } + set { this.boundsPadding = value; } + } + private Size boundsPadding = new Size(-1, 2); + + /// + /// How rounded should the corners of the border be? 0 means no rounding. + /// + /// If this value is too large, the edges of the border will appear odd. + public float CornerRounding { + get { return this.cornerRounding; } + set { this.cornerRounding = value; } + } + private float cornerRounding = 16.0f; + + /// + /// Gets or sets the brush that will be used to fill the border + /// + /// This value is ignored when using gradient brush + public Brush FillBrush { + get { return this.fillBrush; } + set { this.fillBrush = value; } + } + private Brush fillBrush = new SolidBrush(Color.FromArgb(64, Color.Blue)); + + /// + /// Gets or sets the color that will be used as the start of a gradient fill. + /// + /// This and FillGradientTo must be given value to show a gradient + public Color? FillGradientFrom { + get { return this.fillGradientFrom; } + set { this.fillGradientFrom = value; } + } + private Color? fillGradientFrom; + + /// + /// Gets or sets the color that will be used as the end of a gradient fill. + /// + /// This and FillGradientFrom must be given value to show a gradient + public Color? FillGradientTo { + get { return this.fillGradientTo; } + set { this.fillGradientTo = value; } + } + private Color? fillGradientTo; + + /// + /// Gets or sets the fill mode that will be used for the gradient. + /// + public LinearGradientMode FillGradientMode { + get { return this.fillGradientMode; } + set { this.fillGradientMode = value; } + } + private LinearGradientMode fillGradientMode = LinearGradientMode.Vertical; + + #endregion + + #region IOverlay Members + + /// + /// Draw a filled border + /// + /// + /// + /// + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + Rectangle bounds = this.CalculateBounds(); + if (!bounds.IsEmpty) + this.DrawFilledBorder(g, bounds); + } + + #endregion + + #region Subclass responsibility + + /// + /// Subclasses should override this to say where the border should be drawn + /// + /// + protected virtual Rectangle CalculateBounds() { + return Rectangle.Empty; + } + + #endregion + + #region Implementation utilities + + /// + /// Do the actual work of drawing the filled border + /// + /// + /// + protected void DrawFilledBorder(Graphics g, Rectangle bounds) { + bounds.Inflate(this.BoundsPadding); + GraphicsPath path = this.GetRoundedRect(bounds, this.CornerRounding); + if (this.FillGradientFrom != null && this.FillGradientTo != null) { + if (this.FillBrush != null) + this.FillBrush.Dispose(); + this.FillBrush = new LinearGradientBrush(bounds, this.FillGradientFrom.Value, this.FillGradientTo.Value, this.FillGradientMode); + } + if (this.FillBrush != null) + g.FillPath(this.FillBrush, path); + if (this.BorderPen != null) + g.DrawPath(this.BorderPen, path); + } + + /// + /// Create a GraphicsPath that represents a round cornered rectangle. + /// + /// + /// If this is 0 or less, the rectangle will not be rounded. + /// + protected GraphicsPath GetRoundedRect(RectangleF rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + if (diameter <= 0.0f) { + path.AddRectangle(rect); + } else { + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + } + + return path; + } + + #endregion + } + + /// + /// Instances of this class draw a border around the decorated row + /// + public class RowBorderDecoration : BorderDecoration + { + /// + /// Gets or sets the index of the left most column to be used for the border + /// + public int LeftColumn { + get { return leftColumn; } + set { leftColumn = value; } + } + private int leftColumn = -1; + + /// + /// Gets or sets the index of the right most column to be used for the border + /// + public int RightColumn { + get { return rightColumn; } + set { rightColumn = value; } + } + private int rightColumn = -1; + + /// + /// Calculate the boundaries of the border + /// + /// + protected override Rectangle CalculateBounds() { + Rectangle bounds = this.RowBounds; + if (this.ListItem == null) + return bounds; + + if (this.LeftColumn >= 0) { + Rectangle leftCellBounds = this.ListItem.GetSubItemBounds(this.LeftColumn); + if (!leftCellBounds.IsEmpty) { + bounds.Width = bounds.Right - leftCellBounds.Left; + bounds.X = leftCellBounds.Left; + } + } + + if (this.RightColumn >= 0) { + Rectangle rightCellBounds = this.ListItem.GetSubItemBounds(this.RightColumn); + if (!rightCellBounds.IsEmpty) { + bounds.Width = rightCellBounds.Right - bounds.Left; + } + } + + return bounds; + } + } + + /// + /// Instances of this class draw a border around the decorated subitem. + /// + public class CellBorderDecoration : BorderDecoration + { + /// + /// Calculate the boundaries of the border + /// + /// + protected override Rectangle CalculateBounds() { + return this.CellBounds; + } + } + + /// + /// This decoration puts a border around the cell being edited and + /// optionally "lightboxes" the cell (makes the rest of the control dark). + /// + public class EditingCellBorderDecoration : BorderDecoration + { + #region Life and death + + /// + /// Create a EditingCellBorderDecoration + /// + public EditingCellBorderDecoration() { + this.FillBrush = null; + this.BorderPen = new Pen(Color.DarkBlue, 2); + this.CornerRounding = 8; + this.BoundsPadding = new Size(10, 8); + + } + + /// + /// Create a EditingCellBorderDecoration + /// + /// Should the decoration use a lighbox display style? + public EditingCellBorderDecoration(bool useLightBox) : this() + { + this.UseLightbox = useLightbox; + } + + #endregion + + #region Configuration properties + + /// + /// Gets or set whether the decoration should make the rest of + /// the control dark when a cell is being edited + /// + /// If this is true, FillBrush is used to overpaint + /// the control. + public bool UseLightbox { + get { return this.useLightbox; } + set { + if (this.useLightbox == value) + return; + this.useLightbox = value; + if (this.useLightbox) { + if (this.FillBrush == null) + this.FillBrush = new SolidBrush(Color.FromArgb(64, Color.Black)); + } + } + } + private bool useLightbox; + + #endregion + + #region Implementation + + /// + /// Draw the decoration + /// + /// + /// + /// + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + if (!olv.IsCellEditing) + return; + + Rectangle bounds = olv.CellEditor.Bounds; + if (bounds.IsEmpty) + return; + + bounds.Inflate(this.BoundsPadding); + GraphicsPath path = this.GetRoundedRect(bounds, this.CornerRounding); + if (this.FillBrush != null) { + if (this.UseLightbox) { + using (Region newClip = new Region(r)) { + newClip.Exclude(path); + Region originalClip = g.Clip; + g.Clip = newClip; + g.FillRectangle(this.FillBrush, r); + g.Clip = originalClip; + } + } else { + g.FillPath(this.FillBrush, path); + } + } + if (this.BorderPen != null) + g.DrawPath(this.BorderPen, path); + } + + #endregion + } + + /// + /// This decoration causes everything *except* the row under the mouse to be overpainted + /// with a tint, making the row under the mouse stand out in comparison. + /// The darker and more opaque the fill color, the more obvious the + /// decorated row becomes. + /// + public class LightBoxDecoration : BorderDecoration + { + /// + /// Create a LightBoxDecoration + /// + public LightBoxDecoration() { + this.BoundsPadding = new Size(-1, 4); + this.CornerRounding = 8.0f; + this.FillBrush = new SolidBrush(Color.FromArgb(72, Color.Black)); + } + + /// + /// Draw a tint over everything in the ObjectListView except the + /// row under the mouse. + /// + /// + /// + /// + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + if (!r.Contains(olv.PointToClient(Cursor.Position))) + return; + + Rectangle bounds = this.RowBounds; + if (bounds.IsEmpty) { + if (olv.View == View.Tile) + g.FillRectangle(this.FillBrush, r); + return; + } + + using (Region newClip = new Region(r)) { + bounds.Inflate(this.BoundsPadding); + newClip.Exclude(this.GetRoundedRect(bounds, this.CornerRounding)); + Region originalClip = g.Clip; + g.Clip = newClip; + g.FillRectangle(this.FillBrush, r); + g.Clip = originalClip; + } + } + } + + /// + /// Instances of this class put an Image over the row/cell that it is decorating + /// + public class ImageDecoration : ImageAdornment, IDecoration + { + #region Constructors + + /// + /// Create an image decoration + /// + public ImageDecoration() { + this.Alignment = ContentAlignment.MiddleRight; + } + + /// + /// Create an image decoration + /// + /// + public ImageDecoration(Image image) + : this() { + this.Image = image; + } + + /// + /// Create an image decoration + /// + /// + /// + public ImageDecoration(Image image, int transparency) + : this() { + this.Image = image; + this.Transparency = transparency; + } + + /// + /// Create an image decoration + /// + /// + /// + public ImageDecoration(Image image, ContentAlignment alignment) + : this() { + this.Image = image; + this.Alignment = alignment; + } + + /// + /// Create an image decoration + /// + /// + /// + /// + public ImageDecoration(Image image, int transparency, ContentAlignment alignment) + : this() { + this.Image = image; + this.Transparency = transparency; + this.Alignment = alignment; + } + + #endregion + + #region IDecoration Members + + /// + /// Gets or sets the item being decorated + /// + public OLVListItem ListItem { + get { return listItem; } + set { listItem = value; } + } + private OLVListItem listItem; + + /// + /// Gets or sets the sub item being decorated + /// + public OLVListSubItem SubItem { + get { return subItem; } + set { subItem = value; } + } + private OLVListSubItem subItem; + + #endregion + + #region Commands + + /// + /// Draw this decoration + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + this.DrawImage(g, this.CalculateItemBounds(this.ListItem, this.SubItem)); + } + + #endregion + } + + /// + /// Instances of this class draw some text over the row/cell that they are decorating + /// + public class TextDecoration : TextAdornment, IDecoration + { + #region Constructors + + /// + /// Create a TextDecoration + /// + public TextDecoration() { + this.Alignment = ContentAlignment.MiddleRight; + } + + /// + /// Create a TextDecoration + /// + /// + public TextDecoration(string text) + : this() { + this.Text = text; + } + + /// + /// Create a TextDecoration + /// + /// + /// + public TextDecoration(string text, int transparency) + : this() { + this.Text = text; + this.Transparency = transparency; + } + + /// + /// Create a TextDecoration + /// + /// + /// + public TextDecoration(string text, ContentAlignment alignment) + : this() { + this.Text = text; + this.Alignment = alignment; + } + + /// + /// Create a TextDecoration + /// + /// + /// + /// + public TextDecoration(string text, int transparency, ContentAlignment alignment) + : this() { + this.Text = text; + this.Transparency = transparency; + this.Alignment = alignment; + } + + #endregion + + #region IDecoration Members + + /// + /// Gets or sets the item being decorated + /// + public OLVListItem ListItem { + get { return listItem; } + set { listItem = value; } + } + private OLVListItem listItem; + + /// + /// Gets or sets the sub item being decorated + /// + public OLVListSubItem SubItem { + get { return subItem; } + set { subItem = value; } + } + private OLVListSubItem subItem; + + + #endregion + + #region Commands + + /// + /// Draw this decoration + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + this.DrawText(g, this.CalculateItemBounds(this.ListItem, this.SubItem)); + } + + #endregion + } +} diff --git a/ObjectListView/Rendering/Overlays.cs b/ObjectListView/Rendering/Overlays.cs new file mode 100644 index 00000000..d103601d --- /dev/null +++ b/ObjectListView/Rendering/Overlays.cs @@ -0,0 +1,302 @@ +/* + * Overlays - Images, text or other things that can be rendered over the top of a ListView + * + * Author: Phillip Piper + * Date: 14/04/2009 4:36 PM + * + * Change log: + * v2.3 + * 2009-08-17 JPP - Overlays now use Adornments + * - Added ITransparentOverlay interface. Overlays can now have separate transparency levels + * 2009-08-10 JPP - Moved decoration related code to new file + * v2.2.1 + * 200-07-24 JPP - TintedColumnDecoration now works when last item is a member of a collapsed + * group (well, it no longer crashes). + * v2.2 + * 2009-06-01 JPP - Make sure that TintedColumnDecoration reaches to the last item in group view + * 2009-05-05 JPP - Unified BillboardOverlay text rendering with that of TextOverlay + * 2009-04-30 JPP - Added TintedColumnDecoration + * 2009-04-14 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; + +namespace BrightIdeasSoftware +{ + /// + /// The interface for an object which can draw itself over the top of + /// an ObjectListView. + /// + public interface IOverlay + { + /// + /// Draw this overlay + /// + /// The ObjectListView that is being overlaid + /// The Graphics onto the given OLV + /// The content area of the OLV + void Draw(ObjectListView olv, Graphics g, Rectangle r); + } + + /// + /// An interface for an overlay that supports variable levels of transparency + /// + public interface ITransparentOverlay : IOverlay + { + /// + /// Gets or sets the transparency of the overlay. + /// 0 is completely transparent, 255 is completely opaque. + /// + int Transparency { get; set; } + } + + /// + /// A null implementation of the IOverlay interface + /// + public class AbstractOverlay : ITransparentOverlay + { + #region IOverlay Members + + /// + /// Draw this overlay + /// + /// The ObjectListView that is being overlaid + /// The Graphics onto the given OLV + /// The content area of the OLV + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + } + + #endregion + + #region ITransparentOverlay Members + + /// + /// How transparent should this overlay be? + /// + [Category("ObjectListView"), + Description("How transparent should this overlay be"), + DefaultValue(128), + NotifyParentProperty(true)] + public int Transparency { + get { return this.transparency; } + set { this.transparency = Math.Min(255, Math.Max(0, value)); } + } + private int transparency = 128; + + #endregion + } + + /// + /// An overlay that will draw an image over the top of the ObjectListView + /// + [TypeConverter("BrightIdeasSoftware.Design.OverlayConverter")] + public class ImageOverlay : ImageAdornment, ITransparentOverlay + { + /// + /// Create an ImageOverlay + /// + public ImageOverlay() { + this.Alignment = System.Drawing.ContentAlignment.BottomRight; + } + + #region Public properties + + /// + /// Gets or sets the horizontal inset by which the position of the overlay will be adjusted + /// + [Category("ObjectListView"), + Description("The horizontal inset by which the position of the overlay will be adjusted"), + DefaultValue(20), + NotifyParentProperty(true)] + public int InsetX { + get { return this.insetX; } + set { this.insetX = Math.Max(0, value); } + } + private int insetX = 20; + + /// + /// Gets or sets the vertical inset by which the position of the overlay will be adjusted + /// + [Category("ObjectListView"), + Description("Gets or sets the vertical inset by which the position of the overlay will be adjusted"), + DefaultValue(20), + NotifyParentProperty(true)] + public int InsetY { + get { return this.insetY; } + set { this.insetY = Math.Max(0, value); } + } + private int insetY = 20; + + #endregion + + #region Commands + + /// + /// Draw this overlay + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + Rectangle insetRect = r; + insetRect.Inflate(-this.InsetX, -this.InsetY); + + // We hard code a transparency of 255 here since transparency is handled by the glass panel + this.DrawImage(g, insetRect, this.Image, 255); + } + + #endregion + } + + /// + /// An overlay that will draw text over the top of the ObjectListView + /// + [TypeConverter("BrightIdeasSoftware.Design.OverlayConverter")] + public class TextOverlay : TextAdornment, ITransparentOverlay + { + /// + /// Create a TextOverlay + /// + public TextOverlay() { + this.Alignment = System.Drawing.ContentAlignment.BottomRight; + } + + #region Public properties + + /// + /// Gets or sets the horizontal inset by which the position of the overlay will be adjusted + /// + [Category("ObjectListView"), + Description("The horizontal inset by which the position of the overlay will be adjusted"), + DefaultValue(20), + NotifyParentProperty(true)] + public int InsetX { + get { return this.insetX; } + set { this.insetX = Math.Max(0, value); } + } + private int insetX = 20; + + /// + /// Gets or sets the vertical inset by which the position of the overlay will be adjusted + /// + [Category("ObjectListView"), + Description("Gets or sets the vertical inset by which the position of the overlay will be adjusted"), + DefaultValue(20), + NotifyParentProperty(true)] + public int InsetY { + get { return this.insetY; } + set { this.insetY = Math.Max(0, value); } + } + private int insetY = 20; + + /// + /// Gets or sets whether the border will be drawn with rounded corners + /// + [Browsable(false), + Obsolete("Use CornerRounding instead", false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool RoundCorneredBorder { + get { return this.CornerRounding > 0; } + set { + if (value) + this.CornerRounding = 16.0f; + else + this.CornerRounding = 0.0f; + } + } + + #endregion + + #region Commands + + /// + /// Draw this overlay + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + if (String.IsNullOrEmpty(this.Text)) + return; + + Rectangle insetRect = r; + insetRect.Inflate(-this.InsetX, -this.InsetY); + // We hard code a transparency of 255 here since transparency is handled by the glass panel + this.DrawText(g, insetRect, this.Text, 255); + } + + #endregion + } + + /// + /// A Billboard overlay is a TextOverlay positioned at an absolute point + /// + public class BillboardOverlay : TextOverlay + { + /// + /// Create a BillboardOverlay + /// + public BillboardOverlay() { + this.Transparency = 255; + this.BackColor = Color.PeachPuff; + this.TextColor = Color.Black; + this.BorderColor = Color.Empty; + this.Font = new Font("Tahoma", 10); + } + + /// + /// Gets or sets where should the top left of the billboard be placed + /// + public Point Location { + get { return this.location; } + set { this.location = value; } + } + private Point location; + + /// + /// Draw this overlay + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + if (String.IsNullOrEmpty(this.Text)) + return; + + // Calculate the bounds of the text, and then move it to where it should be + Rectangle textRect = this.CalculateTextBounds(g, r, this.Text); + textRect.Location = this.Location; + + // Make sure the billboard is within the bounds of the List, as far as is possible + if (textRect.Right > r.Width) + textRect.X = Math.Max(r.Left, r.Width - textRect.Width); + if (textRect.Bottom > r.Height) + textRect.Y = Math.Max(r.Top, r.Height - textRect.Height); + + this.DrawBorderedText(g, textRect, this.Text, 255); + } + } +} diff --git a/ObjectListView/Rendering/Renderers.cs b/ObjectListView/Rendering/Renderers.cs new file mode 100644 index 00000000..dac6a62e --- /dev/null +++ b/ObjectListView/Rendering/Renderers.cs @@ -0,0 +1,3887 @@ +/* + * Renderers - A collection of useful renderers that are used to owner draw a cell in an ObjectListView + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * 2018-10-06 JPP - Fix rendering so that OLVColumn.WordWrap works when using customised Renderers + * 2018-05-01 JPP - Use ITextMatchFilter interface rather than TextMatchFilter concrete class. + * v2.9.2 + * 2016-06-02 JPP - CalculateImageWidth() no longer adds 2 to the image width + * 2016-05-29 JPP - Fix calculation of cell edit boundaries on TreeListView controls + * v2.9 + * 2015-08-22 JPP - Allow selected row back/fore colours to be specified for each row + * 2015-06-23 JPP - Added ColumnButtonRenderer plus general support for Buttons + * 2015-06-22 JPP - Added BaseRenderer.ConfigureItem() and ConfigureSubItem() to easily allow + * other renderers to be chained for use within a primary renderer. + * - Lots of tightening of hit tests and edit rectangles + * 2015-05-15 JPP - Handle rendering an Image when that Image is returned as an aspect. + * v2.8 + * 2014-09-26 JPP - Dispose of animation timer in a more robust fashion. + * 2014-05-20 JPP - Handle rendering disabled rows + * v2.7 + * 2013-04-29 JPP - Fixed bug where Images were not vertically aligned + * v2.6 + * 2012-10-26 JPP - Hit detection will no longer report check box hits on columns without checkboxes. + * 2012-07-13 JPP - [Breaking change] Added preferedSize parameter to IRenderer.GetEditRectangle(). + * v2.5.1 + * 2012-07-14 JPP - Added CellPadding to various places. Replaced DescribedTaskRenderer.CellPadding. + * 2012-07-11 JPP - Added CellVerticalAlignment to various places allow cell contents to be vertically + * aligned (rather than always being centered). + * v2.5 + * 2010-08-24 JPP - CheckBoxRenderer handles hot boxes and correctly vertically centers the box. + * 2010-06-23 JPP - Major rework of HighlightTextRenderer. Now uses TextMatchFilter directly. + * Draw highlighting underneath text to improve legibility. Works with new + * TextMatchFilter capabilities. + * v2.4 + * 2009-10-30 JPP - Plugged possible resource leak by using using() with CreateGraphics() + * v2.3 + * 2009-09-28 JPP - Added DescribedTaskRenderer + * 2009-09-01 JPP - Correctly handle an ImageRenderer's handling of an aspect that holds + * the image to be displayed at Byte[]. + * 2009-08-29 JPP - Fixed bug where some of a cell's background was not erased. + * 2009-08-15 JPP - Correctly MeasureText() using the appropriate graphic context + * - Handle translucent selection setting + * v2.2.1 + * 2009-07-24 JPP - Try to honour CanWrap setting when GDI rendering text. + * 2009-07-11 JPP - Correctly calculate edit rectangle for subitems of a tree view + * (previously subitems were indented in the same way as the primary column) + * v2.2 + * 2009-06-06 JPP - Tweaked text rendering so that column 0 isn't ellipsed unnecessarily. + * 2009-05-05 JPP - Added Unfocused foreground and background colors + * (thanks to Christophe Hosten) + * 2009-04-21 JPP - Fixed off-by-1 error when calculating text widths. This caused + * middle and right aligned columns to always wrap one character + * when printed using ListViewPrinter (SF#2776634). + * 2009-04-11 JPP - Correctly renderer checkboxes when RowHeight is non-standard + * 2009-04-06 JPP - Allow for item indent when calculating edit rectangle + * v2.1 + * 2009-02-24 JPP - Work properly with ListViewPrinter again + * 2009-01-26 JPP - AUSTRALIA DAY (why aren't I on holidays!) + * - Major overhaul of renderers. Now uses IRenderer interface. + * - ImagesRenderer and FlagsRenderer are now defunct. + * The names are retained for backward compatibility. + * 2009-01-23 JPP - Align bitmap AND text according to column alignment (previously + * only text was aligned and bitmap was always to the left). + * 2009-01-21 JPP - Changed to use TextRenderer rather than native GDI routines. + * 2009-01-20 JPP - Draw images directly from image list if possible. 30% faster! + * - Tweaked some spacings to look more like native ListView + * - Text highlight for non FullRowSelect is now the right color + * when the control doesn't have focus. + * - Commented out experimental animations. Still needs work. + * 2009-01-19 JPP - Changed to draw text using GDI routines. Looks more like + * native control this way. Set UseGdiTextRendering to false to + * revert to previous behavior. + * 2009-01-15 JPP - Draw background correctly when control is disabled + * - Render checkboxes using CheckBoxRenderer + * v2.0.1 + * 2008-12-29 JPP - Render text correctly when HideSelection is true. + * 2008-12-26 JPP - BaseRenderer now works correctly in all Views + * 2008-12-23 JPP - Fixed two small bugs in BarRenderer + * v2.0 + * 2008-10-26 JPP - Don't owner draw when in Design mode + * 2008-09-27 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2018 Phillip Piper + * + * TO DO: + * - Hit detection on renderers doesn't change the controls standard selection behavior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Windows.Forms; +using System.Windows.Forms.VisualStyles; +using Timer = System.Threading.Timer; + +namespace BrightIdeasSoftware { + /// + /// Renderers are the mechanism used for owner drawing cells. As such, they can also handle + /// hit detection and positioning of cell editing rectangles. + /// + public interface IRenderer { + /// + /// Render the whole item within an ObjectListView. This is only used in non-Details views. + /// + /// The event + /// A Graphics for rendering + /// The bounds of the item + /// The model object to be drawn + /// Return true to indicate that the event was handled and no further processing is needed. + bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, Object rowObject); + + /// + /// Render one cell within an ObjectListView when it is in Details mode. + /// + /// The event + /// A Graphics for rendering + /// The bounds of the cell + /// The model object to be drawn + /// Return true to indicate that the event was handled and no further processing is needed. + bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, Object rowObject); + + /// + /// What is under the given point? + /// + /// + /// x co-ordinate + /// y co-ordinate + /// This method should only alter HitTestLocation and/or UserData. + void HitTest(OlvListViewHitTestInfo hti, int x, int y); + + /// + /// When the value in the given cell is to be edited, where should the edit rectangle be placed? + /// + /// + /// + /// + /// + /// + /// + Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize); + } + + /// + /// Renderers that implement this interface will have the filter property updated, + /// each time the filter on the ObjectListView is updated. + /// + public interface IFilterAwareRenderer + { + /// + /// Gets or sets the filter that is currently active + /// + IModelFilter Filter { get; set; } + } + + /// + /// An AbstractRenderer is a do-nothing implementation of the IRenderer interface. + /// + [Browsable(true), + ToolboxItem(false)] + public class AbstractRenderer : Component, IRenderer { + #region IRenderer Members + + /// + /// Render the whole item within an ObjectListView. This is only used in non-Details views. + /// + /// The event + /// A Graphics for rendering + /// The bounds of the item + /// The model object to be drawn + /// Return true to indicate that the event was handled and no further processing is needed. + public virtual bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, object rowObject) { + return true; + } + + /// + /// Render one cell within an ObjectListView when it is in Details mode. + /// + /// The event + /// A Graphics for rendering + /// The bounds of the cell + /// The model object to be drawn + /// Return true to indicate that the event was handled and no further processing is needed. + public virtual bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object rowObject) { + return false; + } + + /// + /// What is under the given point? + /// + /// + /// x co-ordinate + /// y co-ordinate + /// This method should only alter HitTestLocation and/or UserData. + public virtual void HitTest(OlvListViewHitTestInfo hti, int x, int y) {} + + /// + /// When the value in the given cell is to be edited, where should the edit rectangle be placed? + /// + /// + /// + /// + /// + /// + /// + public virtual Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return cellBounds; + } + + #endregion + } + + /// + /// This class provides compatibility for v1 RendererDelegates + /// + [ToolboxItem(false)] + internal class Version1Renderer : AbstractRenderer { + public Version1Renderer(RenderDelegate renderDelegate) { + this.RenderDelegate = renderDelegate; + } + + /// + /// The renderer delegate that this renderer wraps + /// + public RenderDelegate RenderDelegate; + + #region IRenderer Members + + public override bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object rowObject) { + if (this.RenderDelegate == null) + return base.RenderSubItem(e, g, cellBounds, rowObject); + else + return this.RenderDelegate(e, g, cellBounds, rowObject); + } + + #endregion + } + + /// + /// A BaseRenderer provides useful base level functionality for any custom renderer. + /// + /// + /// Subclasses will normally override the Render or OptionalRender method, and use the other + /// methods as helper functions. + /// + [Browsable(true), + ToolboxItem(true)] + public class BaseRenderer : AbstractRenderer { + internal const TextFormatFlags NormalTextFormatFlags = TextFormatFlags.NoPrefix | + TextFormatFlags.EndEllipsis | + TextFormatFlags.PreserveGraphicsTranslateTransform; + + #region Configuration Properties + + /// + /// Can the renderer wrap lines that do not fit completely within the cell? + /// + /// + /// If this is not set specifically, the value will be taken from Column.WordWrap + /// + /// Wrapping text doesn't work with the GDI renderer, so if this set to true, GDI+ rendering will used. + /// The difference between GDI and GDI+ rendering can give word wrapped columns a slight different appearance. + /// + /// + [Category("Appearance"), + Description("Can the renderer wrap text that does not fit completely within the cell"), + DefaultValue(null)] + public bool? CanWrap { + get { return canWrap; } + set { canWrap = value; } + } + + private bool? canWrap; + + /// + /// Get the actual value that should be used right now for CanWrap + /// + [Browsable(false)] + protected bool CanWrapOrDefault { + get { + return this.CanWrap ?? this.Column != null && this.Column.WordWrap; + } + } + /// + /// Gets or sets how many pixels will be left blank around this cell + /// + /// + /// + /// This setting only takes effect when the control is owner drawn. + /// + /// for more details. + /// + [Category("ObjectListView"), + Description("The number of pixels that renderer will leave empty around the edge of the cell"), + DefaultValue(null)] + public Rectangle? CellPadding { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets the horizontal alignment of the column + /// + [Browsable(false)] + public HorizontalAlignment CellHorizontalAlignment + { + get { return this.Column == null ? HorizontalAlignment.Left : this.Column.TextAlign; } + } + + /// + /// Gets or sets how cells drawn by this renderer will be vertically aligned. + /// + /// + /// + /// If this is not set, the value from the column or control itself will be used. + /// + /// + [Category("ObjectListView"), + Description("How will cell values be vertically aligned?"), + DefaultValue(null)] + public virtual StringAlignment? CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment? cellVerticalAlignment; + + /// + /// Gets the optional padding that this renderer should apply before drawing. + /// This property considers all possible sources of padding + /// + [Browsable(false)] + protected virtual Rectangle? EffectiveCellPadding { + get { + if (this.cellPadding.HasValue) + return this.cellPadding.Value; + + if (this.OLVSubItem != null && this.OLVSubItem.CellPadding.HasValue) + return this.OLVSubItem.CellPadding.Value; + + if (this.ListItem != null && this.ListItem.CellPadding.HasValue) + return this.ListItem.CellPadding.Value; + + if (this.Column != null && this.Column.CellPadding.HasValue) + return this.Column.CellPadding.Value; + + if (this.ListView != null && this.ListView.CellPadding.HasValue) + return this.ListView.CellPadding.Value; + + return null; + } + } + + /// + /// Gets the vertical cell alignment that should govern the rendering. + /// This property considers all possible sources. + /// + [Browsable(false)] + protected virtual StringAlignment EffectiveCellVerticalAlignment { + get { + if (this.cellVerticalAlignment.HasValue) + return this.cellVerticalAlignment.Value; + + if (this.OLVSubItem != null && this.OLVSubItem.CellVerticalAlignment.HasValue) + return this.OLVSubItem.CellVerticalAlignment.Value; + + if (this.ListItem != null && this.ListItem.CellVerticalAlignment.HasValue) + return this.ListItem.CellVerticalAlignment.Value; + + if (this.Column != null && this.Column.CellVerticalAlignment.HasValue) + return this.Column.CellVerticalAlignment.Value; + + if (this.ListView != null) + return this.ListView.CellVerticalAlignment; + + return StringAlignment.Center; + } + } + + /// + /// Gets or sets the image list from which keyed images will be fetched + /// + [Category("Appearance"), + Description("The image list from which keyed images will be fetched for drawing. If this is not given, the small ImageList from the ObjectListView will be used"), + DefaultValue(null)] + public ImageList ImageList { + get { return imageList; } + set { imageList = value; } + } + + private ImageList imageList; + + /// + /// When rendering multiple images, how many pixels should be between each image? + /// + [Category("Appearance"), + Description("When rendering multiple images, how many pixels should be between each image?"), + DefaultValue(1)] + public int Spacing { + get { return spacing; } + set { spacing = value; } + } + + private int spacing = 1; + + /// + /// Should text be rendered using GDI routines? This makes the text look more + /// like a native List view control. + /// + /// Even if this is set to true, it will return false if the renderer + /// is set to word wrap, since GDI doesn't handle wrapping. + [Category("Appearance"), + Description("Should text be rendered using GDI routines?"), + DefaultValue(true)] + public virtual bool UseGdiTextRendering { + get { + // Can't use GDI routines on a GDI+ printer context or when word wrapping is required + return !this.IsPrinting && !this.CanWrapOrDefault && useGdiTextRendering; + } + set { useGdiTextRendering = value; } + } + private bool useGdiTextRendering = true; + + #endregion + + #region State Properties + + /// + /// Get or set the aspect of the model object that this renderer should draw + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Object Aspect { + get { + if (aspect == null) + aspect = column.GetValue(this.rowObject); + return aspect; + } + set { aspect = value; } + } + + private Object aspect; + + /// + /// What are the bounds of the cell that is being drawn? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Rectangle Bounds { + get { return bounds; } + set { bounds = value; } + } + + private Rectangle bounds; + + /// + /// Get or set the OLVColumn that this renderer will draw + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVColumn Column { + get { return column; } + set { column = value; } + } + + private OLVColumn column; + + /// + /// Get/set the event that caused this renderer to be called + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public DrawListViewItemEventArgs DrawItemEvent { + get { return drawItemEventArgs; } + set { drawItemEventArgs = value; } + } + + private DrawListViewItemEventArgs drawItemEventArgs; + + /// + /// Get/set the event that caused this renderer to be called + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public DrawListViewSubItemEventArgs Event { + get { return eventArgs; } + set { eventArgs = value; } + } + + private DrawListViewSubItemEventArgs eventArgs; + + /// + /// Gets or sets the font to be used for text in this cell + /// + /// + /// + /// In general, this property should be treated as internal. + /// If you do set this, the given font will be used without any other consideration. + /// All other factors -- selection state, hot item, hyperlinks -- will be ignored. + /// + /// + /// A better way to set the font is to change either ListItem.Font or SubItem.Font + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Font Font { + get { + if (this.font != null || this.ListItem == null) + return this.font; + + if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems) + return this.ListItem.Font; + + return this.SubItem.Font; + } + set { this.font = value; } + } + + private Font font; + + /// + /// Gets the image list from which keyed images will be fetched + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ImageList ImageListOrDefault { + get { return this.ImageList ?? this.ListView.SmallImageList; } + } + + /// + /// Should this renderer fill in the background before drawing? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool IsDrawBackground { + get { return !this.IsPrinting; } + } + + /// + /// Cache whether or not our item is selected + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool IsItemSelected { + get { return isItemSelected; } + set { isItemSelected = value; } + } + + private bool isItemSelected; + + /// + /// Is this renderer being used on a printer context? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool IsPrinting { + get { return isPrinting; } + set { isPrinting = value; } + } + + private bool isPrinting; + + /// + /// Get or set the listitem that this renderer will be drawing + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVListItem ListItem { + get { return listItem; } + set { listItem = value; } + } + + private OLVListItem listItem; + + /// + /// Get/set the listview for which the drawing is to be done + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ObjectListView ListView { + get { return objectListView; } + set { objectListView = value; } + } + + private ObjectListView objectListView; + + /// + /// Get the specialized OLVSubItem that this renderer is drawing + /// + /// This returns null for column 0. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVListSubItem OLVSubItem { + get { return listSubItem as OLVListSubItem; } + } + + /// + /// Get or set the model object that this renderer should draw + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Object RowObject { + get { return rowObject; } + set { rowObject = value; } + } + + private Object rowObject; + + /// + /// Get or set the list subitem that this renderer will be drawing + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVListSubItem SubItem { + get { return listSubItem; } + set { listSubItem = value; } + } + + private OLVListSubItem listSubItem; + + /// + /// The brush that will be used to paint the text + /// + /// + /// + /// In general, this property should be treated as internal. It will be reset after each render. + /// + /// + /// + /// In particular, don't set it to configure the color of the text on the control. That should be done via SubItem.ForeColor + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Brush TextBrush { + get { + if (textBrush == null) + return new SolidBrush(this.GetForegroundColor()); + else + return this.textBrush; + } + set { textBrush = value; } + } + + private Brush textBrush; + + /// + /// Will this renderer use the custom images from the parent ObjectListView + /// to draw the checkbox images. + /// + /// + /// + /// If this is true, the renderer will use the images from the + /// StateImageList to represent checkboxes. 0 - unchecked, 1 - checked, 2 - indeterminate. + /// + /// If this is false (the default), then the renderer will use .NET's standard + /// CheckBoxRenderer. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool UseCustomCheckboxImages { + get { return useCustomCheckboxImages; } + set { useCustomCheckboxImages = value; } + } + + private bool useCustomCheckboxImages; + + private void ClearState() { + this.Event = null; + this.DrawItemEvent = null; + this.Aspect = null; + this.TextBrush = null; + } + + #endregion + + #region Utilities + + /// + /// Align the second rectangle with the first rectangle, + /// according to the alignment of the column + /// + /// The cell's bounds + /// The rectangle to be aligned within the bounds + /// An aligned rectangle + protected virtual Rectangle AlignRectangle(Rectangle outer, Rectangle inner) { + Rectangle r = new Rectangle(outer.Location, inner.Size); + + // Align horizontally depending on the column alignment + if (inner.Width < outer.Width) { + r.X = AlignHorizontally(outer, inner); + } + + // Align vertically too + if (inner.Height < outer.Height) { + r.Y = AlignVertically(outer, inner); + } + + return r; + } + + /// + /// Calculate the left edge of the rectangle that aligns the outer rectangle with the inner one + /// according to this renderer's horizontal alignment + /// + /// + /// + /// + protected int AlignHorizontally(Rectangle outer, Rectangle inner) { + HorizontalAlignment alignment = this.CellHorizontalAlignment; + switch (alignment) { + case HorizontalAlignment.Left: + return outer.Left + 1; + case HorizontalAlignment.Center: + return outer.Left + ((outer.Width - inner.Width) / 2); + case HorizontalAlignment.Right: + return outer.Right - inner.Width - 1; + default: + throw new ArgumentOutOfRangeException(); + } + } + + + /// + /// Calculate the top of the rectangle that aligns the outer rectangle with the inner rectangle + /// according to this renders vertical alignment + /// + /// + /// + /// + protected int AlignVertically(Rectangle outer, Rectangle inner) { + return AlignVertically(outer, inner.Height); + } + + /// + /// Calculate the top of the rectangle that aligns the outer rectangle with a rectangle of the given height + /// according to this renderer's vertical alignment + /// + /// + /// + /// + protected int AlignVertically(Rectangle outer, int innerHeight) { + switch (this.EffectiveCellVerticalAlignment) { + case StringAlignment.Near: + return outer.Top + 1; + case StringAlignment.Center: + return outer.Top + ((outer.Height - innerHeight) / 2); + case StringAlignment.Far: + return outer.Bottom - innerHeight - 1; + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Calculate the space that our rendering will occupy and then align that space + /// with the given rectangle, according to the Column alignment + /// + /// + /// Pre-padded bounds of the cell + /// + protected virtual Rectangle CalculateAlignedRectangle(Graphics g, Rectangle r) { + if (this.Column == null) + return r; + + Rectangle contentRectangle = new Rectangle(Point.Empty, this.CalculateContentSize(g, r)); + return this.AlignRectangle(r, contentRectangle); + } + + /// + /// Calculate the size of the content of this cell. + /// + /// + /// Pre-padded bounds of the cell + /// The width and height of the content + protected virtual Size CalculateContentSize(Graphics g, Rectangle r) + { + Size checkBoxSize = this.CalculatePrimaryCheckBoxSize(g); + Size imageSize = this.CalculateImageSize(g, this.GetImageSelector()); + Size textSize = this.CalculateTextSize(g, this.GetText(), r.Width - (checkBoxSize.Width + imageSize.Width)); + + // If the combined width is greater than the whole cell, we just use the cell itself + + int width = Math.Min(r.Width, checkBoxSize.Width + imageSize.Width + textSize.Width); + int componentMaxHeight = Math.Max(checkBoxSize.Height, Math.Max(imageSize.Height, textSize.Height)); + int height = Math.Min(r.Height, componentMaxHeight); + + return new Size(width, height); + } + + /// + /// Calculate the bounds of a checkbox given the (pre-padded) cell bounds + /// + /// + /// Pre-padded cell bounds + /// + protected Rectangle CalculateCheckBoxBounds(Graphics g, Rectangle cellBounds) { + Size checkBoxSize = this.CalculateCheckBoxSize(g); + return this.AlignRectangle(cellBounds, new Rectangle(0, 0, checkBoxSize.Width, checkBoxSize.Height)); + } + + + /// + /// How much space will the check box for this cell occupy? + /// + /// Only column 0 can have check boxes. Sub item checkboxes are + /// treated as images + /// + /// + protected virtual Size CalculateCheckBoxSize(Graphics g) + { + if (UseCustomCheckboxImages && this.ListView.StateImageList != null) + return this.ListView.StateImageList.ImageSize; + + return CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.UncheckedNormal); + } + + /// + /// How much space will the check box for this row occupy? + /// If the list doesn't have checkboxes, or this isn't the primary column, + /// this returns an empty size. + /// + /// + /// + protected virtual Size CalculatePrimaryCheckBoxSize(Graphics g) { + if (!this.ListView.CheckBoxes || !this.ColumnIsPrimary) + return Size.Empty; + + Size size = this.CalculateCheckBoxSize(g); + size.Width += 6; + return size; + } + + /// + /// How much horizontal space will the image of this cell occupy? + /// + /// + /// + /// + protected virtual int CalculateImageWidth(Graphics g, object imageSelector) + { + return this.CalculateImageSize(g, imageSelector).Width; + } + + /// + /// How much vertical space will the image of this cell occupy? + /// + /// + /// + /// + protected virtual int CalculateImageHeight(Graphics g, object imageSelector) + { + return this.CalculateImageSize(g, imageSelector).Height; + } + + /// + /// How much space will the image of this cell occupy? + /// + /// + /// + /// + protected virtual Size CalculateImageSize(Graphics g, object imageSelector) + { + if (imageSelector == null || imageSelector == DBNull.Value) + return Size.Empty; + + // Check for the image in the image list (most common case) + ImageList il = this.ImageListOrDefault; + if (il != null) + { + int selectorAsInt = -1; + + if (imageSelector is Int32) + selectorAsInt = (Int32)imageSelector; + else + { + String selectorAsString = imageSelector as String; + if (selectorAsString != null) + selectorAsInt = il.Images.IndexOfKey(selectorAsString); + } + if (selectorAsInt >= 0) + return il.ImageSize; + } + + // Is the selector actually an image? + Image image = imageSelector as Image; + if (image != null) + return image.Size; + + return Size.Empty; + } + + /// + /// How much horizontal space will the text of this cell occupy? + /// + /// + /// + /// + /// + protected virtual int CalculateTextWidth(Graphics g, string txt, int width) + { + if (String.IsNullOrEmpty(txt)) + return 0; + + return CalculateTextSize(g, txt, width).Width; + } + + /// + /// How much space will the text of this cell occupy? + /// + /// + /// + /// + /// + protected virtual Size CalculateTextSize(Graphics g, string txt, int width) + { + if (String.IsNullOrEmpty(txt)) + return Size.Empty; + + if (this.UseGdiTextRendering) + { + Size proposedSize = new Size(width, Int32.MaxValue); + return TextRenderer.MeasureText(g, txt, this.Font, proposedSize, NormalTextFormatFlags); + } + + // Using GDI+ rendering + using (StringFormat fmt = new StringFormat()) { + fmt.Trimming = StringTrimming.EllipsisCharacter; + SizeF sizeF = g.MeasureString(txt, this.Font, width, fmt); + return new Size(1 + (int)sizeF.Width, 1 + (int)sizeF.Height); + } + } + + /// + /// Return the Color that is the background color for this item's cell + /// + /// The background color of the subitem + public virtual Color GetBackgroundColor() { + if (!this.ListView.Enabled) + return SystemColors.Control; + + if (this.IsItemSelected && !this.ListView.UseTranslucentSelection && this.ListView.FullRowSelect) + return this.GetSelectedBackgroundColor(); + + if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems) + return this.ListItem.BackColor; + + return this.SubItem.BackColor; + } + + /// + /// Return the color of the background color when the item is selected + /// + /// The background color of the subitem + public virtual Color GetSelectedBackgroundColor() { + if (this.ListView.Focused) + return this.ListItem.SelectedBackColor ?? this.ListView.SelectedBackColorOrDefault; + + if (!this.ListView.HideSelection) + return this.ListView.UnfocusedSelectedBackColorOrDefault; + + return this.ListItem.BackColor; + } + + /// + /// Return the color to be used for text in this cell + /// + /// The text color of the subitem + public virtual Color GetForegroundColor() { + if (this.IsItemSelected && + !this.ListView.UseTranslucentSelection && + (this.ColumnIsPrimary || this.ListView.FullRowSelect)) + return this.GetSelectedForegroundColor(); + + return this.SubItem == null || this.ListItem.UseItemStyleForSubItems ? this.ListItem.ForeColor : this.SubItem.ForeColor; + } + + /// + /// Return the color of the foreground color when the item is selected + /// + /// The foreground color of the subitem + public virtual Color GetSelectedForegroundColor() + { + if (this.ListView.Focused) + return this.ListItem.SelectedForeColor ?? this.ListView.SelectedForeColorOrDefault; + + if (!this.ListView.HideSelection) + return this.ListView.UnfocusedSelectedForeColorOrDefault; + + return this.SubItem == null || this.ListItem.UseItemStyleForSubItems ? this.ListItem.ForeColor : this.SubItem.ForeColor; + } + + /// + /// Return the image that should be drawn against this subitem + /// + /// An Image or null if no image should be drawn. + protected virtual Image GetImage() { + return this.GetImage(this.GetImageSelector()); + } + + /// + /// Return the actual image that should be drawn when keyed by the given image selector. + /// An image selector can be: + /// an int, giving the index into the image list + /// a string, giving the image key into the image list + /// an Image, being the image itself + /// + /// + /// The value that indicates the image to be used + /// An Image or null + protected virtual Image GetImage(Object imageSelector) { + if (imageSelector == null || imageSelector == DBNull.Value) + return null; + + ImageList il = this.ImageListOrDefault; + if (il != null) { + if (imageSelector is Int32) { + Int32 index = (Int32) imageSelector; + if (index < 0 || index >= il.Images.Count) + return null; + + return il.Images[index]; + } + + String str = imageSelector as String; + if (str != null) { + if (il.Images.ContainsKey(str)) + return il.Images[str]; + + return null; + } + } + + return imageSelector as Image; + } + + /// + /// + protected virtual Object GetImageSelector() { + return this.ColumnIsPrimary ? this.ListItem.ImageSelector : this.OLVSubItem.ImageSelector; + } + + /// + /// Return the string that should be drawn within this + /// + /// + protected virtual string GetText() { + return this.SubItem == null ? this.ListItem.Text : this.SubItem.Text; + } + + /// + /// Return the Color that is the background color for this item's text + /// + /// The background color of the subitem's text + [Obsolete("Use GetBackgroundColor() instead")] + protected virtual Color GetTextBackgroundColor() { + return Color.Red; // just so it shows up if it is used + } + + #endregion + + #region IRenderer members + + /// + /// Render the whole item in a non-details view. + /// + /// + /// + /// + /// + /// + public override bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, object model) { + this.ConfigureItem(e, itemBounds, model); + return this.OptionalRender(g, itemBounds); + } + + /// + /// Prepare this renderer to draw in response to the given event + /// + /// + /// + /// + /// Use this if you want to chain a second renderer within a primary renderer. + public virtual void ConfigureItem(DrawListViewItemEventArgs e, Rectangle itemBounds, object model) + { + this.ClearState(); + + this.DrawItemEvent = e; + this.ListItem = (OLVListItem)e.Item; + this.SubItem = null; + this.ListView = (ObjectListView)this.ListItem.ListView; + this.Column = this.ListView.GetColumn(0); + this.RowObject = model; + this.Bounds = itemBounds; + this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; + } + + /// + /// Render one cell + /// + /// + /// + /// + /// + /// + public override bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object model) { + this.ConfigureSubItem(e, cellBounds, model); + return this.OptionalRender(g, cellBounds); + } + + /// + /// Prepare this renderer to draw in response to the given event + /// + /// + /// + /// + /// Use this if you want to chain a second renderer within a primary renderer. + public virtual void ConfigureSubItem(DrawListViewSubItemEventArgs e, Rectangle cellBounds, object model) { + this.ClearState(); + + this.Event = e; + this.ListItem = (OLVListItem)e.Item; + this.SubItem = (OLVListSubItem)e.SubItem; + this.ListView = (ObjectListView)this.ListItem.ListView; + this.Column = (OLVColumn)e.Header; + this.RowObject = model; + this.Bounds = cellBounds; + this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; + } + + /// + /// Calculate which part of this cell was hit + /// + /// + /// + /// + public override void HitTest(OlvListViewHitTestInfo hti, int x, int y) { + this.ClearState(); + + this.ListView = hti.ListView; + this.ListItem = hti.Item; + this.SubItem = hti.SubItem; + this.Column = hti.Column; + this.RowObject = hti.RowObject; + this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; + if (this.SubItem == null) + this.Bounds = this.ListItem.Bounds; + else + this.Bounds = this.ListItem.GetSubItemBounds(this.Column.Index); + + using (Graphics g = this.ListView.CreateGraphics()) { + this.HandleHitTest(g, hti, x, y); + } + } + + /// + /// Calculate the edit rectangle + /// + /// + /// + /// + /// + /// + /// + public override Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + this.ClearState(); + + this.ListView = (ObjectListView) item.ListView; + this.ListItem = item; + this.SubItem = item.GetSubItem(subItemIndex); + this.Column = this.ListView.GetColumn(subItemIndex); + this.RowObject = item.RowObject; + this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; + this.Bounds = cellBounds; + + return this.HandleGetEditRectangle(g, cellBounds, item, subItemIndex, preferredSize); + } + + #endregion + + #region IRenderer implementation + + // Subclasses will probably want to override these methods rather than the IRenderer + // interface methods. + + /// + /// Draw our data into the given rectangle using the given graphics context. + /// + /// + /// Subclasses should override this method. + /// The graphics context that should be used for drawing + /// The bounds of the subitem cell + /// Returns whether the rendering has already taken place. + /// If this returns false, the default processing will take over. + /// + public virtual bool OptionalRender(Graphics g, Rectangle r) { + if (this.ListView.View != View.Details) + return false; + + this.Render(g, r); + return true; + } + + /// + /// Draw our data into the given rectangle using the given graphics context. + /// + /// + /// Subclasses should override this method if they never want + /// to fall back on the default processing + /// The graphics context that should be used for drawing + /// The bounds of the subitem cell + public virtual void Render(Graphics g, Rectangle r) { + this.StandardRender(g, r); + } + + /// + /// Do the actual work of hit testing. Subclasses should override this rather than HitTest() + /// + /// + /// + /// + /// + protected virtual void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + Rectangle r = this.CalculateAlignedRectangle(g, ApplyCellPadding(this.Bounds)); + this.StandardHitTest(g, hti, r, x, y); + } + + /// + /// Handle a HitTest request after all state information has been initialized + /// + /// + /// + /// + /// + /// + /// + protected virtual Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + // MAINTAINER NOTE: This type testing is wrong (design-wise). The base class should return cell bounds, + // and a more specialized class should return StandardGetEditRectangle(). But BaseRenderer is used directly + // to draw most normal cells, as well as being directly subclassed for user implemented renderers. And this + // method needs to return different bounds in each of those cases. We should have a StandardRenderer and make + // BaseRenderer into an ABC -- but that would break too much existing code. And so we have this hack :( + + // If we are a standard renderer, return the position of the text, otherwise, use the whole cell. + if (this.GetType() == typeof (BaseRenderer)) + return this.StandardGetEditRectangle(g, cellBounds, preferredSize); + + // Center the editor vertically + if (cellBounds.Height != preferredSize.Height) + cellBounds.Y += (cellBounds.Height - preferredSize.Height) / 2; + + return cellBounds; + } + + #endregion + + #region Standard IRenderer implementations + + /// + /// Draw the standard "[checkbox] [image] [text]" cell after the state properties have been initialized. + /// + /// + /// + protected void StandardRender(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + + // Adjust the first columns rectangle to match the padding used by the native mode of the ListView + if (this.ColumnIsPrimary && this.CellHorizontalAlignment == HorizontalAlignment.Left ) { + r.X += 3; + r.Width -= 1; + } + r = this.ApplyCellPadding(r); + this.DrawAlignedImageAndText(g, r); + + // Show where the bounds of the cell padding are (debugging) + if (ObjectListView.ShowCellPaddingBounds) + g.DrawRectangle(Pens.Purple, r); + } + + /// + /// Change the bounds of the given rectangle to take any cell padding into account + /// + /// + /// + public virtual Rectangle ApplyCellPadding(Rectangle r) { + Rectangle? padding = this.EffectiveCellPadding; + if (!padding.HasValue) + return r; + // The two subtractions below look wrong, but are correct! + Rectangle paddingRectangle = padding.Value; + r.Width -= paddingRectangle.Right; + r.Height -= paddingRectangle.Bottom; + r.Offset(paddingRectangle.Location); + return r; + } + + /// + /// Perform normal hit testing relative to the given aligned content bounds + /// + /// + /// + /// + /// + /// + protected virtual void StandardHitTest(Graphics g, OlvListViewHitTestInfo hti, Rectangle alignedContentRectangle, int x, int y) { + Rectangle r = alignedContentRectangle; + + // Match tweaking from renderer + if (this.ColumnIsPrimary && this.CellHorizontalAlignment == HorizontalAlignment.Left && !(this is TreeListView.TreeRenderer)) { + r.X += 3; + r.Width -= 1; + } + int width = 0; + + // Did they hit a check box on the primary column? + if (this.ColumnIsPrimary && this.ListView.CheckBoxes) { + Size checkBoxSize = this.CalculateCheckBoxSize(g); + int checkBoxTop = this.AlignVertically(r, checkBoxSize.Height); + Rectangle r3 = new Rectangle(r.X, checkBoxTop, checkBoxSize.Width, checkBoxSize.Height); + width = r3.Width + 6; + // g.DrawRectangle(Pens.DarkGreen, r3); + if (r3.Contains(x, y)) { + hti.HitTestLocation = HitTestLocation.CheckBox; + return; + } + } + + // Did they hit the image? If they hit the image of a + // non-primary column that has a checkbox, it counts as a + // checkbox hit + r.X += width; + r.Width -= width; + width = this.CalculateImageWidth(g, this.GetImageSelector()); + Rectangle rTwo = r; + rTwo.Width = width; + //g.DrawRectangle(Pens.Red, rTwo); + if (rTwo.Contains(x, y)) { + if (this.Column != null && (this.Column.Index > 0 && this.Column.CheckBoxes)) + hti.HitTestLocation = HitTestLocation.CheckBox; + else + hti.HitTestLocation = HitTestLocation.Image; + return; + } + + // Did they hit the text? + r.X += width; + r.Width -= width; + width = this.CalculateTextWidth(g, this.GetText(), r.Width); + rTwo = r; + rTwo.Width = width; + // g.DrawRectangle(Pens.Blue, rTwo); + if (rTwo.Contains(x, y)) { + hti.HitTestLocation = HitTestLocation.Text; + return; + } + + hti.HitTestLocation = HitTestLocation.InCell; + } + + /// + /// This method calculates the bounds of the text within a standard layout + /// (i.e. optional checkbox, optional image, text) + /// + /// This method only works correctly if the state of the renderer + /// has been fully initialized (see BaseRenderer.GetEditRectangle) + /// + /// + /// + /// + protected virtual Rectangle StandardGetEditRectangle(Graphics g, Rectangle cellBounds, Size preferredSize) { + + Size contentSize = this.CalculateContentSize(g, cellBounds); + int contentWidth = this.Column.CellEditUseWholeCellEffective ? cellBounds.Width : contentSize.Width; + Rectangle editControlBounds = this.CalculatePaddedAlignedBounds(g, cellBounds, new Size(contentWidth, preferredSize.Height)); + + Size checkBoxSize = this.CalculatePrimaryCheckBoxSize(g); + int imageWidth = this.CalculateImageWidth(g, this.GetImageSelector()); + + int width = checkBoxSize.Width + imageWidth + 2; + + // Indent the primary column by the required amount + if (this.ColumnIsPrimary && this.ListItem.IndentCount > 0) { + int indentWidth = this.ListView.SmallImageSize.Width * this.ListItem.IndentCount; + editControlBounds.X += indentWidth; + } + + editControlBounds.X += width; + editControlBounds.Width -= width; + + if (editControlBounds.Width < 50) + editControlBounds.Width = 50; + if (editControlBounds.Right > cellBounds.Right) + editControlBounds.Width = cellBounds.Right - editControlBounds.Left; + + return editControlBounds; + } + + /// + /// Apply any padding to the given bounds, and then align a rectangle of the given + /// size within that padded area. + /// + /// + /// + /// + /// + protected Rectangle CalculatePaddedAlignedBounds(Graphics g, Rectangle cellBounds, Size preferredSize) { + Rectangle r = ApplyCellPadding(cellBounds); + r = this.AlignRectangle(r, new Rectangle(Point.Empty, preferredSize)); + return r; + } + + #endregion + + #region Drawing routines + + /// + /// Draw the given image aligned horizontally within the column. + /// + /// + /// Over tall images are scaled to fit. Over-wide images are + /// truncated. This is by design! + /// + /// Graphics context to use for drawing + /// Bounds of the cell + /// The image to be drawn + protected virtual void DrawAlignedImage(Graphics g, Rectangle r, Image image) { + if (image == null) + return; + + // By default, the image goes in the top left of the rectangle + Rectangle imageBounds = new Rectangle(r.Location, image.Size); + + // If the image is too tall to be drawn in the space provided, proportionally scale it down. + // Too wide images are not scaled. + if (image.Height > r.Height) { + float scaleRatio = (float) r.Height / (float) image.Height; + imageBounds.Width = (int) ((float) image.Width * scaleRatio); + imageBounds.Height = r.Height - 1; + } + + // Align and draw our (possibly scaled) image + Rectangle alignRectangle = this.AlignRectangle(r, imageBounds); + if (this.ListItem.Enabled) + g.DrawImage(image, alignRectangle); + else + ControlPaint.DrawImageDisabled(g, image, alignRectangle.X, alignRectangle.Y, GetBackgroundColor()); + } + + /// + /// Draw our subitems image and text + /// + /// Graphics context to use for drawing + /// Pre-padded bounds of the cell + protected virtual void DrawAlignedImageAndText(Graphics g, Rectangle r) { + this.DrawImageAndText(g, this.CalculateAlignedRectangle(g, r)); + } + + /// + /// Fill in the background of this cell + /// + /// Graphics context to use for drawing + /// Bounds of the cell + protected virtual void DrawBackground(Graphics g, Rectangle r) { + if (!this.IsDrawBackground) + return; + + Color backgroundColor = this.GetBackgroundColor(); + + using (Brush brush = new SolidBrush(backgroundColor)) { + g.FillRectangle(brush, r.X - 1, r.Y - 1, r.Width + 2, r.Height + 2); + } + } + + /// + /// Draw the primary check box of this row (checkboxes in other sub items use a different method) + /// + /// Graphics context to use for drawing + /// The pre-aligned and padded target rectangle + protected virtual int DrawCheckBox(Graphics g, Rectangle r) { + // The odd constants are to match checkbox placement in native mode (on XP at least) + // TODO: Unify this with CheckStateRenderer + + // The rectangle r is already horizontally aligned. We still need to align it vertically. + Size checkBoxSize = this.CalculateCheckBoxSize(g); + Point checkBoxLocation = new Point(r.X, this.AlignVertically(r, checkBoxSize.Height)); + + if (this.IsPrinting || this.UseCustomCheckboxImages) { + int imageIndex = this.ListItem.StateImageIndex; + if (this.ListView.StateImageList == null || imageIndex < 0 || imageIndex >= this.ListView.StateImageList.Images.Count) + return 0; + + return this.DrawImage(g, new Rectangle(checkBoxLocation, checkBoxSize), this.ListView.StateImageList.Images[imageIndex]) + 4; + } + + CheckBoxState boxState = this.GetCheckBoxState(this.ListItem.CheckState); + CheckBoxRenderer.DrawCheckBox(g, checkBoxLocation, boxState); + return checkBoxSize.Width; + } + + /// + /// Calculate the CheckBoxState we need to correctly draw the given state + /// + /// + /// + protected virtual CheckBoxState GetCheckBoxState(CheckState checkState) { + + // Should the checkbox be drawn as disabled? + if (this.IsCheckBoxDisabled) { + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedDisabled; + case CheckState.Unchecked: + return CheckBoxState.UncheckedDisabled; + default: + return CheckBoxState.MixedDisabled; + } + } + + // Is the cursor currently over this checkbox? + if (this.IsCheckboxHot) { + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedHot; + case CheckState.Unchecked: + return CheckBoxState.UncheckedHot; + default: + return CheckBoxState.MixedHot; + } + } + + // Not hot and not disabled -- just draw it normally + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedNormal; + case CheckState.Unchecked: + return CheckBoxState.UncheckedNormal; + default: + return CheckBoxState.MixedNormal; + } + + } + + /// + /// Should this checkbox be drawn as disabled? + /// + protected virtual bool IsCheckBoxDisabled { + get { + if (this.ListItem != null && !this.ListItem.Enabled) + return true; + + if (!this.ListView.RenderNonEditableCheckboxesAsDisabled) + return false; + + return (this.ListView.CellEditActivation == ObjectListView.CellEditActivateMode.None || + (this.Column != null && !this.Column.IsEditable)); + } + } + + /// + /// Is the current item hot (i.e. under the mouse)? + /// + protected bool IsCellHot { + get { + return this.ListView != null && + this.ListView.HotRowIndex == this.ListItem.Index && + this.ListView.HotColumnIndex == (this.Column == null ? 0 : this.Column.Index); + } + } + + /// + /// Is the mouse over a checkbox in this cell? + /// + protected bool IsCheckboxHot { + get { + return this.IsCellHot && this.ListView.HotCellHitLocation == HitTestLocation.CheckBox; + } + } + + /// + /// Draw the given text and optional image in the "normal" fashion + /// + /// Graphics context to use for drawing + /// Bounds of the cell + /// The optional image to be drawn + protected virtual int DrawImage(Graphics g, Rectangle r, Object imageSelector) { + if (imageSelector == null || imageSelector == DBNull.Value) + return 0; + + // Draw from the image list (most common case) + ImageList il = this.ImageListOrDefault; + if (il != null) { + + // Try to translate our imageSelector into a valid ImageList index + int selectorAsInt = -1; + if (imageSelector is Int32) { + selectorAsInt = (Int32) imageSelector; + if (selectorAsInt >= il.Images.Count) + selectorAsInt = -1; + } else { + String selectorAsString = imageSelector as String; + if (selectorAsString != null) + selectorAsInt = il.Images.IndexOfKey(selectorAsString); + } + + // If we found a valid index into the ImageList, draw it. + // We want to draw using the native DrawImageList calls, since that let's us do some nice effects + // But the native call does not work on PrinterDCs, so if we're printing we have to skip this bit. + if (selectorAsInt >= 0) { + if (!this.IsPrinting) { + if (il.ImageSize.Height < r.Height) + r.Y = this.AlignVertically(r, new Rectangle(Point.Empty, il.ImageSize)); + + // If we are not printing, it's probable that the given Graphics object is double buffered using a BufferedGraphics object. + // But the ImageList.Draw method doesn't honor the Translation matrix that's probably in effect on the buffered + // graphics. So we have to calculate our drawing rectangle, relative to the cells natural boundaries. + // This effectively simulates the Translation matrix. + + Rectangle r2 = new Rectangle(r.X - this.Bounds.X, r.Y - this.Bounds.Y, r.Width, r.Height); + NativeMethods.DrawImageList(g, il, selectorAsInt, r2.X, r2.Y, this.IsItemSelected, !this.ListItem.Enabled); + return il.ImageSize.Width; + } + + // For some reason, printing from an image list doesn't work onto a printer context + // So get the image from the list and FALL THROUGH to the "print an image" case + imageSelector = il.Images[selectorAsInt]; + } + } + + // Is the selector actually an image? + Image image = imageSelector as Image; + if (image == null) + return 0; // no, give up + + if (image.Size.Height < r.Height) + r.Y = this.AlignVertically(r, new Rectangle(Point.Empty, image.Size)); + + if (this.ListItem.Enabled) + g.DrawImageUnscaled(image, r.X, r.Y); + else + ControlPaint.DrawImageDisabled(g, image, r.X, r.Y, GetBackgroundColor()); + + return image.Width; + } + + /// + /// Draw our subitems image and text + /// + /// Graphics context to use for drawing + /// Bounds of the cell + protected virtual void DrawImageAndText(Graphics g, Rectangle r) { + int offset = 0; + if (this.ListView.CheckBoxes && this.ColumnIsPrimary) { + offset = this.DrawCheckBox(g, r) + 6; + r.X += offset; + r.Width -= offset; + } + + offset = this.DrawImage(g, r, this.GetImageSelector()); + r.X += offset; + r.Width -= offset; + + this.DrawText(g, r, this.GetText()); + } + + /// + /// Draw the given collection of image selectors + /// + /// + /// + /// + protected virtual int DrawImages(Graphics g, Rectangle r, ICollection imageSelectors) { + // Collect the non-null images + List images = new List(); + foreach (Object selector in imageSelectors) { + Image image = this.GetImage(selector); + if (image != null) + images.Add(image); + } + + // Figure out how much space they will occupy + int width = 0; + int height = 0; + foreach (Image image in images) { + width += (image.Width + this.Spacing); + height = Math.Max(height, image.Height); + } + + // Align the collection of images within the cell + Rectangle r2 = this.AlignRectangle(r, new Rectangle(0, 0, width, height)); + + // Finally, draw all the images in their correct location + Color backgroundColor = GetBackgroundColor(); + Point pt = r2.Location; + foreach (Image image in images) { + if (this.ListItem.Enabled) + g.DrawImage(image, pt); + else + ControlPaint.DrawImageDisabled(g, image, pt.X, pt.Y, backgroundColor); + pt.X += (image.Width + this.Spacing); + } + + // Return the width that the images occupy + return width; + } + + /// + /// Draw the given text and optional image in the "normal" fashion + /// + /// Graphics context to use for drawing + /// Bounds of the cell + /// The string to be drawn + public virtual void DrawText(Graphics g, Rectangle r, String txt) { + if (String.IsNullOrEmpty(txt)) + return; + + if (this.UseGdiTextRendering) + this.DrawTextGdi(g, r, txt); + else + this.DrawTextGdiPlus(g, r, txt); + } + + /// + /// Print the given text in the given rectangle using only GDI routines + /// + /// + /// + /// + /// + /// The native list control uses GDI routines to do its drawing, so using them + /// here makes the owner drawn mode looks more natural. + /// This method doesn't honour the CanWrap setting on the renderer. All + /// text is single line + /// + protected virtual void DrawTextGdi(Graphics g, Rectangle r, String txt) { + Color backColor = Color.Transparent; + if (this.IsDrawBackground && this.IsItemSelected && ColumnIsPrimary && !this.ListView.FullRowSelect) + backColor = this.GetSelectedBackgroundColor(); + + TextFormatFlags flags = NormalTextFormatFlags | this.CellVerticalAlignmentAsTextFormatFlag; + + // I think there is a bug in the TextRenderer. Setting or not setting SingleLine doesn't make + // any difference -- it is always single line. + if (!this.CanWrapOrDefault) + flags |= TextFormatFlags.SingleLine; + TextRenderer.DrawText(g, txt, this.Font, r, this.GetForegroundColor(), backColor, flags); + } + + private bool ColumnIsPrimary { + get { return this.Column != null && this.Column.Index == 0; } + } + + /// + /// Gets the cell's vertical alignment as a TextFormatFlag + /// + /// + protected TextFormatFlags CellVerticalAlignmentAsTextFormatFlag { + get { + switch (this.EffectiveCellVerticalAlignment) { + case StringAlignment.Near: + return TextFormatFlags.Top; + case StringAlignment.Center: + return TextFormatFlags.VerticalCenter; + case StringAlignment.Far: + return TextFormatFlags.Bottom; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + /// + /// Gets the StringFormat needed when drawing text using GDI+ + /// + protected virtual StringFormat StringFormatForGdiPlus { + get { + StringFormat fmt = new StringFormat(); + fmt.LineAlignment = this.EffectiveCellVerticalAlignment; + fmt.Trimming = StringTrimming.EllipsisCharacter; + fmt.Alignment = this.Column == null ? StringAlignment.Near : this.Column.TextStringAlign; + if (!this.CanWrapOrDefault) + fmt.FormatFlags = StringFormatFlags.NoWrap; + return fmt; + } + } + + /// + /// Print the given text in the given rectangle using normal GDI+ .NET methods + /// + /// Printing to a printer dc has to be done using this method. + protected virtual void DrawTextGdiPlus(Graphics g, Rectangle r, String txt) { + using (StringFormat fmt = this.StringFormatForGdiPlus) { + // Draw the background of the text as selected, if it's the primary column + // and it's selected and it's not in FullRowSelect mode. + Font f = this.Font; + if (this.IsDrawBackground && this.IsItemSelected && this.ColumnIsPrimary && !this.ListView.FullRowSelect) { + SizeF size = g.MeasureString(txt, f, r.Width, fmt); + Rectangle r2 = r; + r2.Width = (int) size.Width + 1; + using (Brush brush = new SolidBrush(this.GetSelectedBackgroundColor())) { + g.FillRectangle(brush, r2); + } + } + RectangleF rf = r; + g.DrawString(txt, f, this.TextBrush, rf, fmt); + } + + // We should put a focus rectangle around the column 0 text if it's selected -- + // but we don't because: + // - I really dislike this UI convention + // - we are using buffered graphics, so the DrawFocusRecatangle method of the event doesn't work + + //if (this.ColumnIsPrimary) { + // Size size = TextRenderer.MeasureText(this.SubItem.Text, this.ListView.ListFont); + // if (r.Width > size.Width) + // r.Width = size.Width; + // this.Event.DrawFocusRectangle(r); + //} + } + + #endregion + } + + /// + /// This renderer highlights substrings that match a given text filter. + /// + /// + /// Implementation note: + /// This renderer uses the functionality of BaseRenderer to draw the text, and + /// then draws a translucent frame over the top of the already rendered text glyphs. + /// There's no way to draw the matching text in a different font or color in this + /// implementation. + /// + public class HighlightTextRenderer : BaseRenderer, IFilterAwareRenderer { + #region Life and death + + /// + /// Create a HighlightTextRenderer + /// + public HighlightTextRenderer() { + this.FramePen = Pens.DarkGreen; + this.FillBrush = Brushes.Yellow; + } + + /// + /// Create a HighlightTextRenderer + /// + /// + public HighlightTextRenderer(ITextMatchFilter filter) + : this() { + this.Filter = filter; + } + + /// + /// Create a HighlightTextRenderer + /// + /// + [Obsolete("Use HighlightTextRenderer(TextMatchFilter) instead", true)] + public HighlightTextRenderer(string text) {} + + #endregion + + #region Configuration properties + + /// + /// Gets or set how rounded will be the corners of the text match frame + /// + [Category("Appearance"), + DefaultValue(3.0f), + Description("How rounded will be the corners of the text match frame?")] + public float CornerRoundness { + get { return cornerRoundness; } + set { cornerRoundness = value; } + } + + private float cornerRoundness = 3.0f; + + /// + /// Gets or set the brush will be used to paint behind the matched substrings. + /// Set this to null to not fill the frame. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Brush FillBrush { + get { return fillBrush; } + set { fillBrush = value; } + } + + private Brush fillBrush; + + /// + /// Gets or sets the filter that is filtering the ObjectListView and for + /// which this renderer should highlight text + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ITextMatchFilter Filter { + get { return filter; } + set { filter = value; } + } + private ITextMatchFilter filter; + + /// + /// When a filter changes, keep track of the text matching filters + /// + IModelFilter IFilterAwareRenderer.Filter + { + get { return filter; } + set { RegisterNewFilter(value); } + } + + internal void RegisterNewFilter(IModelFilter newFilter) { + TextMatchFilter textFilter = newFilter as TextMatchFilter; + if (textFilter != null) + { + Filter = textFilter; + return; + } + CompositeFilter composite = newFilter as CompositeFilter; + if (composite != null) + { + foreach (TextMatchFilter textSubFilter in composite.TextFilters) + { + Filter = textSubFilter; + return; + } + } + Filter = null; + } + + /// + /// Gets or set the pen will be used to frame the matched substrings. + /// Set this to null to not draw a frame. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Pen FramePen { + get { return framePen; } + set { framePen = value; } + } + + private Pen framePen; + + /// + /// Gets or sets whether the frame around a text match will have rounded corners + /// + [Category("Appearance"), + DefaultValue(true), + Description("Will the frame around a text match will have rounded corners?")] + public bool UseRoundedRectangle { + get { return useRoundedRectangle; } + set { useRoundedRectangle = value; } + } + + private bool useRoundedRectangle = true; + + #endregion + + #region Compatibility properties + + /// + /// Gets or set the text that will be highlighted + /// + [Obsolete("Set the Filter directly rather than just the text", true)] + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public string TextToHighlight { + get { return String.Empty; } + set { } + } + + /// + /// Gets or sets the manner in which substring will be compared. + /// + /// + /// Use this to control if substring matches are case sensitive or insensitive. + [Obsolete("Set the Filter directly rather than just this setting", true)] + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public StringComparison StringComparison { + get { return StringComparison.CurrentCultureIgnoreCase; } + set { } + } + + #endregion + + #region IRenderer interface overrides + + /// + /// Handle a HitTest request after all state information has been initialized + /// + /// + /// + /// + /// + /// + /// + protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return this.StandardGetEditRectangle(g, cellBounds, preferredSize); + } + + #endregion + + #region Rendering + + // This class has two implement two highlighting schemes: one for GDI, another for GDI+. + // Naturally, GDI+ makes the task easier, but we have to provide something for GDI + // since that it is what is normally used. + + /// + /// Draw text using GDI + /// + /// + /// + /// + protected override void DrawTextGdi(Graphics g, Rectangle r, string txt) { + if (this.ShouldDrawHighlighting) + this.DrawGdiTextHighlighting(g, r, txt); + + base.DrawTextGdi(g, r, txt); + } + + /// + /// Draw the highlighted text using GDI + /// + /// + /// + /// + protected virtual void DrawGdiTextHighlighting(Graphics g, Rectangle r, string txt) { + + // TextRenderer puts horizontal padding around the strings, so we need to take + // that into account when measuring strings + const int paddingAdjustment = 6; + + // Cache the font + Font f = this.Font; + + foreach (CharacterRange range in this.Filter.FindAllMatchedRanges(txt)) { + // Measure the text that comes before our substring + Size precedingTextSize = Size.Empty; + if (range.First > 0) { + string precedingText = txt.Substring(0, range.First); + precedingTextSize = TextRenderer.MeasureText(g, precedingText, f, r.Size, NormalTextFormatFlags); + precedingTextSize.Width -= paddingAdjustment; + } + + // Measure the length of our substring (may be different each time due to case differences) + string highlightText = txt.Substring(range.First, range.Length); + Size textToHighlightSize = TextRenderer.MeasureText(g, highlightText, f, r.Size, NormalTextFormatFlags); + textToHighlightSize.Width -= paddingAdjustment; + + float textToHighlightLeft = r.X + precedingTextSize.Width + 1; + float textToHighlightTop = this.AlignVertically(r, textToHighlightSize.Height); + + // Draw a filled frame around our substring + this.DrawSubstringFrame(g, textToHighlightLeft, textToHighlightTop, textToHighlightSize.Width, textToHighlightSize.Height); + } + } + + /// + /// Draw an indication around the given frame that shows a text match + /// + /// + /// + /// + /// + /// + protected virtual void DrawSubstringFrame(Graphics g, float x, float y, float width, float height) { + if (this.UseRoundedRectangle) { + using (GraphicsPath path = this.GetRoundedRect(x, y, width, height, 3.0f)) { + if (this.FillBrush != null) + g.FillPath(this.FillBrush, path); + if (this.FramePen != null) + g.DrawPath(this.FramePen, path); + } + } else { + if (this.FillBrush != null) + g.FillRectangle(this.FillBrush, x, y, width, height); + if (this.FramePen != null) + g.DrawRectangle(this.FramePen, x, y, width, height); + } + } + + /// + /// Draw the text using GDI+ + /// + /// + /// + /// + protected override void DrawTextGdiPlus(Graphics g, Rectangle r, string txt) { + if (this.ShouldDrawHighlighting) + this.DrawGdiPlusTextHighlighting(g, r, txt); + + base.DrawTextGdiPlus(g, r, txt); + } + + /// + /// Draw the highlighted text using GDI+ + /// + /// + /// + /// + protected virtual void DrawGdiPlusTextHighlighting(Graphics g, Rectangle r, string txt) { + // Find the substrings we want to highlight + List ranges = new List(this.Filter.FindAllMatchedRanges(txt)); + + if (ranges.Count == 0) + return; + + using (StringFormat fmt = this.StringFormatForGdiPlus) { + RectangleF rf = r; + fmt.SetMeasurableCharacterRanges(ranges.ToArray()); + Region[] stringRegions = g.MeasureCharacterRanges(txt, this.Font, rf, fmt); + + foreach (Region region in stringRegions) { + RectangleF bounds = region.GetBounds(g); + this.DrawSubstringFrame(g, bounds.X - 1, bounds.Y - 1, bounds.Width + 2, bounds.Height); + } + } + } + + #endregion + + #region Utilities + + /// + /// Gets whether the renderer should actually draw highlighting + /// + protected bool ShouldDrawHighlighting { + get { return this.Column == null || (this.Column.Searchable && this.Filter != null); } + } + + /// + /// Return a GraphicPath that is a round cornered rectangle + /// + /// A round cornered rectangle path + /// If I could rely on people using C# 3.0+, this should be + /// an extension method of GraphicsPath. + /// + /// + /// + /// + /// + protected GraphicsPath GetRoundedRect(float x, float y, float width, float height, float diameter) { + return GetRoundedRect(new RectangleF(x, y, width, height), diameter); + } + + /// + /// Return a GraphicPath that is a round cornered rectangle + /// + /// The rectangle + /// The diameter of the corners + /// A round cornered rectangle path + /// If I could rely on people using C# 3.0+, this should be + /// an extension method of GraphicsPath. + protected GraphicsPath GetRoundedRect(RectangleF rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + if (diameter > 0) { + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + } else { + path.AddRectangle(rect); + } + + return path; + } + + #endregion + } + + /// + /// This class maps a data value to an image that should be drawn for that value. + /// + /// It is useful for drawing data that is represented as an enum or boolean. + public class MappedImageRenderer : BaseRenderer { + /// + /// Return a renderer that draw boolean values using the given images + /// + /// Draw this when our data value is true + /// Draw this when our data value is false + /// A Renderer + public static MappedImageRenderer Boolean(Object trueImage, Object falseImage) { + return new MappedImageRenderer(true, trueImage, false, falseImage); + } + + /// + /// Return a renderer that draw tristate boolean values using the given images + /// + /// Draw this when our data value is true + /// Draw this when our data value is false + /// Draw this when our data value is null + /// A Renderer + public static MappedImageRenderer TriState(Object trueImage, Object falseImage, Object nullImage) { + return new MappedImageRenderer(new Object[] {true, trueImage, false, falseImage, null, nullImage}); + } + + /// + /// Make a new empty renderer + /// + public MappedImageRenderer() { + map = new System.Collections.Hashtable(); + } + + /// + /// Make a new renderer that will show the given image when the given key is the aspect value + /// + /// The data value to be matched + /// The image to be shown when the key is matched + public MappedImageRenderer(Object key, Object image) + : this() { + this.Add(key, image); + } + + /// + /// Make a new renderer that will show the given images when it receives the given keys + /// + /// + /// + /// + /// + public MappedImageRenderer(Object key1, Object image1, Object key2, Object image2) + : this() { + this.Add(key1, image1); + this.Add(key2, image2); + } + + /// + /// Build a renderer from the given array of keys and their matching images + /// + /// An array of key/image pairs + public MappedImageRenderer(Object[] keysAndImages) + : this() { + if ((keysAndImages.GetLength(0) % 2) != 0) + throw new ArgumentException("Array must have key/image pairs"); + + for (int i = 0; i < keysAndImages.GetLength(0); i += 2) + this.Add(keysAndImages[i], keysAndImages[i + 1]); + } + + /// + /// Register the image that should be drawn when our Aspect has the data value. + /// + /// Value that the Aspect must match + /// An ImageSelector -- an int, string or image + public void Add(Object value, Object image) { + if (value == null) + this.nullImage = image; + else + map[value] = image; + } + + /// + /// Render our value + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + r = this.ApplyCellPadding(r); + + ICollection aspectAsCollection = this.Aspect as ICollection; + if (aspectAsCollection == null) + this.RenderOne(g, r, this.Aspect); + else + this.RenderCollection(g, r, aspectAsCollection); + } + + /// + /// Draw a collection of images + /// + /// + /// + /// + protected void RenderCollection(Graphics g, Rectangle r, ICollection imageSelectors) { + ArrayList images = new ArrayList(); + Image image = null; + foreach (Object selector in imageSelectors) { + if (selector == null) + image = this.GetImage(this.nullImage); + else if (map.ContainsKey(selector)) + image = this.GetImage(map[selector]); + else + image = null; + + if (image != null) + images.Add(image); + } + + this.DrawImages(g, r, images); + } + + /// + /// Draw one image + /// + /// + /// + /// + protected void RenderOne(Graphics g, Rectangle r, Object selector) { + Image image = null; + if (selector == null) + image = this.GetImage(this.nullImage); + else if (map.ContainsKey(selector)) + image = this.GetImage(map[selector]); + + if (image != null) + this.DrawAlignedImage(g, r, image); + } + + #region Private variables + + private Hashtable map; // Track the association between values and images + private Object nullImage; // image to be drawn for null values (since null can't be a key) + + #endregion + } + + /// + /// This renderer draws just a checkbox to match the check state of our model object. + /// + public class CheckStateRenderer : BaseRenderer { + /// + /// Draw our cell + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + if (this.Column == null) + return; + r = this.ApplyCellPadding(r); + CheckState state = this.Column.GetCheckState(this.RowObject); + if (this.IsPrinting) { + // Renderers don't work onto printer DCs, so we have to draw the image ourselves + string key = ObjectListView.CHECKED_KEY; + if (state == CheckState.Unchecked) + key = ObjectListView.UNCHECKED_KEY; + if (state == CheckState.Indeterminate) + key = ObjectListView.INDETERMINATE_KEY; + this.DrawAlignedImage(g, r, this.ImageListOrDefault.Images[key]); + } else { + r = this.CalculateCheckBoxBounds(g, r); + CheckBoxRenderer.DrawCheckBox(g, r.Location, this.GetCheckBoxState(state)); + } + } + + + /// + /// Handle the GetEditRectangle request + /// + /// + /// + /// + /// + /// + /// + protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize); + } + + /// + /// Handle the HitTest request + /// + /// + /// + /// + /// + protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + Rectangle r = this.CalculateCheckBoxBounds(g, this.Bounds); + if (r.Contains(x, y)) + hti.HitTestLocation = HitTestLocation.CheckBox; + } + } + + /// + /// Render an image that comes from our data source. + /// + /// The image can be sourced from: + /// + /// a byte-array (normally when the image to be shown is + /// stored as a value in a database) + /// an int, which is treated as an index into the image list + /// a string, which is treated first as a file name, and failing that as an index into the image list + /// an ICollection of ints or strings, which will be drawn as consecutive images + /// + /// If an image is an animated GIF, it's state is stored in the SubItem object. + /// By default, the image renderer does not render animations (it begins life with animations paused). + /// To enable animations, you must call Unpause(). + /// In the current implementation (2009-09), each column showing animated gifs must have a + /// different instance of ImageRenderer assigned to it. You cannot share the same instance of + /// an image renderer between two animated gif columns. If you do, only the last column will be + /// animated. + /// + public class ImageRenderer : BaseRenderer { + /// + /// Make an empty image renderer + /// + public ImageRenderer() { + this.stopwatch = new Stopwatch(); + } + + /// + /// Make an empty image renderer that begins life ready for animations + /// + public ImageRenderer(bool startAnimations) + : this() { + this.Paused = !startAnimations; + } + + /// + /// Finalizer + /// + protected override void Dispose(bool disposing) { + Paused = true; + base.Dispose(disposing); + } + + #region Properties + + /// + /// Should the animations in this renderer be paused? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool Paused { + get { return isPaused; } + set { + if (this.isPaused == value) + return; + + this.isPaused = value; + if (this.isPaused) { + this.StopTickler(); + this.stopwatch.Stop(); + } else { + this.Tickler.Change(1, Timeout.Infinite); + this.stopwatch.Start(); + } + } + } + + private bool isPaused = true; + + private void StopTickler() { + if (this.tickler == null) + return; + + this.tickler.Dispose(); + this.tickler = null; + } + + /// + /// Gets a timer that can be used to trigger redraws on animations + /// + protected Timer Tickler { + get { + if (this.tickler == null) + this.tickler = new System.Threading.Timer(new TimerCallback(this.OnTimer), null, Timeout.Infinite, Timeout.Infinite); + return this.tickler; + } + } + + #endregion + + #region Commands + + /// + /// Pause any animations + /// + public void Pause() { + this.Paused = true; + } + + /// + /// Unpause any animations + /// + public void Unpause() { + this.Paused = false; + } + + #endregion + + #region Drawing + + /// + /// Draw our image + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + + if (this.Aspect == null || this.Aspect == System.DBNull.Value) + return; + r = this.ApplyCellPadding(r); + + if (this.Aspect is System.Byte[]) { + this.DrawAlignedImage(g, r, this.GetImageFromAspect()); + } else { + ICollection imageSelectors = this.Aspect as ICollection; + if (imageSelectors == null) + this.DrawAlignedImage(g, r, this.GetImageFromAspect()); + else + this.DrawImages(g, r, imageSelectors); + } + } + + /// + /// Translate our Aspect into an image. + /// + /// The strategy is: + /// If its a byte array, we treat it as an in-memory image + /// If it's an int, we use that as an index into our image list + /// If it's a string, we try to load a file by that name. If we can't, + /// we use the string as an index into our image list. + /// + /// An image + protected Image GetImageFromAspect() { + // If we've already figured out the image, don't do it again + if (this.OLVSubItem != null && this.OLVSubItem.ImageSelector is Image) { + if (this.OLVSubItem.AnimationState == null) + return (Image) this.OLVSubItem.ImageSelector; + else + return this.OLVSubItem.AnimationState.image; + } + + // Try to convert our Aspect into an Image + // If its a byte array, we treat it as an in-memory image + // If it's an int, we use that as an index into our image list + // If it's a string, we try to find a file by that name. + // If we can't, we use the string as an index into our image list. + Image image = this.Aspect as Image; + if (image != null) { + // Don't do anything else + } else if (this.Aspect is System.Byte[]) { + using (MemoryStream stream = new MemoryStream((System.Byte[]) this.Aspect)) { + try { + image = Image.FromStream(stream); + } + catch (ArgumentException) { + // ignore + } + } + } else if (this.Aspect is Int32) { + image = this.GetImage(this.Aspect); + } else { + String str = this.Aspect as String; + if (!String.IsNullOrEmpty(str)) { + try { + image = Image.FromFile(str); + } + catch (FileNotFoundException) { + image = this.GetImage(this.Aspect); + } + catch (OutOfMemoryException) { + image = this.GetImage(this.Aspect); + } + } + } + + // If this image is an animation, initialize the animation process + if (this.OLVSubItem != null && AnimationState.IsAnimation(image)) { + this.OLVSubItem.AnimationState = new AnimationState(image); + } + + // Cache the image so we don't repeat this dreary process + if (this.OLVSubItem != null) + this.OLVSubItem.ImageSelector = image; + + return image; + } + + #endregion + + #region Events + + /// + /// This is the method that is invoked by the timer. It basically switches control to the listview thread. + /// + /// not used + public void OnTimer(Object state) { + + if (this.IsListViewDead) + return; + + if (this.Paused) + return; + + if (this.ListView.InvokeRequired) + this.ListView.Invoke((MethodInvoker) delegate { this.OnTimer(state); }); + else + this.OnTimerInThread(); + } + + private bool IsListViewDead { + get { + // Apply a whole heap of sanity checks, which basically ensure that the ListView is still alive + return this.ListView == null || + this.ListView.Disposing || + this.ListView.IsDisposed || + !this.ListView.IsHandleCreated; + } + } + + /// + /// This is the OnTimer callback, but invoked in the same thread as the creator of the ListView. + /// This method can use all of ListViews methods without creating a CrossThread exception. + /// + protected void OnTimerInThread() { + // MAINTAINER NOTE: This method must renew the tickler. If it doesn't the animations will stop. + + // If this listview has been destroyed, we can't do anything, so we return without + // renewing the tickler, effectively killing all animations on this renderer + + if (this.IsListViewDead) + return; + + if (this.Paused) + return; + + // If we're not in Detail view or our column has been removed from the list, + // we can't do anything at the moment, but we still renew the tickler because the view may change later. + if (this.ListView.View != System.Windows.Forms.View.Details || this.Column == null || this.Column.Index < 0) { + this.Tickler.Change(1000, Timeout.Infinite); + return; + } + + long elapsedMilliseconds = this.stopwatch.ElapsedMilliseconds; + int subItemIndex = this.Column.Index; + long nextCheckAt = elapsedMilliseconds + 1000; // wait at most one second before checking again + Rectangle updateRect = new Rectangle(); // what part of the view must be updated to draw the changed gifs? + + // Run through all the subitems in the view for our column, and for each one that + // has an animation attached to it, see if the frame needs updating. + + for (int i = 0; i < this.ListView.GetItemCount(); i++) { + OLVListItem lvi = this.ListView.GetItem(i); + + // Get the animation state from the subitem. If there isn't an animation state, skip this row. + OLVListSubItem lvsi = lvi.GetSubItem(subItemIndex); + AnimationState state = lvsi.AnimationState; + if (state == null || !state.IsValid) + continue; + + // Has this frame of the animation expired? + if (elapsedMilliseconds >= state.currentFrameExpiresAt) { + state.AdvanceFrame(elapsedMilliseconds); + + // Track the area of the view that needs to be redrawn to show the changed images + if (updateRect.IsEmpty) + updateRect = lvsi.Bounds; + else + updateRect = Rectangle.Union(updateRect, lvsi.Bounds); + } + + // Remember the minimum time at which a frame is next due to change + nextCheckAt = Math.Min(nextCheckAt, state.currentFrameExpiresAt); + } + + // Update the part of the listview where frames have changed + if (!updateRect.IsEmpty) + this.ListView.Invalidate(updateRect); + + // Renew the tickler in time for the next frame change + this.Tickler.Change(nextCheckAt - elapsedMilliseconds, Timeout.Infinite); + } + + #endregion + + /// + /// Instances of this class kept track of the animation state of a single image. + /// + internal class AnimationState { + private const int PropertyTagTypeShort = 3; + private const int PropertyTagTypeLong = 4; + private const int PropertyTagFrameDelay = 0x5100; + private const int PropertyTagLoopCount = 0x5101; + + /// + /// Is the given image an animation + /// + /// The image to be tested + /// Is the image an animation? + public static bool IsAnimation(Image image) { + if (image == null) + return false; + else + return (new List(image.FrameDimensionsList)).Contains(FrameDimension.Time.Guid); + } + + /// + /// Create an AnimationState in a quiet state + /// + public AnimationState() { + this.imageDuration = new List(); + } + + /// + /// Create an animation state for the given image, which may or may not + /// be an animation + /// + /// The image to be rendered + public AnimationState(Image image) + : this() { + if (!AnimationState.IsAnimation(image)) + return; + + // How many frames in the animation? + this.image = image; + this.frameCount = this.image.GetFrameCount(FrameDimension.Time); + + // Find the delay between each frame. + // The delays are stored an array of 4-byte ints. Each int is the + // number of 1/100th of a second that should elapsed before the frame expires + foreach (PropertyItem pi in this.image.PropertyItems) { + if (pi.Id == PropertyTagFrameDelay) { + for (int i = 0; i < pi.Len; i += 4) { + //TODO: There must be a better way to convert 4-bytes to an int + int delay = (pi.Value[i + 3] << 24) + (pi.Value[i + 2] << 16) + (pi.Value[i + 1] << 8) + pi.Value[i]; + this.imageDuration.Add(delay * 10); // store delays as milliseconds + } + break; + } + } + + // There should be as many frame durations as frames + Debug.Assert(this.imageDuration.Count == this.frameCount, "There should be as many frame durations as there are frames."); + } + + /// + /// Does this state represent a valid animation + /// + public bool IsValid { + get { return (this.image != null && this.frameCount > 0); } + } + + /// + /// Advance our images current frame and calculate when it will expire + /// + public void AdvanceFrame(long millisecondsNow) { + this.currentFrame = (this.currentFrame + 1) % this.frameCount; + this.currentFrameExpiresAt = millisecondsNow + this.imageDuration[this.currentFrame]; + this.image.SelectActiveFrame(FrameDimension.Time, this.currentFrame); + } + + internal int currentFrame; + internal long currentFrameExpiresAt; + internal Image image; + internal List imageDuration; + internal int frameCount; + } + + #region Private variables + + private System.Threading.Timer tickler; // timer used to tickle the animations + private Stopwatch stopwatch; // clock used to time the animation frame changes + + #endregion + } + + /// + /// Render our Aspect as a progress bar + /// + public class BarRenderer : BaseRenderer { + #region Constructors + + /// + /// Make a BarRenderer + /// + public BarRenderer() + : base() {} + + /// + /// Make a BarRenderer for the given range of data values + /// + public BarRenderer(int minimum, int maximum) + : this() { + this.MinimumValue = minimum; + this.MaximumValue = maximum; + } + + /// + /// Make a BarRenderer using a custom bar scheme + /// + public BarRenderer(Pen pen, Brush brush) + : this() { + this.Pen = pen; + this.Brush = brush; + this.UseStandardBar = false; + } + + /// + /// Make a BarRenderer using a custom bar scheme + /// + public BarRenderer(int minimum, int maximum, Pen pen, Brush brush) + : this(minimum, maximum) { + this.Pen = pen; + this.Brush = brush; + this.UseStandardBar = false; + } + + /// + /// Make a BarRenderer that uses a horizontal gradient + /// + public BarRenderer(Pen pen, Color start, Color end) + : this() { + this.Pen = pen; + this.SetGradient(start, end); + } + + /// + /// Make a BarRenderer that uses a horizontal gradient + /// + public BarRenderer(int minimum, int maximum, Pen pen, Color start, Color end) + : this(minimum, maximum) { + this.Pen = pen; + this.SetGradient(start, end); + } + + #endregion + + #region Configuration Properties + + /// + /// Should this bar be drawn in the system style? + /// + [Category("ObjectListView"), + Description("Should this bar be drawn in the system style?"), + DefaultValue(true)] + public bool UseStandardBar { + get { return useStandardBar; } + set { useStandardBar = value; } + } + + private bool useStandardBar = true; + + /// + /// How many pixels in from our cell border will this bar be drawn + /// + [Category("ObjectListView"), + Description("How many pixels in from our cell border will this bar be drawn"), + DefaultValue(2)] + public int Padding { + get { return padding; } + set { padding = value; } + } + + private int padding = 2; + + /// + /// What color will be used to fill the interior of the control before the + /// progress bar is drawn? + /// + [Category("ObjectListView"), + Description("The color of the interior of the bar"), + DefaultValue(typeof (Color), "AliceBlue")] + public Color BackgroundColor { + get { return backgroundColor; } + set { backgroundColor = value; } + } + + private Color backgroundColor = Color.AliceBlue; + + /// + /// What color should the frame of the progress bar be? + /// + [Category("ObjectListView"), + Description("What color should the frame of the progress bar be"), + DefaultValue(typeof (Color), "Black")] + public Color FrameColor { + get { return frameColor; } + set { frameColor = value; } + } + + private Color frameColor = Color.Black; + + /// + /// How many pixels wide should the frame of the progress bar be? + /// + [Category("ObjectListView"), + Description("How many pixels wide should the frame of the progress bar be"), + DefaultValue(1.0f)] + public float FrameWidth { + get { return frameWidth; } + set { frameWidth = value; } + } + + private float frameWidth = 1.0f; + + /// + /// What color should the 'filled in' part of the progress bar be? + /// + /// This is only used if GradientStartColor is Color.Empty + [Category("ObjectListView"), + Description("What color should the 'filled in' part of the progress bar be"), + DefaultValue(typeof (Color), "BlueViolet")] + public Color FillColor { + get { return fillColor; } + set { fillColor = value; } + } + + private Color fillColor = Color.BlueViolet; + + /// + /// Use a gradient to fill the progress bar starting with this color + /// + [Category("ObjectListView"), + Description("Use a gradient to fill the progress bar starting with this color"), + DefaultValue(typeof (Color), "CornflowerBlue")] + public Color GradientStartColor { + get { return startColor; } + set { startColor = value; } + } + + private Color startColor = Color.CornflowerBlue; + + /// + /// Use a gradient to fill the progress bar ending with this color + /// + [Category("ObjectListView"), + Description("Use a gradient to fill the progress bar ending with this color"), + DefaultValue(typeof (Color), "DarkBlue")] + public Color GradientEndColor { + get { return endColor; } + set { endColor = value; } + } + + private Color endColor = Color.DarkBlue; + + /// + /// Regardless of how wide the column become the progress bar will never be wider than this + /// + [Category("Behavior"), + Description("The progress bar will never be wider than this"), + DefaultValue(100)] + public int MaximumWidth { + get { return maximumWidth; } + set { maximumWidth = value; } + } + + private int maximumWidth = 100; + + /// + /// Regardless of how high the cell is the progress bar will never be taller than this + /// + [Category("Behavior"), + Description("The progress bar will never be taller than this"), + DefaultValue(16)] + public int MaximumHeight { + get { return maximumHeight; } + set { maximumHeight = value; } + } + + private int maximumHeight = 16; + + /// + /// The minimum data value expected. Values less than this will given an empty bar + /// + [Category("Behavior"), + Description("The minimum data value expected. Values less than this will given an empty bar"), + DefaultValue(0.0)] + public double MinimumValue { + get { return minimumValue; } + set { minimumValue = value; } + } + + private double minimumValue = 0.0; + + /// + /// The maximum value for the range. Values greater than this will give a full bar + /// + [Category("Behavior"), + Description("The maximum value for the range. Values greater than this will give a full bar"), + DefaultValue(100.0)] + public double MaximumValue { + get { return maximumValue; } + set { maximumValue = value; } + } + + private double maximumValue = 100.0; + + #endregion + + #region Public Properties (non-IDE) + + /// + /// The Pen that will draw the frame surrounding this bar + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Pen Pen { + get { + if (this.pen == null && !this.FrameColor.IsEmpty) + return new Pen(this.FrameColor, this.FrameWidth); + else + return this.pen; + } + set { this.pen = value; } + } + + private Pen pen; + + /// + /// The brush that will be used to fill the bar + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Brush Brush { + get { + if (this.brush == null && !this.FillColor.IsEmpty) + return new SolidBrush(this.FillColor); + else + return this.brush; + } + set { this.brush = value; } + } + + private Brush brush; + + /// + /// The brush that will be used to fill the background of the bar + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Brush BackgroundBrush { + get { + if (this.backgroundBrush == null && !this.BackgroundColor.IsEmpty) + return new SolidBrush(this.BackgroundColor); + else + return this.backgroundBrush; + } + set { this.backgroundBrush = value; } + } + + private Brush backgroundBrush; + + #endregion + + /// + /// Draw this progress bar using a gradient + /// + /// + /// + public void SetGradient(Color start, Color end) { + this.GradientStartColor = start; + this.GradientEndColor = end; + } + + /// + /// Draw our aspect + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + + r = this.ApplyCellPadding(r); + + Rectangle frameRect = Rectangle.Inflate(r, 0 - this.Padding, 0 - this.Padding); + frameRect.Width = Math.Min(frameRect.Width, this.MaximumWidth); + frameRect.Height = Math.Min(frameRect.Height, this.MaximumHeight); + frameRect = this.AlignRectangle(r, frameRect); + + // Convert our aspect to a numeric value + IConvertible convertable = this.Aspect as IConvertible; + if (convertable == null) + return; + double aspectValue = convertable.ToDouble(NumberFormatInfo.InvariantInfo); + + Rectangle fillRect = Rectangle.Inflate(frameRect, -1, -1); + if (aspectValue <= this.MinimumValue) + fillRect.Width = 0; + else if (aspectValue < this.MaximumValue) + fillRect.Width = (int) (fillRect.Width * (aspectValue - this.MinimumValue) / this.MaximumValue); + + // MS-themed progress bars don't work when printing + if (this.UseStandardBar && ProgressBarRenderer.IsSupported && !this.IsPrinting) { + ProgressBarRenderer.DrawHorizontalBar(g, frameRect); + ProgressBarRenderer.DrawHorizontalChunks(g, fillRect); + } else { + g.FillRectangle(this.BackgroundBrush, frameRect); + if (fillRect.Width > 0) { + // FillRectangle fills inside the given rectangle, so expand it a little + fillRect.Width++; + fillRect.Height++; + if (this.GradientStartColor == Color.Empty) + g.FillRectangle(this.Brush, fillRect); + else { + using (LinearGradientBrush gradient = new LinearGradientBrush(frameRect, this.GradientStartColor, this.GradientEndColor, LinearGradientMode.Horizontal)) { + g.FillRectangle(gradient, fillRect); + } + } + } + g.DrawRectangle(this.Pen, frameRect); + } + } + + /// + /// Handle the GetEditRectangle request + /// + /// + /// + /// + /// + /// + /// + protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize); + } + } + + + /// + /// An ImagesRenderer draws zero or more images depending on the data returned by its Aspect. + /// + /// This renderer's Aspect must return a ICollection of ints, strings or Images, + /// each of which will be drawn horizontally one after the other. + /// As of v2.1, this functionality has been absorbed into ImageRenderer and this is now an + /// empty shell, solely for backwards compatibility. + /// + [ToolboxItem(false)] + public class ImagesRenderer : ImageRenderer {} + + /// + /// A MultiImageRenderer draws the same image a number of times based on our data value + /// + /// The stars in the Rating column of iTunes is a good example of this type of renderer. + public class MultiImageRenderer : BaseRenderer { + /// + /// Make a quiet renderer + /// + public MultiImageRenderer() + : base() {} + + /// + /// Make an image renderer that will draw the indicated image, at most maxImages times. + /// + /// + /// + /// + /// + public MultiImageRenderer(Object imageSelector, int maxImages, int minValue, int maxValue) + : this() { + this.ImageSelector = imageSelector; + this.MaxNumberImages = maxImages; + this.MinimumValue = minValue; + this.MaximumValue = maxValue; + } + + #region Configuration Properties + + /// + /// The index of the image that should be drawn + /// + [Category("Behavior"), + Description("The index of the image that should be drawn"), + DefaultValue(-1)] + public int ImageIndex { + get { + if (imageSelector is Int32) + return (Int32) imageSelector; + else + return -1; + } + set { imageSelector = value; } + } + + /// + /// The name of the image that should be drawn + /// + [Category("Behavior"), + Description("The index of the image that should be drawn"), + DefaultValue(null)] + public string ImageName { + get { return imageSelector as String; } + set { imageSelector = value; } + } + + /// + /// The image selector that will give the image to be drawn + /// + /// Like all image selectors, this can be an int, string or Image + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Object ImageSelector { + get { return imageSelector; } + set { imageSelector = value; } + } + + private Object imageSelector; + + /// + /// What is the maximum number of images that this renderer should draw? + /// + [Category("Behavior"), + Description("The maximum number of images that this renderer should draw"), + DefaultValue(10)] + public int MaxNumberImages { + get { return maxNumberImages; } + set { maxNumberImages = value; } + } + + private int maxNumberImages = 10; + + /// + /// Values less than or equal to this will have 0 images drawn + /// + [Category("Behavior"), + Description("Values less than or equal to this will have 0 images drawn"), + DefaultValue(0)] + public int MinimumValue { + get { return minimumValue; } + set { minimumValue = value; } + } + + private int minimumValue = 0; + + /// + /// Values greater than or equal to this will have MaxNumberImages images drawn + /// + [Category("Behavior"), + Description("Values greater than or equal to this will have MaxNumberImages images drawn"), + DefaultValue(100)] + public int MaximumValue { + get { return maximumValue; } + set { maximumValue = value; } + } + + private int maximumValue = 100; + + #endregion + + /// + /// Draw our data value + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + r = this.ApplyCellPadding(r); + + Image image = this.GetImage(this.ImageSelector); + if (image == null) + return; + + // Convert our aspect to a numeric value + IConvertible convertable = this.Aspect as IConvertible; + if (convertable == null) + return; + double aspectValue = convertable.ToDouble(NumberFormatInfo.InvariantInfo); + + // Calculate how many images we need to draw to represent our aspect value + int numberOfImages; + if (aspectValue <= this.MinimumValue) + numberOfImages = 0; + else if (aspectValue < this.MaximumValue) + numberOfImages = 1 + (int) (this.MaxNumberImages * (aspectValue - this.MinimumValue) / this.MaximumValue); + else + numberOfImages = this.MaxNumberImages; + + // If we need to shrink the image, what will its on-screen dimensions be? + int imageScaledWidth = image.Width; + int imageScaledHeight = image.Height; + if (r.Height < image.Height) { + imageScaledWidth = (int) ((float) image.Width * (float) r.Height / (float) image.Height); + imageScaledHeight = r.Height; + } + // Calculate where the images should be drawn + Rectangle imageBounds = r; + imageBounds.Width = (this.MaxNumberImages * (imageScaledWidth + this.Spacing)) - this.Spacing; + imageBounds.Height = imageScaledHeight; + imageBounds = this.AlignRectangle(r, imageBounds); + + // Finally, draw the images + Rectangle singleImageRect = new Rectangle(imageBounds.X, imageBounds.Y, imageScaledWidth, imageScaledHeight); + Color backgroundColor = GetBackgroundColor(); + for (int i = 0; i < numberOfImages; i++) { + if (this.ListItem.Enabled) { + this.DrawImage(g, singleImageRect, this.ImageSelector); + } else + ControlPaint.DrawImageDisabled(g, image, singleImageRect.X, singleImageRect.Y, backgroundColor); + singleImageRect.X += (imageScaledWidth + this.Spacing); + } + } + } + + + /// + /// A class to render a value that contains a bitwise-OR'ed collection of values. + /// + public class FlagRenderer : BaseRenderer { + /// + /// Register the given image to the given value + /// + /// When this flag is present... + /// ...draw this image + public void Add(Object key, Object imageSelector) { + Int32 k2 = ((IConvertible) key).ToInt32(NumberFormatInfo.InvariantInfo); + + this.imageMap[k2] = imageSelector; + this.keysInOrder.Remove(k2); + this.keysInOrder.Add(k2); + } + + /// + /// Draw the flags + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + + IConvertible convertable = this.Aspect as IConvertible; + if (convertable == null) + return; + + r = this.ApplyCellPadding(r); + + Int32 v2 = convertable.ToInt32(NumberFormatInfo.InvariantInfo); + ArrayList images = new ArrayList(); + foreach (Int32 key in this.keysInOrder) { + if ((v2 & key) == key) { + Image image = this.GetImage(this.imageMap[key]); + if (image != null) + images.Add(image); + } + } + if (images.Count > 0) + this.DrawImages(g, r, images); + } + + /// + /// Do the actual work of hit testing. Subclasses should override this rather than HitTest() + /// + /// + /// + /// + /// + protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + IConvertible convertable = this.Aspect as IConvertible; + if (convertable == null) + return; + + Int32 v2 = convertable.ToInt32(NumberFormatInfo.InvariantInfo); + + Point pt = this.Bounds.Location; + foreach (Int32 key in this.keysInOrder) { + if ((v2 & key) == key) { + Image image = this.GetImage(this.imageMap[key]); + if (image != null) { + Rectangle imageRect = new Rectangle(pt, image.Size); + if (imageRect.Contains(x, y)) { + hti.UserData = key; + return; + } + pt.X += (image.Width + this.Spacing); + } + } + } + } + + private List keysInOrder = new List(); + private Dictionary imageMap = new Dictionary(); + } + + /// + /// This renderer draws an image, a single line title, and then multi-line description + /// under the title. + /// + /// + /// This class works best with FullRowSelect = true. + /// It's not designed to work with cell editing -- it will work but will look odd. + /// + /// It's not RightToLeft friendly. + /// + /// + public class DescribedTaskRenderer : BaseRenderer, IFilterAwareRenderer + { + private readonly StringFormat noWrapStringFormat; + private readonly HighlightTextRenderer highlightTextRenderer = new HighlightTextRenderer(); + + /// + /// Create a DescribedTaskRenderer + /// + public DescribedTaskRenderer() { + this.noWrapStringFormat = new StringFormat(StringFormatFlags.NoWrap); + this.noWrapStringFormat.Trimming = StringTrimming.EllipsisCharacter; + this.noWrapStringFormat.Alignment = StringAlignment.Near; + this.noWrapStringFormat.LineAlignment = StringAlignment.Near; + this.highlightTextRenderer.CellVerticalAlignment = StringAlignment.Near; + } + + #region Configuration properties + + /// + /// Should text be rendered using GDI routines? This makes the text look more + /// like a native List view control. + /// + public override bool UseGdiTextRendering + { + get { return base.UseGdiTextRendering; } + set + { + base.UseGdiTextRendering = value; + this.highlightTextRenderer.UseGdiTextRendering = value; + } + } + + /// + /// Gets or set the font that will be used to draw the title of the task + /// + /// If this is null, the ListView's font will be used + [Category("ObjectListView"), + Description("The font that will be used to draw the title of the task"), + DefaultValue(null)] + public Font TitleFont { + get { return titleFont; } + set { titleFont = value; } + } + + private Font titleFont; + + /// + /// Return a font that has been set for the title or a reasonable default + /// + [Browsable(false)] + public Font TitleFontOrDefault { + get { return this.TitleFont ?? this.ListView.Font; } + } + + /// + /// Gets or set the color of the title of the task + /// + /// This color is used when the task is not selected or when the listview + /// has a translucent selection mechanism. + [Category("ObjectListView"), + Description("The color of the title"), + DefaultValue(typeof (Color), "")] + public Color TitleColor { + get { return titleColor; } + set { titleColor = value; } + } + + private Color titleColor; + + /// + /// Return the color of the title of the task or a reasonable default + /// + [Browsable(false)] + public Color TitleColorOrDefault { + get { + if (!this.ListItem.Enabled) + return this.SubItem.ForeColor; + if (this.IsItemSelected || this.TitleColor.IsEmpty) + return this.GetForegroundColor(); + + return this.TitleColor; + } + } + + /// + /// Gets or set the font that will be used to draw the description of the task + /// + /// If this is null, the ListView's font will be used + [Category("ObjectListView"), + Description("The font that will be used to draw the description of the task"), + DefaultValue(null)] + public Font DescriptionFont { + get { return descriptionFont; } + set { descriptionFont = value; } + } + + private Font descriptionFont; + + /// + /// Return a font that has been set for the title or a reasonable default + /// + [Browsable(false)] + public Font DescriptionFontOrDefault { + get { return this.DescriptionFont ?? this.ListView.Font; } + } + + /// + /// Gets or set the color of the description of the task + /// + /// This color is used when the task is not selected or when the listview + /// has a translucent selection mechanism. + [Category("ObjectListView"), + Description("The color of the description"), + DefaultValue(typeof (Color), "")] + public Color DescriptionColor { + get { return descriptionColor; } + set { descriptionColor = value; } + } + private Color descriptionColor = Color.Empty; + + /// + /// Return the color of the description of the task or a reasonable default + /// + [Browsable(false)] + public Color DescriptionColorOrDefault { + get { + if (!this.ListItem.Enabled) + return this.SubItem.ForeColor; + if (this.IsItemSelected && !this.ListView.UseTranslucentSelection) + return this.GetForegroundColor(); + return this.DescriptionColor.IsEmpty ? defaultDescriptionColor : this.DescriptionColor; + } + } + private static Color defaultDescriptionColor = Color.FromArgb(45, 46, 49); + + /// + /// Gets or sets the number of pixels that will be left between the image and the text + /// + [Category("ObjectListView"), + Description("The number of pixels that will be left between the image and the text"), + DefaultValue(4)] + public int ImageTextSpace + { + get { return imageTextSpace; } + set { imageTextSpace = value; } + } + private int imageTextSpace = 4; + + /// + /// Gets or sets the number of pixels that will be left between the title and the description + /// + [Category("ObjectListView"), + Description("The number of pixels that that will be left between the title and the description"), + DefaultValue(2)] + public int TitleDescriptionSpace + { + get { return titleDescriptionSpace; } + set { titleDescriptionSpace = value; } + } + private int titleDescriptionSpace = 2; + + /// + /// Gets or sets the name of the aspect of the model object that contains the task description + /// + [Category("ObjectListView"), + Description("The name of the aspect of the model object that contains the task description"), + DefaultValue(null)] + public string DescriptionAspectName { + get { return descriptionAspectName; } + set { descriptionAspectName = value; } + } + private string descriptionAspectName; + + #endregion + + #region Text highlighting + + /// + /// Gets or sets the filter that is filtering the ObjectListView and for + /// which this renderer should highlight text + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ITextMatchFilter Filter + { + get { return this.highlightTextRenderer.Filter; } + set { this.highlightTextRenderer.Filter = value; } + } + + /// + /// When a filter changes, keep track of the text matching filters + /// + IModelFilter IFilterAwareRenderer.Filter { + get { return this.Filter; } + set { this.highlightTextRenderer.RegisterNewFilter(value); } + } + + #endregion + + #region Calculating + + /// + /// Fetch the description from the model class + /// + /// + /// + public virtual string GetDescription(object model) { + if (String.IsNullOrEmpty(this.DescriptionAspectName)) + return String.Empty; + + if (this.descriptionGetter == null) + this.descriptionGetter = new Munger(this.DescriptionAspectName); + + return this.descriptionGetter.GetValue(model) as string; + } + private Munger descriptionGetter; + + #endregion + + #region Rendering + + /// + /// + /// + /// + /// + /// + public override void ConfigureSubItem(DrawListViewSubItemEventArgs e, Rectangle cellBounds, object model) { + base.ConfigureSubItem(e, cellBounds, model); + this.highlightTextRenderer.ConfigureSubItem(e, cellBounds, model); + } + + /// + /// Draw our item + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + r = this.ApplyCellPadding(r); + this.DrawDescribedTask(g, r, this.GetText(), this.GetDescription(this.RowObject), this.GetImageSelector()); + } + + /// + /// Draw the task + /// + /// + /// + /// + /// + /// + protected virtual void DrawDescribedTask(Graphics g, Rectangle r, string title, string description, object imageSelector) { + + //Debug.WriteLine(String.Format("DrawDescribedTask({0}, {1}, {2}, {3})", r, title, description, imageSelector)); + + // Draw the image if one's been given + Rectangle textBounds = r; + if (imageSelector != null) { + int imageWidth = this.DrawImage(g, r, imageSelector); + int gapToText = imageWidth + this.ImageTextSpace; + textBounds.X += gapToText; + textBounds.Width -= gapToText; + } + + // Draw the title + if (!String.IsNullOrEmpty(title)) { + using (SolidBrush b = new SolidBrush(this.TitleColorOrDefault)) { + this.highlightTextRenderer.CanWrap = false; + this.highlightTextRenderer.Font = this.TitleFontOrDefault; + this.highlightTextRenderer.TextBrush = b; + this.highlightTextRenderer.DrawText(g, textBounds, title); + } + + // How tall was the title? + SizeF size = g.MeasureString(title, this.TitleFontOrDefault, textBounds.Width, this.noWrapStringFormat); + int pixelsToDescription = this.TitleDescriptionSpace + (int)size.Height; + textBounds.Y += pixelsToDescription; + textBounds.Height -= pixelsToDescription; + } + + // Draw the description + if (!String.IsNullOrEmpty(description)) { + using (SolidBrush b = new SolidBrush(this.DescriptionColorOrDefault)) { + this.highlightTextRenderer.CanWrap = true; + this.highlightTextRenderer.Font = this.DescriptionFontOrDefault; + this.highlightTextRenderer.TextBrush = b; + this.highlightTextRenderer.DrawText(g, textBounds, description); + } + } + + //g.DrawRectangle(Pens.OrangeRed, r); + } + + #endregion + + #region Hit Testing + + /// + /// Handle the HitTest request + /// + /// + /// + /// + /// + protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + if (this.Bounds.Contains(x, y)) + hti.HitTestLocation = HitTestLocation.Text; + } + + #endregion + } + + /// + /// This renderer draws a functioning button in its cell + /// + public class ColumnButtonRenderer : BaseRenderer { + + #region Properties + + /// + /// Gets or sets how each button will be sized + /// + [Category("ObjectListView"), + Description("How each button will be sized"), + DefaultValue(OLVColumn.ButtonSizingMode.TextBounds)] + public OLVColumn.ButtonSizingMode SizingMode + { + get { return this.sizingMode; } + set { this.sizingMode = value; } + } + private OLVColumn.ButtonSizingMode sizingMode = OLVColumn.ButtonSizingMode.TextBounds; + + /// + /// Gets or sets the size of the button when the SizingMode is FixedBounds + /// + /// If this is not set, the bounds of the cell will be used + [Category("ObjectListView"), + Description("The size of the button when the SizingMode is FixedBounds"), + DefaultValue(null)] + public Size? ButtonSize + { + get { return this.buttonSize; } + set { this.buttonSize = value; } + } + private Size? buttonSize; + + /// + /// Gets or sets the extra space that surrounds the cell when the SizingMode is TextBounds + /// + [Category("ObjectListView"), + Description("The extra space that surrounds the cell when the SizingMode is TextBounds")] + public Size? ButtonPadding + { + get { return this.buttonPadding; } + set { this.buttonPadding = value; } + } + private Size? buttonPadding = new Size(10, 10); + + private Size ButtonPaddingOrDefault { + get { return this.ButtonPadding ?? new Size(10, 10); } + } + + /// + /// Gets or sets the maximum width that a button can occupy. + /// -1 means there is no maximum width. + /// + /// This is only considered when the SizingMode is TextBounds + [Category("ObjectListView"), + Description("The maximum width that a button can occupy when the SizingMode is TextBounds"), + DefaultValue(-1)] + public int MaxButtonWidth + { + get { return this.maxButtonWidth; } + set { this.maxButtonWidth = value; } + } + private int maxButtonWidth = -1; + + /// + /// Gets or sets the minimum width that a button can occupy. + /// -1 means there is no minimum width. + /// + /// This is only considered when the SizingMode is TextBounds + [Category("ObjectListView"), + Description("The minimum width that a button can be when the SizingMode is TextBounds"), + DefaultValue(-1)] + public int MinButtonWidth { + get { return this.minButtonWidth; } + set { this.minButtonWidth = value; } + } + private int minButtonWidth = -1; + + #endregion + + #region Rendering + + /// + /// Calculate the size of the contents + /// + /// + /// + /// + protected override Size CalculateContentSize(Graphics g, Rectangle r) { + if (this.SizingMode == OLVColumn.ButtonSizingMode.CellBounds) + return r.Size; + + if (this.SizingMode == OLVColumn.ButtonSizingMode.FixedBounds) + return this.ButtonSize ?? r.Size; + + // Ok, SizingMode must be TextBounds. So figure out the size of the text + Size textSize = this.CalculateTextSize(g, this.GetText(), r.Width); + + // Allow for padding and max width + textSize.Height += this.ButtonPaddingOrDefault.Height * 2; + textSize.Width += this.ButtonPaddingOrDefault.Width * 2; + if (this.MaxButtonWidth != -1 && textSize.Width > this.MaxButtonWidth) + textSize.Width = this.MaxButtonWidth; + if (textSize.Width < this.MinButtonWidth) + textSize.Width = this.MinButtonWidth; + + return textSize; + } + + /// + /// Draw the button + /// + /// + /// + protected override void DrawImageAndText(Graphics g, Rectangle r) { + TextFormatFlags textFormatFlags = TextFormatFlags.HorizontalCenter | + TextFormatFlags.VerticalCenter | + TextFormatFlags.EndEllipsis | + TextFormatFlags.NoPadding | + TextFormatFlags.SingleLine | + TextFormatFlags.PreserveGraphicsTranslateTransform; + if (this.ListView.RightToLeftLayout) + textFormatFlags |= TextFormatFlags.RightToLeft; + + string buttonText = GetText(); + if (!String.IsNullOrEmpty(buttonText)) + ButtonRenderer.DrawButton(g, r, buttonText, this.Font, textFormatFlags, false, CalculatePushButtonState()); + } + + /// + /// What part of the control is under the given point? + /// + /// + /// + /// + /// + /// + protected override void StandardHitTest(Graphics g, OlvListViewHitTestInfo hti, Rectangle bounds, int x, int y) { + Rectangle r = ApplyCellPadding(bounds); + if (r.Contains(x, y)) + hti.HitTestLocation = HitTestLocation.Button; + } + + /// + /// What is the state of the button? + /// + /// + protected PushButtonState CalculatePushButtonState() { + if (!this.ListItem.Enabled && !this.Column.EnableButtonWhenItemIsDisabled) + return PushButtonState.Disabled; + + if (this.IsButtonHot) + return ObjectListView.IsLeftMouseDown ? PushButtonState.Pressed : PushButtonState.Hot; + + return PushButtonState.Normal; + } + + /// + /// Is the mouse over the button? + /// + protected bool IsButtonHot { + get { + return this.IsCellHot && this.ListView.HotCellHitLocation == HitTestLocation.Button; + } + } + + #endregion + } +} diff --git a/ObjectListView/Rendering/Styles.cs b/ObjectListView/Rendering/Styles.cs new file mode 100644 index 00000000..bc1daa22 --- /dev/null +++ b/ObjectListView/Rendering/Styles.cs @@ -0,0 +1,400 @@ +/* + * Styles - A style is a group of formatting attributes that can be applied to a row or a cell + * + * Author: Phillip Piper + * Date: 29/07/2009 23:09 + * + * Change log: + * v2.4 + * 2010-03-23 JPP - Added HeaderFormatStyle and support + * v2.3 + * 2009-08-15 JPP - Added Decoration and Overlay properties to HotItemStyle + * 2009-07-29 JPP - Initial version + * + * To do: + * - These should be more generally available. It should be possible to do something like this: + * this.olv.GetItem(i).Style = new ItemStyle(); + * this.olv.GetItem(i).GetSubItem(j).Style = new CellStyle(); + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// The common interface supported by all style objects + /// + public interface IItemStyle + { + /// + /// Gets or set the font that will be used by this style + /// + Font Font { get; set; } + + /// + /// Gets or set the font style + /// + FontStyle FontStyle { get; set; } + + /// + /// Gets or sets the ForeColor + /// + Color ForeColor { get; set; } + + /// + /// Gets or sets the BackColor + /// + Color BackColor { get; set; } + } + + /// + /// Basic implementation of IItemStyle + /// + public class SimpleItemStyle : System.ComponentModel.Component, IItemStyle + { + /// + /// Gets or sets the font that will be applied by this style + /// + [DefaultValue(null)] + public Font Font + { + get { return this.font; } + set { this.font = value; } + } + + private Font font; + + /// + /// Gets or sets the style of font that will be applied by this style + /// + [DefaultValue(FontStyle.Regular)] + public FontStyle FontStyle + { + get { return this.fontStyle; } + set { this.fontStyle = value; } + } + + private FontStyle fontStyle; + + /// + /// Gets or sets the color of the text that will be applied by this style + /// + [DefaultValue(typeof (Color), "")] + public Color ForeColor + { + get { return this.foreColor; } + set { this.foreColor = value; } + } + + private Color foreColor; + + /// + /// Gets or sets the background color that will be applied by this style + /// + [DefaultValue(typeof (Color), "")] + public Color BackColor + { + get { return this.backColor; } + set { this.backColor = value; } + } + + private Color backColor; + } + + + /// + /// Instances of this class specify how should "hot items" (non-selected + /// rows under the cursor) be rendered. + /// + public class HotItemStyle : SimpleItemStyle + { + /// + /// Gets or sets the overlay that should be drawn as part of the hot item + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IOverlay Overlay { + get { return this.overlay; } + set { this.overlay = value; } + } + private IOverlay overlay; + + /// + /// Gets or sets the decoration that should be drawn as part of the hot item + /// + /// A decoration is different from an overlay in that an decoration + /// scrolls with the listview contents, whilst an overlay does not. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IDecoration Decoration { + get { return this.decoration; } + set { this.decoration = value; } + } + private IDecoration decoration; + } + + /// + /// This class defines how a cell should be formatted + /// + [TypeConverter(typeof(ExpandableObjectConverter))] + public class CellStyle : IItemStyle + { + /// + /// Gets or sets the font that will be applied by this style + /// + public Font Font { + get { return this.font; } + set { this.font = value; } + } + private Font font; + + /// + /// Gets or sets the style of font that will be applied by this style + /// + [DefaultValue(FontStyle.Regular)] + public FontStyle FontStyle { + get { return this.fontStyle; } + set { this.fontStyle = value; } + } + private FontStyle fontStyle; + + /// + /// Gets or sets the color of the text that will be applied by this style + /// + [DefaultValue(typeof(Color), "")] + public Color ForeColor { + get { return this.foreColor; } + set { this.foreColor = value; } + } + private Color foreColor; + + /// + /// Gets or sets the background color that will be applied by this style + /// + [DefaultValue(typeof(Color), "")] + public Color BackColor { + get { return this.backColor; } + set { this.backColor = value; } + } + private Color backColor; + } + + /// + /// Instances of this class describe how hyperlinks will appear + /// + public class HyperlinkStyle : System.ComponentModel.Component + { + /// + /// Create a HyperlinkStyle + /// + public HyperlinkStyle() { + this.Normal = new CellStyle(); + this.Normal.ForeColor = Color.Blue; + this.Over = new CellStyle(); + this.Over.FontStyle = FontStyle.Underline; + this.Visited = new CellStyle(); + this.Visited.ForeColor = Color.Purple; + this.OverCursor = Cursors.Hand; + } + + /// + /// What sort of formatting should be applied to hyperlinks in their normal state? + /// + [Category("Appearance"), + Description("How should hyperlinks be drawn")] + public CellStyle Normal { + get { return this.normalStyle; } + set { this.normalStyle = value; } + } + private CellStyle normalStyle; + + /// + /// What sort of formatting should be applied to hyperlinks when the mouse is over them? + /// + [Category("Appearance"), + Description("How should hyperlinks be drawn when the mouse is over them?")] + public CellStyle Over { + get { return this.overStyle; } + set { this.overStyle = value; } + } + private CellStyle overStyle; + + /// + /// What sort of formatting should be applied to hyperlinks after they have been clicked? + /// + [Category("Appearance"), + Description("How should hyperlinks be drawn after they have been clicked")] + public CellStyle Visited { + get { return this.visitedStyle; } + set { this.visitedStyle = value; } + } + private CellStyle visitedStyle; + + /// + /// Gets or sets the cursor that should be shown when the mouse is over a hyperlink. + /// + [Category("Appearance"), + Description("What cursor should be shown when the mouse is over a link?")] + public Cursor OverCursor { + get { return this.overCursor; } + set { this.overCursor = value; } + } + private Cursor overCursor; + } + + /// + /// Instances of this class control one the styling of one particular state + /// (normal, hot, pressed) of a header control + /// + [TypeConverter(typeof(ExpandableObjectConverter))] + public class HeaderStateStyle + { + /// + /// Gets or sets the font that will be applied by this style + /// + [DefaultValue(null)] + public Font Font { + get { return this.font; } + set { this.font = value; } + } + private Font font; + + /// + /// Gets or sets the color of the text that will be applied by this style + /// + [DefaultValue(typeof(Color), "")] + public Color ForeColor { + get { return this.foreColor; } + set { this.foreColor = value; } + } + private Color foreColor; + + /// + /// Gets or sets the background color that will be applied by this style + /// + [DefaultValue(typeof(Color), "")] + public Color BackColor { + get { return this.backColor; } + set { this.backColor = value; } + } + private Color backColor; + + /// + /// Gets or sets the color in which a frame will be drawn around the header for this column + /// + [DefaultValue(typeof(Color), "")] + public Color FrameColor { + get { return this.frameColor; } + set { this.frameColor = value; } + } + private Color frameColor; + + /// + /// Gets or sets the width of the frame that will be drawn around the header for this column + /// + [DefaultValue(0.0f)] + public float FrameWidth { + get { return this.frameWidth; } + set { this.frameWidth = value; } + } + private float frameWidth; + } + + /// + /// This class defines how a header should be formatted in its various states. + /// + public class HeaderFormatStyle : System.ComponentModel.Component + { + /// + /// Create a new HeaderFormatStyle + /// + public HeaderFormatStyle() { + this.Hot = new HeaderStateStyle(); + this.Normal = new HeaderStateStyle(); + this.Pressed = new HeaderStateStyle(); + } + + /// + /// What sort of formatting should be applied to a column header when the mouse is over it? + /// + [Category("Appearance"), + Description("How should the header be drawn when the mouse is over it?")] + public HeaderStateStyle Hot { + get { return this.hotStyle; } + set { this.hotStyle = value; } + } + private HeaderStateStyle hotStyle; + + /// + /// What sort of formatting should be applied to a column header in its normal state? + /// + [Category("Appearance"), + Description("How should a column header normally be drawn")] + public HeaderStateStyle Normal { + get { return this.normalStyle; } + set { this.normalStyle = value; } + } + private HeaderStateStyle normalStyle; + + /// + /// What sort of formatting should be applied to a column header when pressed? + /// + [Category("Appearance"), + Description("How should a column header be drawn when it is pressed")] + public HeaderStateStyle Pressed { + get { return this.pressedStyle; } + set { this.pressedStyle = value; } + } + private HeaderStateStyle pressedStyle; + + /// + /// Set the font for all three states + /// + /// + public void SetFont(Font font) { + this.Normal.Font = font; + this.Hot.Font = font; + this.Pressed.Font = font; + } + + /// + /// Set the fore color for all three states + /// + /// + public void SetForeColor(Color color) { + this.Normal.ForeColor = color; + this.Hot.ForeColor = color; + this.Pressed.ForeColor = color; + } + + /// + /// Set the back color for all three states + /// + /// + public void SetBackColor(Color color) { + this.Normal.BackColor = color; + this.Hot.BackColor = color; + this.Pressed.BackColor = color; + } + } +} diff --git a/ObjectListView/Rendering/TreeRenderer.cs b/ObjectListView/Rendering/TreeRenderer.cs new file mode 100644 index 00000000..a3bef8a6 --- /dev/null +++ b/ObjectListView/Rendering/TreeRenderer.cs @@ -0,0 +1,309 @@ +/* + * TreeRenderer - Draw the major column in a TreeListView + * + * Author: Phillip Piper + * Date: 27/06/2015 + * + * Change log: + * 2016-07-17 JPP - Added TreeRenderer.UseTriangles and IsShowGlyphs + * 2015-06-27 JPP - Split out from TreeListView.cs + * + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Drawing; +using System.Windows.Forms; +using System.Windows.Forms.VisualStyles; +using System.Drawing.Drawing2D; + +namespace BrightIdeasSoftware { + + public partial class TreeListView { + /// + /// This class handles drawing the tree structure of the primary column. + /// + public class TreeRenderer : HighlightTextRenderer { + /// + /// Create a TreeRenderer + /// + public TreeRenderer() { + this.LinePen = new Pen(Color.Blue, 1.0f); + this.LinePen.DashStyle = DashStyle.Dot; + } + + #region Configuration properties + + /// + /// Should the renderer draw glyphs at the expansion points? + /// + /// The expansion points will still function to expand/collapse even if this is false. + public bool IsShowGlyphs + { + get { return isShowGlyphs; } + set { isShowGlyphs = value; } + } + private bool isShowGlyphs = true; + + /// + /// Should the renderer draw lines connecting siblings? + /// + public bool IsShowLines + { + get { return isShowLines; } + set { isShowLines = value; } + } + private bool isShowLines = true; + + /// + /// Return the pen that will be used to draw the lines between branches + /// + public Pen LinePen + { + get { return linePen; } + set { linePen = value; } + } + private Pen linePen; + + /// + /// Should the renderer draw triangles as the expansion glyphs? + /// + /// + /// This looks best with ShowLines = false + /// + public bool UseTriangles + { + get { return useTriangles; } + set { useTriangles = value; } + } + private bool useTriangles = false; + + #endregion + + /// + /// Return the branch that the renderer is currently drawing. + /// + private Branch Branch { + get { + return this.TreeListView.TreeModel.GetBranch(this.RowObject); + } + } + + /// + /// Return the TreeListView for which the renderer is being used. + /// + public TreeListView TreeListView { + get { + return (TreeListView)this.ListView; + } + } + + /// + /// How many pixels will be reserved for each level of indentation? + /// + public static int PIXELS_PER_LEVEL = 16 + 1; + + /// + /// The real work of drawing the tree is done in this method + /// + /// + /// + public override void Render(System.Drawing.Graphics g, System.Drawing.Rectangle r) { + this.DrawBackground(g, r); + + Branch br = this.Branch; + + Rectangle paddedRectangle = this.ApplyCellPadding(r); + + Rectangle expandGlyphRectangle = paddedRectangle; + expandGlyphRectangle.Offset((br.Level - 1) * PIXELS_PER_LEVEL, 0); + expandGlyphRectangle.Width = PIXELS_PER_LEVEL; + expandGlyphRectangle.Height = PIXELS_PER_LEVEL; + expandGlyphRectangle.Y = this.AlignVertically(paddedRectangle, expandGlyphRectangle); + int expandGlyphRectangleMidVertical = expandGlyphRectangle.Y + (expandGlyphRectangle.Height/2); + + if (this.IsShowLines) + this.DrawLines(g, r, this.LinePen, br, expandGlyphRectangleMidVertical); + + if (br.CanExpand && this.IsShowGlyphs) + this.DrawExpansionGlyph(g, expandGlyphRectangle, br.IsExpanded); + + int indent = br.Level * PIXELS_PER_LEVEL; + paddedRectangle.Offset(indent, 0); + paddedRectangle.Width -= indent; + + this.DrawImageAndText(g, paddedRectangle); + } + + /// + /// Draw the expansion indicator + /// + /// + /// + /// + protected virtual void DrawExpansionGlyph(Graphics g, Rectangle r, bool isExpanded) { + if (this.UseStyles) { + this.DrawExpansionGlyphStyled(g, r, isExpanded); + } else { + this.DrawExpansionGlyphManual(g, r, isExpanded); + } + } + + /// + /// Gets whether or not we should render using styles + /// + protected virtual bool UseStyles { + get { + return !this.IsPrinting && Application.RenderWithVisualStyles; + } + } + + /// + /// Draw the expansion indicator using styles + /// + /// + /// + /// + protected virtual void DrawExpansionGlyphStyled(Graphics g, Rectangle r, bool isExpanded) { + if (this.UseTriangles && this.IsShowLines) { + using (SolidBrush b = new SolidBrush(GetBackgroundColor())) { + Rectangle r2 = r; + r2.Inflate(-2, -2); + g.FillRectangle(b, r2); + } + } + + VisualStyleRenderer renderer = new VisualStyleRenderer(DecideVisualElement(isExpanded)); + renderer.DrawBackground(g, r); + } + + private VisualStyleElement DecideVisualElement(bool isExpanded) { + string klass = this.UseTriangles ? "Explorer::TreeView" : "TREEVIEW"; + int part = this.UseTriangles && this.IsExpansionHot ? 4 : 2; + int state = isExpanded ? 2 : 1; + return VisualStyleElement.CreateElement(klass, part, state); + } + + /// + /// Is the mouse over a checkbox in this cell? + /// + protected bool IsExpansionHot { + get { return this.IsCellHot && this.ListView.HotCellHitLocation == HitTestLocation.ExpandButton; } + } + + /// + /// Draw the expansion indicator without using styles + /// + /// + /// + /// + protected virtual void DrawExpansionGlyphManual(Graphics g, Rectangle r, bool isExpanded) { + int h = 8; + int w = 8; + int x = r.X + 4; + int y = r.Y + (r.Height / 2) - 4; + + g.DrawRectangle(new Pen(SystemBrushes.ControlDark), x, y, w, h); + g.FillRectangle(Brushes.White, x + 1, y + 1, w - 1, h - 1); + g.DrawLine(Pens.Black, x + 2, y + 4, x + w - 2, y + 4); + + if (!isExpanded) + g.DrawLine(Pens.Black, x + 4, y + 2, x + 4, y + h - 2); + } + + /// + /// Draw the lines of the tree + /// + /// + /// + /// + /// + /// + protected virtual void DrawLines(Graphics g, Rectangle r, Pen p, Branch br, int glyphMidVertical) { + Rectangle r2 = r; + r2.Width = PIXELS_PER_LEVEL; + + // Vertical lines have to start on even points, otherwise the dotted line looks wrong. + // This is only needed if pen is dotted. + int top = r2.Top; + //if (p.DashStyle == DashStyle.Dot && (top & 1) == 0) + // top += 1; + + // Draw lines for ancestors + int midX; + IList ancestors = br.Ancestors; + foreach (Branch ancestor in ancestors) { + if (!ancestor.IsLastChild && !ancestor.IsOnlyBranch) { + midX = r2.Left + r2.Width / 2; + g.DrawLine(p, midX, top, midX, r2.Bottom); + } + r2.Offset(PIXELS_PER_LEVEL, 0); + } + + // Draw lines for this branch + midX = r2.Left + r2.Width / 2; + + // Horizontal line first + g.DrawLine(p, midX, glyphMidVertical, r2.Right, glyphMidVertical); + + // Vertical line second + if (br.IsFirstBranch) { + if (!br.IsLastChild && !br.IsOnlyBranch) + g.DrawLine(p, midX, glyphMidVertical, midX, r2.Bottom); + } else { + if (br.IsLastChild) + g.DrawLine(p, midX, top, midX, glyphMidVertical); + else + g.DrawLine(p, midX, top, midX, r2.Bottom); + } + } + + /// + /// Do the hit test + /// + /// + /// + /// + /// + protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + Branch br = this.Branch; + + Rectangle r = this.ApplyCellPadding(this.Bounds); + if (br.CanExpand) { + r.Offset((br.Level - 1) * PIXELS_PER_LEVEL, 0); + r.Width = PIXELS_PER_LEVEL; + if (r.Contains(x, y)) { + hti.HitTestLocation = HitTestLocation.ExpandButton; + return; + } + } + + r = this.Bounds; + int indent = br.Level * PIXELS_PER_LEVEL; + r.X += indent; + r.Width -= indent; + + // Ignore events in the indent zone + if (x < r.Left) { + hti.HitTestLocation = HitTestLocation.Nothing; + } else { + this.StandardHitTest(g, hti, r, x, y); + } + } + + /// + /// Calculate the edit rect + /// + /// + /// + /// + /// + /// + /// + protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return this.StandardGetEditRectangle(g, cellBounds, preferredSize); + } + } + } +} \ No newline at end of file diff --git a/ObjectListView/Resources/clear-filter.png b/ObjectListView/Resources/clear-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..2ddf7073b0c3de5791448e7a8effdf05f2c25e77 GIT binary patch literal 1381 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstPBjy3;{kN zu3$w13IYNSK;YpJ;1K`>2|$pMP*6~?L4aXGMZtoMfCCZ?4-^DGXfXWOVECXR@E?dQ z1pYfH{P$4!A7F4Hz~MoF!-oim{|OHNGaUXG1pKcE_)wAXABY+fK6DiP? zKrmy%f&~jUtk?hoJ2qU{vEjpnh7U6u{?BN50A#ON@L|J(4?8v-*m2;)feiRR|N(hXMsm#F#`kNArNL1)$nQn3QCr^MwA5Sr_kFdQ zoeld1Cq2lBJ3DomuG-I@|6fF&{HffzJymf>EyJTdrCUYrF&?;bSL@WRXdYXZ8L_8& z<{G4lv8T+tYq*RrCy!C_(dAdCziTk7Os_f-vdCUx*~%@3S$j_~ZHe|x2$B30aO11n q@yq4if(I79Qcsd^-jTfMt#}vzTCZ0dwIzWLW$<+Mb6Mw<&;$TKY4H>Q literal 0 HcmV?d00001 diff --git a/ObjectListView/Resources/coffee.jpg b/ObjectListView/Resources/coffee.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6032d83fc7a5e1db6b76067d8de98e2bce6619c4 GIT binary patch literal 73464 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 literal 0 HcmV?d00001 diff --git a/ObjectListView/Resources/filter-icons3.png b/ObjectListView/Resources/filter-icons3.png new file mode 100644 index 0000000000000000000000000000000000000000..80178919d5b63ab83df2b7c669ab0779a49aa2aa GIT binary patch literal 1305 zcmeAS@N?(olHy`uVBq!ia0vp^+(695!3-or^+GlRDVB6cUq=Rpjs4tz5?L7-m>B|m zLR>-OY^*$7-1heN&W=uQZf+hP9-f|_-hqLkp`l@6VHp`2`FT0T#l=NMCB?<1B}K(e zO)YJ0ZTkrW0Q7{?;10DkVe@>jlz`)2*666>Be`EuO;P33J zzzE?i@Q5sCVBk9h!i=ICUJXD&$r9IylHmNblJdl&REB`W%)AmkKi3e2GGjecJqvT| zhFG8?MUW!rqSVBa%=|oskj&gv1|tJQLn{LlD?>vCBSR}gBP(MQ&19DgK*fQcE{-7* z;jU-hg&GtXm>qO(2x@=3_wWB9j=m(lnN#F*)B0@kMOI+Vn;qXQmn}9~_T~=D z+XX%QB%0i|8O%7kPm;e@o7oiiks3xCpOkgD9y%{jLaR6H_xy85}S Ib4q9e03rsV#sB~S literal 0 HcmV?d00001 diff --git a/ObjectListView/Resources/sort-ascending.png b/ObjectListView/Resources/sort-ascending.png new file mode 100644 index 0000000000000000000000000000000000000000..a21be93fa9f9415cfb652bd3f1fea9f14d09338c GIT binary patch literal 1364 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstPBjy3;{kN zu0U~1E30yA9S={>)b#X{vND5!2?h}ptRkldbB_{`{(WMnzDc9vUQs_&0BM1<bo1Nd^W+hLRw^;Qu2VFa&>RR|SSK zXMsm#F#`kNArNL1)$nQn3QCr^MwA5Sr=C^PE^0&oV$SnRjJ>Se)@el9KzgPb(xGQ0`HCE5kgy}D{ z`~Ouz>UtNs#CGW&tXB%1p1&yert|7m7J~gd`a(DPrKwMx!92N9ht=$^ar1VS;Aj5% zj}5qGI!+k)v>%we)P+^=ZeLFF8>M517RUD-ikl)^c|iKt?Ty_hV~;%rx{JZn)z4*} HQ$iB}j@iab literal 0 HcmV?d00001 diff --git a/ObjectListView/Resources/sort-descending.png b/ObjectListView/Resources/sort-descending.png new file mode 100644 index 0000000000000000000000000000000000000000..92dbe63f20afc6c51cf837b3aa6b0de64bfd126d GIT binary patch literal 1371 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstPBjy3;{kN zu0U~1D=Pzs0xPd_tHAPdYaI_y&(!qvlCm-bj|PW`1wjezAz8Cy3uk*KEb+`(5>mJ_ zzIa_`?dFp19SsHwZPq$dt*v|9EvGtJPjGOU;1Mu2FmhH_{erCc1!=j<%W^iBl`XC9 zUtQhMFk$`t#fzse-MhMN`o{iwySt|BpSf(^rcI02A6d8a^twH#_Z~dB@6@g1%a)zk zwD#DAn

dzjor>jk9NOoV#%O_LIwZpWS`@{PE+*Po6w|`R*Oi*`r`I1Sk&yjoeww z7#J8CN`m}?|Br0I5d5886&RwN1s;*b3=DjSK$uZf!>a)(C|TkfQ4*Y=R#Ki=l*$m0 zn3-3i=jR%tP-d)Ws%K$t-4F{@qzF>vT$Gwvl9`{U5R#dj%3x$*XlP|%Vr6KgU|?xw zWMXAtBrKh33{*VX)5S4FBRIB?o$rtVkE^f7?kRgUb8oi9nmze<{a1>@M6*p#p1k~N z-Eu`SL2~V)lb*#tCz*=Q4?JJNw&&=s+HH*Mod1ib@bvsXa8~LjPfF4c_V0OmAFLZw zPrO#0*_f`&8uipQN%z&d#SVpjdlp=i+dJcJioJkf_=1P$=Bx_(H07fFXSt;FNoxyO jXDxlJeOk+k^#@aL!J4$pI`)r1=P`J?`njxgN@xNAhOF7) literal 0 HcmV?d00001 diff --git a/ObjectListView/SubControls/GlassPanelForm.cs b/ObjectListView/SubControls/GlassPanelForm.cs new file mode 100644 index 00000000..bcaba679 --- /dev/null +++ b/ObjectListView/SubControls/GlassPanelForm.cs @@ -0,0 +1,459 @@ +/* + * GlassPanelForm - A transparent form that is placed over an ObjectListView + * to allow flicker-free overlay images during scrolling. + * + * Author: Phillip Piper + * Date: 14/04/2009 4:36 PM + * + * Change log: + * 2010-08-18 JPP - Added WS_EX_TOOLWINDOW style so that the form won't appear in Alt-Tab list. + * v2.4 + * 2010-03-11 JPP - Work correctly in MDI applications -- more or less. Actually, less than more. + * They don't crash but they don't correctly handle overlapping MDI children. + * Overlays from one control are shown on top of the other windows. + * 2010-03-09 JPP - Correctly Unbind() when the related ObjectListView is disposed. + * 2009-10-28 JPP - Use FindForm() rather than TopMostControl, since the latter doesn't work + * as I expected when the OLV is part of an MDI child window. Thanks to + * wvd_vegt who tracked this down. + * v2.3 + * 2009-08-19 JPP - Only hide the glass pane on resize, not on move + * - Each glass panel now only draws one overlays + * v2.2 + * 2009-06-05 JPP - Handle when owning window is a topmost window + * 2009-04-14 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + ///

+ /// A GlassPanelForm sits transparently over an ObjectListView to show overlays. + /// + internal partial class GlassPanelForm : Form + { + public GlassPanelForm() { + this.Name = "GlassPanelForm"; + this.Text = "GlassPanelForm"; + + ClientSize = new System.Drawing.Size(0, 0); + ControlBox = false; + FormBorderStyle = FormBorderStyle.None; + SizeGripStyle = SizeGripStyle.Hide; + StartPosition = FormStartPosition.Manual; + MaximizeBox = false; + MinimizeBox = false; + ShowIcon = false; + ShowInTaskbar = false; + FormBorderStyle = FormBorderStyle.None; + + SetStyle(ControlStyles.Selectable, false); + + this.Opacity = 0.5f; + this.BackColor = Color.FromArgb(255, 254, 254, 254); + this.TransparencyKey = this.BackColor; + this.HideGlass(); + NativeMethods.ShowWithoutActivate(this); + } + + protected override void Dispose(bool disposing) { + if (disposing) + this.Unbind(); + + base.Dispose(disposing); + } + + #region Properties + + /// + /// Get the low-level windows flag that will be given to CreateWindow. + /// + protected override CreateParams CreateParams { + get { + CreateParams cp = base.CreateParams; + cp.ExStyle |= 0x20; // WS_EX_TRANSPARENT + cp.ExStyle |= 0x80; // WS_EX_TOOLWINDOW + return cp; + } + } + + #endregion + + #region Commands + + /// + /// Attach this form to the given ObjectListView + /// + public void Bind(ObjectListView olv, IOverlay overlay) { + if (this.objectListView != null) + this.Unbind(); + + this.objectListView = olv; + this.Overlay = overlay; + this.mdiClient = null; + this.mdiOwner = null; + + if (this.objectListView == null) + return; + + // NOTE: If you listen to any events here, you *must* stop listening in Unbind() + + this.objectListView.Disposed += new EventHandler(objectListView_Disposed); + this.objectListView.LocationChanged += new EventHandler(objectListView_LocationChanged); + this.objectListView.SizeChanged += new EventHandler(objectListView_SizeChanged); + this.objectListView.VisibleChanged += new EventHandler(objectListView_VisibleChanged); + this.objectListView.ParentChanged += new EventHandler(objectListView_ParentChanged); + + // Collect our ancestors in the widget hierarchy + if (this.ancestors == null) + this.ancestors = new List(); + Control parent = this.objectListView.Parent; + while (parent != null) { + this.ancestors.Add(parent); + parent = parent.Parent; + } + + // Listen for changes in the hierarchy + foreach (Control ancestor in this.ancestors) { + ancestor.ParentChanged += new EventHandler(objectListView_ParentChanged); + TabControl tabControl = ancestor as TabControl; + if (tabControl != null) { + tabControl.Selected += new TabControlEventHandler(tabControl_Selected); + } + } + + // Listen for changes in our owning form + this.Owner = this.objectListView.FindForm(); + this.myOwner = this.Owner; + if (this.Owner != null) { + this.Owner.LocationChanged += new EventHandler(Owner_LocationChanged); + this.Owner.SizeChanged += new EventHandler(Owner_SizeChanged); + this.Owner.ResizeBegin += new EventHandler(Owner_ResizeBegin); + this.Owner.ResizeEnd += new EventHandler(Owner_ResizeEnd); + if (this.Owner.TopMost) { + // We can't do this.TopMost = true; since that will activate the panel, + // taking focus away from the owner of the listview + NativeMethods.MakeTopMost(this); + } + + // We need special code to handle MDI + this.mdiOwner = this.Owner.MdiParent; + if (this.mdiOwner != null) { + this.mdiOwner.LocationChanged += new EventHandler(Owner_LocationChanged); + this.mdiOwner.SizeChanged += new EventHandler(Owner_SizeChanged); + this.mdiOwner.ResizeBegin += new EventHandler(Owner_ResizeBegin); + this.mdiOwner.ResizeEnd += new EventHandler(Owner_ResizeEnd); + + // Find the MDIClient control, which houses all MDI children + foreach (Control c in this.mdiOwner.Controls) { + this.mdiClient = c as MdiClient; + if (this.mdiClient != null) { + break; + } + } + if (this.mdiClient != null) { + this.mdiClient.ClientSizeChanged += new EventHandler(myMdiClient_ClientSizeChanged); + } + } + } + + this.UpdateTransparency(); + } + + void myMdiClient_ClientSizeChanged(object sender, EventArgs e) { + this.RecalculateBounds(); + this.Invalidate(); + } + + /// + /// Made the overlay panel invisible + /// + public void HideGlass() { + if (!this.isGlassShown) + return; + this.isGlassShown = false; + this.Bounds = new Rectangle(-10000, -10000, 1, 1); + } + + /// + /// Show the overlay panel in its correctly location + /// + /// + /// If the panel is always shown, this method does nothing. + /// If the panel is being resized, this method also does nothing. + /// + public void ShowGlass() { + if (this.isGlassShown || this.isDuringResizeSequence) + return; + + this.isGlassShown = true; + this.RecalculateBounds(); + } + + /// + /// Detach this glass panel from its previous ObjectListView + /// + /// + /// You should unbind the overlay panel before making any changes to the + /// widget hierarchy. + /// + public void Unbind() { + if (this.objectListView != null) { + this.objectListView.Disposed -= new EventHandler(objectListView_Disposed); + this.objectListView.LocationChanged -= new EventHandler(objectListView_LocationChanged); + this.objectListView.SizeChanged -= new EventHandler(objectListView_SizeChanged); + this.objectListView.VisibleChanged -= new EventHandler(objectListView_VisibleChanged); + this.objectListView.ParentChanged -= new EventHandler(objectListView_ParentChanged); + this.objectListView = null; + } + + if (this.ancestors != null) { + foreach (Control parent in this.ancestors) { + parent.ParentChanged -= new EventHandler(objectListView_ParentChanged); + TabControl tabControl = parent as TabControl; + if (tabControl != null) { + tabControl.Selected -= new TabControlEventHandler(tabControl_Selected); + } + } + this.ancestors = null; + } + + if (this.myOwner != null) { + this.myOwner.LocationChanged -= new EventHandler(Owner_LocationChanged); + this.myOwner.SizeChanged -= new EventHandler(Owner_SizeChanged); + this.myOwner.ResizeBegin -= new EventHandler(Owner_ResizeBegin); + this.myOwner.ResizeEnd -= new EventHandler(Owner_ResizeEnd); + this.myOwner = null; + } + + if (this.mdiOwner != null) { + this.mdiOwner.LocationChanged -= new EventHandler(Owner_LocationChanged); + this.mdiOwner.SizeChanged -= new EventHandler(Owner_SizeChanged); + this.mdiOwner.ResizeBegin -= new EventHandler(Owner_ResizeBegin); + this.mdiOwner.ResizeEnd -= new EventHandler(Owner_ResizeEnd); + this.mdiOwner = null; + } + + if (this.mdiClient != null) { + this.mdiClient.ClientSizeChanged -= new EventHandler(myMdiClient_ClientSizeChanged); + this.mdiClient = null; + } + } + + #endregion + + #region Event Handlers + + void objectListView_Disposed(object sender, EventArgs e) { + this.Unbind(); + } + + /// + /// Handle when the form that owns the ObjectListView begins to be resized + /// + /// + /// + void Owner_ResizeBegin(object sender, EventArgs e) { + // When the top level window is being resized, we just want to hide + // the overlay window. When the resizing finishes, we want to show + // the overlay window, if it was shown before the resize started. + this.isDuringResizeSequence = true; + this.wasGlassShownBeforeResize = this.isGlassShown; + } + + /// + /// Handle when the form that owns the ObjectListView finished to be resized + /// + /// + /// + void Owner_ResizeEnd(object sender, EventArgs e) { + this.isDuringResizeSequence = false; + if (this.wasGlassShownBeforeResize) + this.ShowGlass(); + } + + /// + /// The owning form has moved. Move the overlay panel too. + /// + /// + /// + void Owner_LocationChanged(object sender, EventArgs e) { + if (this.mdiOwner != null) + this.HideGlass(); + else + this.RecalculateBounds(); + } + + /// + /// The owning form is resizing. Hide our overlay panel until the resizing stops + /// + /// + /// + void Owner_SizeChanged(object sender, EventArgs e) { + this.HideGlass(); + } + + + /// + /// Handle when the bound OLV changes its location. The overlay panel must + /// be moved too, IFF it is currently visible. + /// + /// + /// + void objectListView_LocationChanged(object sender, EventArgs e) { + if (this.isGlassShown) { + this.RecalculateBounds(); + } + } + + /// + /// Handle when the bound OLV changes size. The overlay panel must + /// resize too, IFF it is currently visible. + /// + /// + /// + void objectListView_SizeChanged(object sender, EventArgs e) { + // This event is triggered in all sorts of places, and not always when the size changes. + //if (this.isGlassShown) { + // this.Size = this.objectListView.ClientSize; + //} + } + + /// + /// Handle when the bound OLV is part of a TabControl and that + /// TabControl changes tabs. The overlay panel is hidden. The + /// first time the bound OLV is redrawn, the overlay panel will + /// be shown again. + /// + /// + /// + void tabControl_Selected(object sender, TabControlEventArgs e) { + this.HideGlass(); + } + + /// + /// Somewhere the parent of the bound OLV has changed. Update + /// our events. + /// + /// + /// + void objectListView_ParentChanged(object sender, EventArgs e) { + ObjectListView olv = this.objectListView; + IOverlay overlay = this.Overlay; + this.Unbind(); + this.Bind(olv, overlay); + } + + /// + /// Handle when the bound OLV changes its visibility. + /// The overlay panel should match the OLV's visibility. + /// + /// + /// + void objectListView_VisibleChanged(object sender, EventArgs e) { + if (this.objectListView.Visible) + this.ShowGlass(); + else + this.HideGlass(); + } + + #endregion + + #region Implementation + + protected override void OnPaint(PaintEventArgs e) { + if (this.objectListView == null || this.Overlay == null) + return; + + Graphics g = e.Graphics; + g.TextRenderingHint = ObjectListView.TextRenderingHint; + g.SmoothingMode = ObjectListView.SmoothingMode; + //g.DrawRectangle(new Pen(Color.Green, 4.0f), this.ClientRectangle); + + // If we are part of an MDI app, make sure we don't draw outside the bounds + if (this.mdiClient != null) { + Rectangle r = mdiClient.RectangleToScreen(mdiClient.ClientRectangle); + Rectangle r2 = this.objectListView.RectangleToClient(r); + g.SetClip(r2, System.Drawing.Drawing2D.CombineMode.Intersect); + } + + this.Overlay.Draw(this.objectListView, g, this.objectListView.ClientRectangle); + } + + protected void RecalculateBounds() { + if (!this.isGlassShown) + return; + + Rectangle rect = this.objectListView.ClientRectangle; + rect.X = 0; + rect.Y = 0; + this.Bounds = this.objectListView.RectangleToScreen(rect); + } + + internal void UpdateTransparency() { + ITransparentOverlay transparentOverlay = this.Overlay as ITransparentOverlay; + if (transparentOverlay == null) + this.Opacity = this.objectListView.OverlayTransparency / 255.0f; + else + this.Opacity = transparentOverlay.Transparency / 255.0f; + } + + protected override void WndProc(ref Message m) { + const int WM_NCHITTEST = 132; + const int HTTRANSPARENT = -1; + switch (m.Msg) { + // Ignore all mouse interactions + case WM_NCHITTEST: + m.Result = (IntPtr)HTTRANSPARENT; + break; + } + base.WndProc(ref m); + } + + #endregion + + #region Implementation variables + + internal IOverlay Overlay; + + #endregion + + #region Private variables + + private ObjectListView objectListView; + private bool isDuringResizeSequence; + private bool isGlassShown; + private bool wasGlassShownBeforeResize; + + // Cache these so we can unsubscribe from events even when the OLV has been disposed. + private Form myOwner; + private Form mdiOwner; + private List ancestors; + MdiClient mdiClient; + + #endregion + + } +} diff --git a/ObjectListView/SubControls/HeaderControl.cs b/ObjectListView/SubControls/HeaderControl.cs new file mode 100644 index 00000000..298680b7 --- /dev/null +++ b/ObjectListView/SubControls/HeaderControl.cs @@ -0,0 +1,1230 @@ +/* + * HeaderControl - A limited implementation of HeaderControl + * + * Author: Phillip Piper + * Date: 25/11/2008 17:15 + * + * Change log: + * 2015-06-12 JPP - Use HeaderTextAlignOrDefault instead of HeaderTextAlign + * 2014-09-07 JPP - Added ability to have checkboxes in headers + * + * 2011-05-11 JPP - Fixed bug that prevented columns from being resized in IDE Designer + * by dragging the column divider + * 2011-04-12 JPP - Added ability to draw filter indicator in a column's header + * v2.4.1 + * 2010-08-23 JPP - Added ability to draw header vertically (thanks to Mark Fenwick) + * - Uses OLVColumn.HeaderTextAlign to decide how to align the column's header + * 2010-08-08 JPP - Added ability to have image in header + * v2.4 + * 2010-03-22 JPP - Draw header using header styles + * 2009-10-30 JPP - Plugged GDI resource leak, where font handles were created during custom + * drawing, but never destroyed + * v2.3 + * 2009-10-03 JPP - Handle when ListView.HeaderFormatStyle is None + * 2009-08-24 JPP - Handle the header being destroyed + * v2.2.1 + * 2009-08-16 JPP - Correctly handle header themes + * 2009-08-15 JPP - Added formatting capabilities: font, color, word wrap + * v2.2 + * 2009-06-01 JPP - Use ToolTipControl + * 2009-05-10 JPP - Removed all unsafe code + * 2008-11-25 JPP - Initial version + * + * TO DO: + * - Put drawing code into header style object, so that it can be easily customized. + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Drawing; +using System.Runtime.Remoting; +using System.Windows.Forms; +using System.Runtime.InteropServices; +using System.Windows.Forms.VisualStyles; +using System.Drawing.Drawing2D; +using BrightIdeasSoftware.Properties; +using System.Security.Permissions; + +namespace BrightIdeasSoftware { + + /// + /// Class used to capture window messages for the header of the list view + /// control. + /// + public class HeaderControl : NativeWindow { + /// + /// Create a header control for the given ObjectListView. + /// + /// + public HeaderControl(ObjectListView olv) { + this.ListView = olv; + this.AssignHandle(NativeMethods.GetHeaderControl(olv)); + } + + #region Properties + + /// + /// Return the index of the column under the current cursor position, + /// or -1 if the cursor is not over a column + /// + /// Index of the column under the cursor, or -1 + public int ColumnIndexUnderCursor { + get { + Point pt = this.ScrolledCursorPosition; + return NativeMethods.GetColumnUnderPoint(this.Handle, pt); + } + } + + /// + /// Return the Windows handle behind this control + /// + /// + /// When an ObjectListView is initialized as part of a UserControl, the + /// GetHeaderControl() method returns 0 until the UserControl is + /// completely initialized. So the AssignHandle() call in the constructor + /// doesn't work. So we override the Handle property so value is always + /// current. + /// + public new IntPtr Handle { + get { return NativeMethods.GetHeaderControl(this.ListView); } + } + + //TODO: The Handle property may no longer be necessary. CHECK! 2008/11/28 + + /// + /// Gets or sets a style that should be applied to the font of the + /// column's header text when the mouse is over that column + /// + /// THIS IS EXPERIMENTAL. USE AT OWN RISK. August 2009 + [Obsolete("Use HeaderStyle.Hot.FontStyle instead")] + public FontStyle HotFontStyle { + get { return FontStyle.Regular; } + set { } + } + + /// + /// Gets the index of the column under the cursor if the cursor is over it's checkbox + /// + protected int GetColumnCheckBoxUnderCursor() { + Point pt = this.ScrolledCursorPosition; + + int columnIndex = NativeMethods.GetColumnUnderPoint(this.Handle, pt); + return this.IsPointOverHeaderCheckBox(columnIndex, pt) ? columnIndex : -1; + } + + /// + /// Gets the client rectangle for the header + /// + public Rectangle ClientRectangle { + get { + Rectangle r = new Rectangle(); + NativeMethods.GetClientRect(this.Handle, ref r); + return r; + } + } + + /// + /// Return true if the given point is over the checkbox for the given column. + /// + /// + /// + /// + protected bool IsPointOverHeaderCheckBox(int columnIndex, Point pt) { + if (columnIndex < 0 || columnIndex >= this.ListView.Columns.Count) + return false; + + OLVColumn column = this.ListView.GetColumn(columnIndex); + if (!this.HasCheckBox(column)) + return false; + + Rectangle r = this.GetCheckBoxBounds(column); + r.Inflate(1, 1); // make the target slightly bigger + return r.Contains(pt); + } + + /// + /// Gets whether the cursor is over a "locked" divider, i.e. + /// one that cannot be dragged by the user. + /// + protected bool IsCursorOverLockedDivider { + get { + Point pt = this.ScrolledCursorPosition; + int dividerIndex = NativeMethods.GetDividerUnderPoint(this.Handle, pt); + if (dividerIndex >= 0 && dividerIndex < this.ListView.Columns.Count) { + OLVColumn column = this.ListView.GetColumn(dividerIndex); + return column.IsFixedWidth || column.FillsFreeSpace; + } else + return false; + } + } + + private Point ScrolledCursorPosition { + get { + Point pt = this.ListView.PointToClient(Cursor.Position); + pt.X += this.ListView.LowLevelScrollPosition.X; + return pt; + } + } + + /// + /// Gets or sets the listview that this header belongs to + /// + protected ObjectListView ListView { + get { return this.listView; } + set { this.listView = value; } + } + + private ObjectListView listView; + + /// + /// Gets the maximum height of the header. -1 means no maximum. + /// + public int MaximumHeight + { + get { return this.ListView.HeaderMaximumHeight; } + } + + /// + /// Gets the minimum height of the header. -1 means no minimum. + /// + public int MinimumHeight + { + get { return this.ListView.HeaderMinimumHeight; } + } + + /// + /// Get or set the ToolTip that shows tips for the header + /// + public ToolTipControl ToolTip { + get { + if (this.toolTip == null) { + this.CreateToolTip(); + } + return this.toolTip; + } + protected set { this.toolTip = value; } + } + + private ToolTipControl toolTip; + + /// + /// Gets or sets whether the text in column headers should be word + /// wrapped when it is too long to fit within the column + /// + public bool WordWrap { + get { return this.wordWrap; } + set { this.wordWrap = value; } + } + + private bool wordWrap; + + #endregion + + #region Commands + + /// + /// Calculate how height the header needs to be + /// + /// Height in pixels + protected int CalculateHeight(Graphics g) { + TextFormatFlags flags = this.TextFormatFlags; + int columnUnderCursor = this.ColumnIndexUnderCursor; + float height = this.MinimumHeight; + for (int i = 0; i < this.ListView.Columns.Count; i++) { + OLVColumn column = this.ListView.GetColumn(i); + height = Math.Max(height, CalculateColumnHeight(g, column, flags, columnUnderCursor == i, i)); + } + return this.MaximumHeight == -1 ? (int) height : Math.Min(this.MaximumHeight, (int) height); + } + + private float CalculateColumnHeight(Graphics g, OLVColumn column, TextFormatFlags flags, bool isHot, int i) { + Font f = this.CalculateFont(column, isHot, false); + if (column.IsHeaderVertical) + return TextRenderer.MeasureText(g, column.Text, f, new Size(10000, 10000), flags).Width; + + const int fudge = 9; // 9 is a magic constant that makes it perfectly match XP behavior + if (!this.WordWrap) + return f.Height + fudge; + + Rectangle r = this.GetHeaderDrawRect(i); + if (this.HasNonThemedSortIndicator(column)) + r.Width -= 16; + if (column.HasHeaderImage) + r.Width -= column.ImageList.ImageSize.Width + 3; + if (this.HasFilterIndicator(column)) + r.Width -= this.CalculateFilterIndicatorWidth(r); + if (this.HasCheckBox(column)) + r.Width -= this.CalculateCheckBoxBounds(g, r).Width; + SizeF size = TextRenderer.MeasureText(g, column.Text, f, new Size(r.Width, 100), flags); + return size.Height + fudge; + } + + /// + /// Get the bounds of the checkbox against the given column + /// + /// + /// + public Rectangle GetCheckBoxBounds(OLVColumn column) { + Rectangle r = this.GetHeaderDrawRect(column.Index); + + using (Graphics g = this.ListView.CreateGraphics()) + return this.CalculateCheckBoxBounds(g, r); + } + + /// + /// Should the given column be drawn with a checkbox against it? + /// + /// + /// + public bool HasCheckBox(OLVColumn column) { + return column.HeaderCheckBox || column.HeaderTriStateCheckBox; + } + + /// + /// Should the given column show a sort indicator? + /// + /// + /// + protected bool HasSortIndicator(OLVColumn column) { + if (!this.ListView.ShowSortIndicators) + return false; + return column == this.ListView.LastSortColumn && this.ListView.LastSortOrder != SortOrder.None; + } + + /// + /// Should the given column be drawn with a filter indicator against it? + /// + /// + /// + protected bool HasFilterIndicator(OLVColumn column) { + return (this.ListView.UseFiltering && this.ListView.UseFilterIndicator && column.HasFilterIndicator); + } + + /// + /// Should the given column show a non-themed sort indicator? + /// + /// + /// + protected bool HasNonThemedSortIndicator(OLVColumn column) { + if (!this.ListView.ShowSortIndicators) + return false; + if (VisualStyleRenderer.IsSupported) + return !VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.SortArrow.SortedUp) && + this.HasSortIndicator(column); + else + return this.HasSortIndicator(column); + } + + /// + /// Return the bounds of the item with the given index + /// + /// + /// + public Rectangle GetItemRect(int itemIndex) { + const int HDM_FIRST = 0x1200; + const int HDM_GETITEMRECT = HDM_FIRST + 7; + NativeMethods.RECT r = new NativeMethods.RECT(); + NativeMethods.SendMessageRECT(this.Handle, HDM_GETITEMRECT, itemIndex, ref r); + return Rectangle.FromLTRB(r.left, r.top, r.right, r.bottom); + } + + /// + /// Return the bounds within which the given column will be drawn + /// + /// + /// + public Rectangle GetHeaderDrawRect(int itemIndex) { + Rectangle r = this.GetItemRect(itemIndex); + + // Tweak the text rectangle a little to improve aesthetics + r.Inflate(-3, 0); + r.Y -= 2; + + return r; + } + + /// + /// Force the header to redraw by invalidating it + /// + public void Invalidate() { + NativeMethods.InvalidateRect(this.Handle, 0, true); + } + + /// + /// Force the header to redraw a single column by invalidating it + /// + public void Invalidate(OLVColumn column) { + NativeMethods.InvalidateRect(this.Handle, 0, true); // todo + } + + #endregion + + #region Tooltip + + /// + /// Create a native tool tip control for this listview + /// + protected virtual void CreateToolTip() { + this.ToolTip = new ToolTipControl(); + this.ToolTip.Create(this.Handle); + this.ToolTip.AddTool(this); + this.ToolTip.Showing += new EventHandler(this.ListView.HeaderToolTipShowingCallback); + } + + #endregion + + #region Windows messaging + + /// + /// Override the basic message pump + /// + /// + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + protected override void WndProc(ref Message m) { + const int WM_DESTROY = 2; + const int WM_SETCURSOR = 0x20; + const int WM_NOTIFY = 0x4E; + const int WM_MOUSEMOVE = 0x200; + const int WM_LBUTTONDOWN = 0x201; + const int WM_LBUTTONUP = 0x202; + const int WM_MOUSELEAVE = 675; + const int HDM_FIRST = 0x1200; + const int HDM_LAYOUT = (HDM_FIRST + 5); + + // System.Diagnostics.Debug.WriteLine(String.Format("WndProc: {0}", m.Msg)); + + switch (m.Msg) { + case WM_SETCURSOR: + if (!this.HandleSetCursor(ref m)) + return; + break; + + case WM_NOTIFY: + if (!this.HandleNotify(ref m)) + return; + break; + + case WM_MOUSEMOVE: + if (!this.HandleMouseMove(ref m)) + return; + break; + + case WM_MOUSELEAVE: + if (!this.HandleMouseLeave(ref m)) + return; + break; + + case HDM_LAYOUT: + if (!this.HandleLayout(ref m)) + return; + break; + + case WM_DESTROY: + if (!this.HandleDestroy(ref m)) + return; + break; + + case WM_LBUTTONDOWN: + if (!this.HandleLButtonDown(ref m)) + return; + break; + + case WM_LBUTTONUP: + if (!this.HandleLButtonUp(ref m)) + return; + break; + } + + base.WndProc(ref m); + } + + private bool HandleReflectNotify(ref Message m) + { + NativeMethods.NMHDR nmhdr = (NativeMethods.NMHDR)m.GetLParam(typeof(NativeMethods.NMHDR)); + // System.Diagnostics.Debug.WriteLine(String.Format("rn: {0}", nmhdr.code)); + return true; + } + + /// + /// Handle the LButtonDown windows message + /// + /// + /// + protected bool HandleLButtonDown(ref Message m) + { + // Was there a header checkbox under the cursor? + this.columnIndexCheckBoxMouseDown = this.GetColumnCheckBoxUnderCursor(); + if (this.columnIndexCheckBoxMouseDown < 0) + return true; + + // Redraw the header so the checkbox redraws + this.Invalidate(); + + // Force the owning control to ignore this mouse click + // We don't want to sort the listview when they click the checkbox + m.Result = (IntPtr)1; + return false; + } + + private int columnIndexCheckBoxMouseDown = -1; + + /// + /// Handle the LButtonUp windows message + /// + /// + /// + protected bool HandleLButtonUp(ref Message m) { + //System.Diagnostics.Debug.WriteLine("WM_LBUTTONUP"); + + // Was the mouse released over a header checkbox? + if (this.columnIndexCheckBoxMouseDown < 0) + return true; + + // Was the mouse released over the same checkbox on which it was pressed? + if (this.columnIndexCheckBoxMouseDown != this.GetColumnCheckBoxUnderCursor()) + return true; + + // Toggle the header's checkbox + OLVColumn column = this.ListView.GetColumn(this.columnIndexCheckBoxMouseDown); + this.ListView.ToggleHeaderCheckBox(column); + + return true; + } + + /// + /// Handle the SetCursor windows message + /// + /// + /// + protected bool HandleSetCursor(ref Message m) { + if (this.IsCursorOverLockedDivider) { + m.Result = (IntPtr) 1; // Don't change the cursor + return false; + } + return true; + } + + /// + /// Handle the MouseMove windows message + /// + /// + /// + protected bool HandleMouseMove(ref Message m) { + + // Forward the mouse move event to the ListView itself + if (this.ListView.TriggerCellOverEventsWhenOverHeader) { + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + this.ListView.HandleMouseMove(new Point(x, y)); + } + + int columnIndex = this.ColumnIndexUnderCursor; + + // If the mouse has moved to a different header, pop the current tip (if any) + // For some reason, references this.ToolTip when in design mode, causes the + // columns to not be resizable by dragging the divider in the Designer. No idea why. + if (columnIndex != this.columnShowingTip && !this.ListView.IsDesignMode) { + this.ToolTip.PopToolTip(this); + this.columnShowingTip = columnIndex; + } + + // If the mouse has moved onto or away from a checkbox, we need to draw + int checkBoxUnderCursor = this.GetColumnCheckBoxUnderCursor(); + if (checkBoxUnderCursor != this.lastCheckBoxUnderCursor) { + this.Invalidate(); + this.lastCheckBoxUnderCursor = checkBoxUnderCursor; + } + + return true; + } + + private int columnShowingTip = -1; + private int lastCheckBoxUnderCursor = -1; + + /// + /// Handle the MouseLeave windows message + /// + /// + /// + protected bool HandleMouseLeave(ref Message m) { + // Forward the mouse leave event to the ListView itself + if (this.ListView.TriggerCellOverEventsWhenOverHeader) + this.ListView.HandleMouseMove(new Point(-1, -1)); + + return true; + } + + /// + /// Handle the Notify windows message + /// + /// + /// + protected bool HandleNotify(ref Message m) { + // Can this ever happen? JPP 2009-05-22 + if (m.LParam == IntPtr.Zero) + return false; + + NativeMethods.NMHDR nmhdr = (NativeMethods.NMHDR)m.GetLParam(typeof(NativeMethods.NMHDR)); + switch (nmhdr.code) + { + + case ToolTipControl.TTN_SHOW: + //System.Diagnostics.Debug.WriteLine("hdr TTN_SHOW"); + //System.Diagnostics.Trace.Assert(this.ToolTip.Handle == nmhdr.hwndFrom); + return this.ToolTip.HandleShow(ref m); + + case ToolTipControl.TTN_POP: + //System.Diagnostics.Debug.WriteLine("hdr TTN_POP"); + //System.Diagnostics.Trace.Assert(this.ToolTip.Handle == nmhdr.hwndFrom); + return this.ToolTip.HandlePop(ref m); + + case ToolTipControl.TTN_GETDISPINFO: + //System.Diagnostics.Debug.WriteLine("hdr TTN_GETDISPINFO"); + //System.Diagnostics.Trace.Assert(this.ToolTip.Handle == nmhdr.hwndFrom); + return this.ToolTip.HandleGetDispInfo(ref m); + } + + return false; + } + + /// + /// Handle the CustomDraw windows message + /// + /// + /// + internal virtual bool HandleHeaderCustomDraw(ref Message m) { + const int CDRF_NEWFONT = 2; + const int CDRF_SKIPDEFAULT = 4; + const int CDRF_NOTIFYPOSTPAINT = 0x10; + const int CDRF_NOTIFYITEMDRAW = 0x20; + + const int CDDS_PREPAINT = 1; + const int CDDS_POSTPAINT = 2; + const int CDDS_ITEM = 0x00010000; + const int CDDS_ITEMPREPAINT = (CDDS_ITEM | CDDS_PREPAINT); + const int CDDS_ITEMPOSTPAINT = (CDDS_ITEM | CDDS_POSTPAINT); + + NativeMethods.NMCUSTOMDRAW nmcustomdraw = (NativeMethods.NMCUSTOMDRAW) m.GetLParam(typeof (NativeMethods.NMCUSTOMDRAW)); + //System.Diagnostics.Debug.WriteLine(String.Format("header cd: {0:x}, {1}, {2:x}", nmcustomdraw.dwDrawStage, nmcustomdraw.dwItemSpec, nmcustomdraw.uItemState)); + switch (nmcustomdraw.dwDrawStage) { + case CDDS_PREPAINT: + this.cachedNeedsCustomDraw = this.NeedsCustomDraw(); + m.Result = (IntPtr) CDRF_NOTIFYITEMDRAW; + return true; + + case CDDS_ITEMPREPAINT: + int columnIndex = nmcustomdraw.dwItemSpec.ToInt32(); + OLVColumn column = this.ListView.GetColumn(columnIndex); + + // These don't work when visual styles are enabled + //NativeMethods.SetBkColor(nmcustomdraw.hdc, ColorTranslator.ToWin32(Color.Red)); + //NativeMethods.SetTextColor(nmcustomdraw.hdc, ColorTranslator.ToWin32(Color.Blue)); + //m.Result = IntPtr.Zero; + + if (this.cachedNeedsCustomDraw) { + using (Graphics g = Graphics.FromHdc(nmcustomdraw.hdc)) { + g.TextRenderingHint = ObjectListView.TextRenderingHint; + this.CustomDrawHeaderCell(g, columnIndex, nmcustomdraw.uItemState); + } + m.Result = (IntPtr) CDRF_SKIPDEFAULT; + } else { + const int CDIS_SELECTED = 1; + bool isPressed = ((nmcustomdraw.uItemState & CDIS_SELECTED) == CDIS_SELECTED); + + // We don't need to modify this based on checkboxes, since there can't be checkboxes if we are here + bool isHot = columnIndex == this.ColumnIndexUnderCursor; + + Font f = this.CalculateFont(column, isHot, isPressed); + + this.fontHandle = f.ToHfont(); + NativeMethods.SelectObject(nmcustomdraw.hdc, this.fontHandle); + + m.Result = (IntPtr) (CDRF_NEWFONT | CDRF_NOTIFYPOSTPAINT); + } + + return true; + + case CDDS_ITEMPOSTPAINT: + if (this.fontHandle != IntPtr.Zero) { + NativeMethods.DeleteObject(this.fontHandle); + this.fontHandle = IntPtr.Zero; + } + break; + } + + return false; + } + + private bool cachedNeedsCustomDraw; + private IntPtr fontHandle; + + /// + /// The message divides a ListView's space between the header and the rows of the listview. + /// The WINDOWPOS structure controls the headers bounds, the RECT controls the listview bounds. + /// + /// + /// + protected bool HandleLayout(ref Message m) { + if (this.ListView.HeaderStyle == ColumnHeaderStyle.None) + return true; + + NativeMethods.HDLAYOUT hdlayout = (NativeMethods.HDLAYOUT) m.GetLParam(typeof (NativeMethods.HDLAYOUT)); + NativeMethods.RECT rect = (NativeMethods.RECT) Marshal.PtrToStructure(hdlayout.prc, typeof (NativeMethods.RECT)); + NativeMethods.WINDOWPOS wpos = (NativeMethods.WINDOWPOS) Marshal.PtrToStructure(hdlayout.pwpos, typeof (NativeMethods.WINDOWPOS)); + + using (Graphics g = this.ListView.CreateGraphics()) { + g.TextRenderingHint = ObjectListView.TextRenderingHint; + int height = this.CalculateHeight(g); + wpos.hwnd = this.Handle; + wpos.hwndInsertAfter = IntPtr.Zero; + wpos.flags = NativeMethods.SWP_FRAMECHANGED; + wpos.x = rect.left; + wpos.y = rect.top; + wpos.cx = rect.right - rect.left; + wpos.cy = height; + + rect.top = height; + + Marshal.StructureToPtr(rect, hdlayout.prc, false); + Marshal.StructureToPtr(wpos, hdlayout.pwpos, false); + } + + this.ListView.BeginInvoke((MethodInvoker) delegate { + this.Invalidate(); + this.ListView.Invalidate(); + }); + return false; + } + + /// + /// Handle when the underlying header control is destroyed + /// + /// + /// + protected bool HandleDestroy(ref Message m) { + if (this.toolTip != null) { + this.toolTip.Showing -= new EventHandler(this.ListView.HeaderToolTipShowingCallback); + } + return false; + } + + #endregion + + #region Rendering + + /// + /// Does this header need to be custom drawn? + /// + /// Word wrapping and colored text require custom drawing. Funnily enough, we + /// can change the font natively. + protected bool NeedsCustomDraw() { + if (this.WordWrap) + return true; + + if (this.ListView.HeaderUsesThemes) + return false; + + if (this.NeedsCustomDraw(this.ListView.HeaderFormatStyle)) + return true; + + foreach (OLVColumn column in this.ListView.Columns) { + if (column.HasHeaderImage || + !column.ShowTextInHeader || + column.IsHeaderVertical || + this.HasFilterIndicator(column) || + this.HasCheckBox(column) || + column.TextAlign != column.HeaderTextAlignOrDefault || + (column.Index == 0 && column.HeaderTextAlignOrDefault != HorizontalAlignment.Left) || + this.NeedsCustomDraw(column.HeaderFormatStyle)) + return true; + } + + return false; + } + + private bool NeedsCustomDraw(HeaderFormatStyle style) { + if (style == null) + return false; + + return (this.NeedsCustomDraw(style.Normal) || + this.NeedsCustomDraw(style.Hot) || + this.NeedsCustomDraw(style.Pressed)); + } + + private bool NeedsCustomDraw(HeaderStateStyle style) { + if (style == null) + return false; + + // If we want fancy colors or frames, we have to custom draw. Oddly enough, we + // can handle font changes without custom drawing. + if (!style.BackColor.IsEmpty) + return true; + + if (style.FrameWidth > 0f && !style.FrameColor.IsEmpty) + return true; + + return (!style.ForeColor.IsEmpty && style.ForeColor != Color.Black); + } + + /// + /// Draw one cell of the header + /// + /// + /// + /// + protected void CustomDrawHeaderCell(Graphics g, int columnIndex, int itemState) { + OLVColumn column = this.ListView.GetColumn(columnIndex); + + bool hasCheckBox = this.HasCheckBox(column); + bool isMouseOverCheckBox = columnIndex == this.lastCheckBoxUnderCursor; + bool isMouseDownOnCheckBox = isMouseOverCheckBox && Control.MouseButtons == MouseButtons.Left; + bool isHot = (columnIndex == this.ColumnIndexUnderCursor) && (!(hasCheckBox && isMouseOverCheckBox)); + + const int CDIS_SELECTED = 1; + bool isPressed = ((itemState & CDIS_SELECTED) == CDIS_SELECTED); + + // System.Diagnostics.Debug.WriteLine(String.Format("{2:hh:mm:ss.ff} - HeaderCustomDraw: {0}, {1}", columnIndex, itemState, DateTime.Now)); + + // Calculate which style should be used for the header + HeaderStateStyle stateStyle = this.CalculateStateStyle(column, isHot, isPressed); + + // If there is an owner drawn delegate installed, give it a chance to draw the header + Rectangle fullCellBounds = this.GetItemRect(columnIndex); + if (column.HeaderDrawing != null) + { + if (!column.HeaderDrawing(g, fullCellBounds, columnIndex, column, isPressed, stateStyle)) + return; + } + + // Draw the background + if (this.ListView.HeaderUsesThemes && + VisualStyleRenderer.IsSupported && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.Item.Normal)) + this.DrawThemedBackground(g, fullCellBounds, columnIndex, isPressed, isHot); + else + this.DrawUnthemedBackground(g, fullCellBounds, columnIndex, isPressed, isHot, stateStyle); + + Rectangle r = this.GetHeaderDrawRect(columnIndex); + + // Draw the sort indicator if this column has one + if (this.HasSortIndicator(column)) { + if (this.ListView.HeaderUsesThemes && + VisualStyleRenderer.IsSupported && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.SortArrow.SortedUp)) + this.DrawThemedSortIndicator(g, r); + else + r = this.DrawUnthemedSortIndicator(g, r); + } + + if (this.HasFilterIndicator(column)) + r = this.DrawFilterIndicator(g, r); + + if (hasCheckBox) + r = this.DrawCheckBox(g, r, column.HeaderCheckState, column.HeaderCheckBoxDisabled, isMouseOverCheckBox, isMouseDownOnCheckBox); + + // Debugging - Where is the text going to be drawn + // g.DrawRectangle(Pens.Blue, r); + + // Finally draw the text + this.DrawHeaderImageAndText(g, r, column, stateStyle); + } + + private Rectangle DrawCheckBox(Graphics g, Rectangle r, CheckState checkState, bool isDisabled, bool isHot, + bool isPressed) { + CheckBoxState checkBoxState = this.GetCheckBoxState(checkState, isDisabled, isHot, isPressed); + Rectangle checkBoxBounds = this.CalculateCheckBoxBounds(g, r); + CheckBoxRenderer.DrawCheckBox(g, checkBoxBounds.Location, checkBoxState); + + // Move the left edge without changing the right edge + int newX = checkBoxBounds.Right + 3; + r.Width -= (newX - r.X); + r.X = newX; + + return r; + } + + private Rectangle CalculateCheckBoxBounds(Graphics g, Rectangle cellBounds) { + Size checkBoxSize = CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.CheckedNormal); + + // Vertically center the checkbox + int topOffset = (cellBounds.Height - checkBoxSize.Height)/2; + return new Rectangle(cellBounds.X + 3, cellBounds.Y + topOffset, checkBoxSize.Width, checkBoxSize.Height); + } + + private CheckBoxState GetCheckBoxState(CheckState checkState, bool isDisabled, bool isHot, bool isPressed) { + // Should the checkbox be drawn as disabled? + if (isDisabled) { + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedDisabled; + case CheckState.Unchecked: + return CheckBoxState.UncheckedDisabled; + default: + return CheckBoxState.MixedDisabled; + } + } + + // Is the mouse button currently down? + if (isPressed) { + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedPressed; + case CheckState.Unchecked: + return CheckBoxState.UncheckedPressed; + default: + return CheckBoxState.MixedPressed; + } + } + + // Is the cursor currently over this checkbox? + if (isHot) { + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedHot; + case CheckState.Unchecked: + return CheckBoxState.UncheckedHot; + default: + return CheckBoxState.MixedHot; + } + } + + // Not hot and not disabled -- just draw it normally + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedNormal; + case CheckState.Unchecked: + return CheckBoxState.UncheckedNormal; + default: + return CheckBoxState.MixedNormal; + } + } + + /// + /// Draw a background for the header, without using Themes. + /// + /// + /// + /// + /// + /// + /// + protected void DrawUnthemedBackground(Graphics g, Rectangle r, int columnIndex, bool isPressed, bool isHot, HeaderStateStyle stateStyle) { + if (stateStyle.BackColor.IsEmpty) + // I know we're supposed to be drawing the unthemed background, but let's just see if we + // can draw something more interesting than the dull raised block + if (VisualStyleRenderer.IsSupported && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.Item.Normal)) + this.DrawThemedBackground(g, r, columnIndex, isPressed, isHot); + else + ControlPaint.DrawBorder3D(g, r, Border3DStyle.RaisedInner); + else { + using (Brush b = new SolidBrush(stateStyle.BackColor)) + g.FillRectangle(b, r); + } + + // Draw the frame if the style asks for one + if (!stateStyle.FrameColor.IsEmpty && stateStyle.FrameWidth > 0f) { + RectangleF r2 = r; + r2.Inflate(-stateStyle.FrameWidth, -stateStyle.FrameWidth); + using (Pen pen = new Pen(stateStyle.FrameColor, stateStyle.FrameWidth)) + g.DrawRectangle(pen, r2.X, r2.Y, r2.Width, r2.Height); + } + } + + /// + /// Draw a more-or-less pure themed header background. + /// + /// + /// + /// + /// + /// + protected void DrawThemedBackground(Graphics g, Rectangle r, int columnIndex, bool isPressed, bool isHot) { + int part = 1; // normal item + if (columnIndex == 0 && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.ItemLeft.Normal)) + part = 2; // left item + if (columnIndex == this.ListView.Columns.Count - 1 && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.ItemRight.Normal)) + part = 3; // right item + + int state = 1; // normal state + if (isPressed) + state = 3; // pressed + else if (isHot) + state = 2; // hot + + VisualStyleRenderer renderer = new VisualStyleRenderer("HEADER", part, state); + renderer.DrawBackground(g, r); + } + + /// + /// Draw a sort indicator using themes + /// + /// + /// + protected void DrawThemedSortIndicator(Graphics g, Rectangle r) { + VisualStyleRenderer renderer2 = null; + if (this.ListView.LastSortOrder == SortOrder.Ascending) + renderer2 = new VisualStyleRenderer(VisualStyleElement.Header.SortArrow.SortedUp); + if (this.ListView.LastSortOrder == SortOrder.Descending) + renderer2 = new VisualStyleRenderer(VisualStyleElement.Header.SortArrow.SortedDown); + if (renderer2 != null) { + Size sz = renderer2.GetPartSize(g, ThemeSizeType.True); + Point pt = renderer2.GetPoint(PointProperty.Offset); + // GetPoint() should work, but if it doesn't, put the arrow in the top middle + if (pt.X == 0 && pt.Y == 0) + pt = new Point(r.X + (r.Width/2) - (sz.Width/2), r.Y); + renderer2.DrawBackground(g, new Rectangle(pt, sz)); + } + } + + /// + /// Draw a sort indicator without using themes + /// + /// + /// + /// + protected Rectangle DrawUnthemedSortIndicator(Graphics g, Rectangle r) { + // No theme support for sort indicators. So, we draw a triangle at the right edge + // of the column header. + const int triangleHeight = 16; + const int triangleWidth = 16; + const int midX = triangleWidth/2; + const int midY = (triangleHeight/2) - 1; + const int deltaX = midX - 2; + const int deltaY = deltaX/2; + + Point triangleLocation = new Point(r.Right - triangleWidth - 2, r.Top + (r.Height - triangleHeight)/2); + Point[] pts = new Point[] {triangleLocation, triangleLocation, triangleLocation}; + + if (this.ListView.LastSortOrder == SortOrder.Ascending) { + pts[0].Offset(midX - deltaX, midY + deltaY); + pts[1].Offset(midX, midY - deltaY - 1); + pts[2].Offset(midX + deltaX, midY + deltaY); + } else { + pts[0].Offset(midX - deltaX, midY - deltaY); + pts[1].Offset(midX, midY + deltaY); + pts[2].Offset(midX + deltaX, midY - deltaY); + } + + g.FillPolygon(Brushes.SlateGray, pts); + r.Width = r.Width - triangleWidth; + return r; + } + + /// + /// Draw an indication that this column has a filter applied to it + /// + /// + /// + /// + protected Rectangle DrawFilterIndicator(Graphics g, Rectangle r) { + int width = this.CalculateFilterIndicatorWidth(r); + if (width <= 0) + return r; + + Image indicator = Resources.ColumnFilterIndicator; + int x = r.Right - width; + int y = r.Top + (r.Height - indicator.Height)/2; + g.DrawImageUnscaled(indicator, x, y); + + r.Width -= width; + return r; + } + + private int CalculateFilterIndicatorWidth(Rectangle r) { + if (Resources.ColumnFilterIndicator == null || r.Width < 48) + return 0; + return Resources.ColumnFilterIndicator.Width + 1; + } + + /// + /// Draw the header's image and text + /// + /// + /// + /// + /// + protected void DrawHeaderImageAndText(Graphics g, Rectangle r, OLVColumn column, HeaderStateStyle stateStyle) { + + TextFormatFlags flags = this.TextFormatFlags; + flags |= TextFormatFlags.VerticalCenter; + if (column.HeaderTextAlignOrDefault == HorizontalAlignment.Center) + flags |= TextFormatFlags.HorizontalCenter; + if (column.HeaderTextAlignOrDefault == HorizontalAlignment.Right) + flags |= TextFormatFlags.Right; + + Font f = this.ListView.HeaderUsesThemes ? this.ListView.Font : stateStyle.Font ?? this.ListView.Font; + Color color = this.ListView.HeaderUsesThemes ? Color.Black : stateStyle.ForeColor; + if (color.IsEmpty) + color = Color.Black; + + const int imageTextGap = 3; + + if (column.IsHeaderVertical) { + DrawVerticalText(g, r, column, f, color); + } else { + // Does the column have a header image and is there space for it? + if (column.HasHeaderImage && r.Width > column.ImageList.ImageSize.Width*2) + DrawImageAndText(g, r, column, flags, f, color, imageTextGap); + else + DrawText(g, r, column, flags, f, color); + } + } + + private void DrawText(Graphics g, Rectangle r, OLVColumn column, TextFormatFlags flags, Font f, Color color) { + if (column.ShowTextInHeader) + TextRenderer.DrawText(g, column.Text, f, r, color, Color.Transparent, flags); + } + + private void DrawImageAndText(Graphics g, Rectangle r, OLVColumn column, TextFormatFlags flags, Font f, + Color color, int imageTextGap) { + Rectangle textRect = r; + textRect.X += (column.ImageList.ImageSize.Width + imageTextGap); + textRect.Width -= (column.ImageList.ImageSize.Width + imageTextGap); + + Size textSize = Size.Empty; + if (column.ShowTextInHeader) + textSize = TextRenderer.MeasureText(g, column.Text, f, textRect.Size, flags); + + int imageY = r.Top + ((r.Height - column.ImageList.ImageSize.Height)/2); + int imageX = textRect.Left; + if (column.HeaderTextAlignOrDefault == HorizontalAlignment.Center) + imageX = textRect.Left + ((textRect.Width - textSize.Width)/2); + if (column.HeaderTextAlignOrDefault == HorizontalAlignment.Right) + imageX = textRect.Right - textSize.Width; + imageX -= (column.ImageList.ImageSize.Width + imageTextGap); + + column.ImageList.Draw(g, imageX, imageY, column.ImageList.Images.IndexOfKey(column.HeaderImageKey)); + + this.DrawText(g, textRect, column, flags, f, color); + } + + private static void DrawVerticalText(Graphics g, Rectangle r, OLVColumn column, Font f, Color color) { + try { + // Create a matrix transformation that will rotate the text 90 degrees vertically + // AND place the text in the middle of where it was previously. [Think of tipping + // a box over by its bottom left edge -- you have to move it back a bit so it's + // in the same place as it started] + Matrix m = new Matrix(); + m.RotateAt(-90, new Point(r.X, r.Bottom)); + m.Translate(0, r.Height); + g.Transform = m; + StringFormat fmt = new StringFormat(StringFormatFlags.NoWrap); + fmt.Alignment = StringAlignment.Near; + fmt.LineAlignment = column.HeaderTextAlignAsStringAlignment; + //fmt.Trimming = StringTrimming.EllipsisCharacter; + + // The drawing is rotated 90 degrees, so switch our text boundaries + Rectangle textRect = r; + textRect.Width = r.Height; + textRect.Height = r.Width; + using (Brush b = new SolidBrush(color)) + g.DrawString(column.Text, f, b, textRect, fmt); + } + finally { + g.ResetTransform(); + } + } + + /// + /// Return the header format that should be used for the given column + /// + /// + /// + protected HeaderFormatStyle CalculateHeaderStyle(OLVColumn column) { + return column.HeaderFormatStyle ?? this.ListView.HeaderFormatStyle ?? new HeaderFormatStyle(); + } + + /// + /// What style should be applied to the header? + /// + /// + /// + /// + /// + protected HeaderStateStyle CalculateStateStyle(OLVColumn column, bool isHot, bool isPressed) { + HeaderFormatStyle headerStyle = this.CalculateHeaderStyle(column); + if (this.ListView.IsDesignMode) + return headerStyle.Normal; + if (isPressed) + return headerStyle.Pressed; + if (isHot) + return headerStyle.Hot; + return headerStyle.Normal; + } + + /// + /// What font should be used to draw the header text? + /// + /// + /// + /// + /// + protected Font CalculateFont(OLVColumn column, bool isHot, bool isPressed) { + HeaderStateStyle stateStyle = this.CalculateStateStyle(column, isHot, isPressed); + return stateStyle.Font ?? this.ListView.Font; + } + + /// + /// What flags will be used when drawing text + /// + protected TextFormatFlags TextFormatFlags { + get { + TextFormatFlags flags = TextFormatFlags.EndEllipsis | + TextFormatFlags.NoPrefix | + TextFormatFlags.WordEllipsis | + TextFormatFlags.PreserveGraphicsTranslateTransform; + if (this.WordWrap) + flags |= TextFormatFlags.WordBreak; + else + flags |= TextFormatFlags.SingleLine; + if (this.ListView.RightToLeft == RightToLeft.Yes) + flags |= TextFormatFlags.RightToLeft; + + return flags; + } + } + + /// + /// Perform a HitTest for the header control + /// + /// + /// + /// Null if the given point isn't over the header + internal OlvListViewHitTestInfo.HeaderHitTestInfo HitTest(int x, int y) + { + Rectangle r = this.ClientRectangle; + if (!r.Contains(x, y)) + return null; + + Point pt = new Point(x + this.ListView.LowLevelScrollPosition.X, y); + + OlvListViewHitTestInfo.HeaderHitTestInfo hti = new OlvListViewHitTestInfo.HeaderHitTestInfo(); + hti.ColumnIndex = NativeMethods.GetColumnUnderPoint(this.Handle, pt); + hti.IsOverCheckBox = this.IsPointOverHeaderCheckBox(hti.ColumnIndex, pt); + hti.OverDividerIndex = NativeMethods.GetDividerUnderPoint(this.Handle, pt); + + return hti; + } + + #endregion + } +} diff --git a/ObjectListView/SubControls/ToolStripCheckedListBox.cs b/ObjectListView/SubControls/ToolStripCheckedListBox.cs new file mode 100644 index 00000000..e8eab011 --- /dev/null +++ b/ObjectListView/SubControls/ToolStripCheckedListBox.cs @@ -0,0 +1,189 @@ +/* + * ToolStripCheckedListBox - Puts a CheckedListBox into a tool strip menu item + * + * Author: Phillip Piper + * Date: 4-March-2011 11:59 pm + * + * Change log: + * 2011-03-04 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Drawing; + +namespace BrightIdeasSoftware { + + /// + /// Instances of this class put a CheckedListBox into a tool strip menu item. + /// + public class ToolStripCheckedListBox : ToolStripControlHost { + + /// + /// Create a ToolStripCheckedListBox + /// + public ToolStripCheckedListBox() + : base(new CheckedListBox()) { + this.CheckedListBoxControl.MaximumSize = new Size(400, 700); + this.CheckedListBoxControl.ThreeDCheckBoxes = true; + this.CheckedListBoxControl.CheckOnClick = true; + this.CheckedListBoxControl.SelectionMode = SelectionMode.One; + } + + /// + /// Gets the control embedded in the menu + /// + public CheckedListBox CheckedListBoxControl { + get { + return Control as CheckedListBox; + } + } + + /// + /// Gets the items shown in the checkedlistbox + /// + public CheckedListBox.ObjectCollection Items { + get { + return this.CheckedListBoxControl.Items; + } + } + + /// + /// Gets or sets whether an item should be checked when it is clicked + /// + public bool CheckedOnClick { + get { + return this.CheckedListBoxControl.CheckOnClick; + } + set { + this.CheckedListBoxControl.CheckOnClick = value; + } + } + + /// + /// Gets a collection of the checked items + /// + public CheckedListBox.CheckedItemCollection CheckedItems { + get { + return this.CheckedListBoxControl.CheckedItems; + } + } + + /// + /// Add a possibly checked item to the control + /// + /// + /// + public void AddItem(object item, bool isChecked) { + this.Items.Add(item); + if (isChecked) + this.CheckedListBoxControl.SetItemChecked(this.Items.Count - 1, true); + } + + /// + /// Add an item with the given state to the control + /// + /// + /// + public void AddItem(object item, CheckState state) { + this.Items.Add(item); + this.CheckedListBoxControl.SetItemCheckState(this.Items.Count - 1, state); + } + + /// + /// Gets the checkedness of the i'th item + /// + /// + /// + public CheckState GetItemCheckState(int i) { + return this.CheckedListBoxControl.GetItemCheckState(i); + } + + /// + /// Set the checkedness of the i'th item + /// + /// + /// + public void SetItemState(int i, CheckState checkState) { + if (i >= 0 && i < this.Items.Count) + this.CheckedListBoxControl.SetItemCheckState(i, checkState); + } + + /// + /// Check all the items in the control + /// + public void CheckAll() { + for (int i = 0; i < this.Items.Count; i++) + this.CheckedListBoxControl.SetItemChecked(i, true); + } + + /// + /// Unchecked all the items in the control + /// + public void UncheckAll() { + for (int i = 0; i < this.Items.Count; i++) + this.CheckedListBoxControl.SetItemChecked(i, false); + } + + #region Events + + /// + /// Listen for events on the underlying control + /// + /// + protected override void OnSubscribeControlEvents(Control c) { + base.OnSubscribeControlEvents(c); + + CheckedListBox control = (CheckedListBox)c; + control.ItemCheck += new ItemCheckEventHandler(OnItemCheck); + } + + /// + /// Stop listening for events on the underlying control + /// + /// + protected override void OnUnsubscribeControlEvents(Control c) { + base.OnUnsubscribeControlEvents(c); + + CheckedListBox control = (CheckedListBox)c; + control.ItemCheck -= new ItemCheckEventHandler(OnItemCheck); + } + + /// + /// Tell the world that an item was checked + /// + public event ItemCheckEventHandler ItemCheck; + + /// + /// Trigger the ItemCheck event + /// + /// + /// + private void OnItemCheck(object sender, ItemCheckEventArgs e) { + if (ItemCheck != null) { + ItemCheck(this, e); + } + } + + #endregion + } +} diff --git a/ObjectListView/SubControls/ToolTipControl.cs b/ObjectListView/SubControls/ToolTipControl.cs new file mode 100644 index 00000000..22c1f636 --- /dev/null +++ b/ObjectListView/SubControls/ToolTipControl.cs @@ -0,0 +1,699 @@ +/* + * ToolTipControl - A limited wrapper around a Windows tooltip control + * + * For some reason, the ToolTip class in the .NET framework is implemented in a significantly + * different manner to other controls. For our purposes, the worst of these problems + * is that we cannot get the Handle, so we cannot send Windows level messages to the control. + * + * Author: Phillip Piper + * Date: 2009-05-17 7:22PM + * + * Change log: + * v2.3 + * 2009-06-13 JPP - Moved ToolTipShowingEventArgs to Events.cs + * v2.2 + * 2009-06-06 JPP - Fixed some Vista specific problems + * 2009-05-17 JPP - Initial version + * + * TO DO: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.ComponentModel; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using System.Security.Permissions; + +namespace BrightIdeasSoftware +{ + /// + /// A limited wrapper around a Windows tooltip window. + /// + public class ToolTipControl : NativeWindow + { + #region Constants + + /// + /// These are the standard icons that a tooltip can display. + /// + public enum StandardIcons + { + /// + /// No icon + /// + None = 0, + + /// + /// Info + /// + Info = 1, + + /// + /// Warning + /// + Warning = 2, + + /// + /// Error + /// + Error = 3, + + /// + /// Large info (Vista and later only) + /// + InfoLarge = 4, + + /// + /// Large warning (Vista and later only) + /// + WarningLarge = 5, + + /// + /// Large error (Vista and later only) + /// + ErrorLarge = 6 + } + + const int GWL_STYLE = -16; + const int WM_GETFONT = 0x31; + const int WM_SETFONT = 0x30; + const int WS_BORDER = 0x800000; + const int WS_EX_TOPMOST = 8; + + const int TTM_ADDTOOL = 0x432; + const int TTM_ADJUSTRECT = 0x400 + 31; + const int TTM_DELTOOL = 0x433; + const int TTM_GETBUBBLESIZE = 0x400 + 30; + const int TTM_GETCURRENTTOOL = 0x400 + 59; + const int TTM_GETTIPBKCOLOR = 0x400 + 22; + const int TTM_GETTIPTEXTCOLOR = 0x400 + 23; + const int TTM_GETDELAYTIME = 0x400 + 21; + const int TTM_NEWTOOLRECT = 0x400 + 52; + const int TTM_POP = 0x41c; + const int TTM_SETDELAYTIME = 0x400 + 3; + const int TTM_SETMAXTIPWIDTH = 0x400 + 24; + const int TTM_SETTIPBKCOLOR = 0x400 + 19; + const int TTM_SETTIPTEXTCOLOR = 0x400 + 20; + const int TTM_SETTITLE = 0x400 + 33; + const int TTM_SETTOOLINFO = 0x400 + 54; + + const int TTF_IDISHWND = 1; + //const int TTF_ABSOLUTE = 0x80; + const int TTF_CENTERTIP = 2; + const int TTF_RTLREADING = 4; + const int TTF_SUBCLASS = 0x10; + //const int TTF_TRACK = 0x20; + //const int TTF_TRANSPARENT = 0x100; + const int TTF_PARSELINKS = 0x1000; + + const int TTS_NOPREFIX = 2; + const int TTS_BALLOON = 0x40; + const int TTS_USEVISUALSTYLE = 0x100; + + const int TTN_FIRST = -520; + + /// + /// + /// + public const int TTN_SHOW = (TTN_FIRST - 1); + + /// + /// + /// + public const int TTN_POP = (TTN_FIRST - 2); + + /// + /// + /// + public const int TTN_LINKCLICK = (TTN_FIRST - 3); + + /// + /// + /// + public const int TTN_GETDISPINFO = (TTN_FIRST - 10); + + const int TTDT_AUTOMATIC = 0; + const int TTDT_RESHOW = 1; + const int TTDT_AUTOPOP = 2; + const int TTDT_INITIAL = 3; + + #endregion + + #region Properties + + /// + /// Get or set if the style of the tooltip control + /// + internal int WindowStyle { + get { + return (int)NativeMethods.GetWindowLong(this.Handle, GWL_STYLE); + } + set { + NativeMethods.SetWindowLong(this.Handle, GWL_STYLE, value); + } + } + + /// + /// Get or set if the tooltip should be shown as a balloon + /// + public bool IsBalloon { + get { + return (this.WindowStyle & TTS_BALLOON) == TTS_BALLOON; + } + set { + if (this.IsBalloon == value) + return; + + int windowStyle = this.WindowStyle; + if (value) { + windowStyle |= (TTS_BALLOON | TTS_USEVISUALSTYLE); + // On XP, a border makes the balloon look wrong + if (!ObjectListView.IsVistaOrLater) + windowStyle &= ~WS_BORDER; + } else { + windowStyle &= ~(TTS_BALLOON | TTS_USEVISUALSTYLE); + if (!ObjectListView.IsVistaOrLater) { + if (this.hasBorder) + windowStyle |= WS_BORDER; + else + windowStyle &= ~WS_BORDER; + } + } + this.WindowStyle = windowStyle; + } + } + + /// + /// Get or set if the tooltip should be shown as a balloon + /// + public bool HasBorder { + get { + return this.hasBorder; + } + set { + if (this.hasBorder == value) + return; + + if (value) { + this.WindowStyle |= WS_BORDER; + } else { + this.WindowStyle &= ~WS_BORDER; + } + } + } + private bool hasBorder = true; + + /// + /// Get or set the background color of the tooltip + /// + public Color BackColor { + get { + int color = (int)NativeMethods.SendMessage(this.Handle, TTM_GETTIPBKCOLOR, 0, 0); + return ColorTranslator.FromWin32(color); + } + set { + // For some reason, setting the color fails on Vista and messes up later ops. + // So we don't even try to set it. + if (!ObjectListView.IsVistaOrLater) { + int color = ColorTranslator.ToWin32(value); + NativeMethods.SendMessage(this.Handle, TTM_SETTIPBKCOLOR, color, 0); + //int x2 = Marshal.GetLastWin32Error(); + } + } + } + + /// + /// Get or set the color of the text and border on the tooltip. + /// + public Color ForeColor { + get { + int color = (int)NativeMethods.SendMessage(this.Handle, TTM_GETTIPTEXTCOLOR, 0, 0); + return ColorTranslator.FromWin32(color); + } + set { + // For some reason, setting the color fails on Vista and messes up later ops. + // So we don't even try to set it. + if (!ObjectListView.IsVistaOrLater) { + int color = ColorTranslator.ToWin32(value); + NativeMethods.SendMessage(this.Handle, TTM_SETTIPTEXTCOLOR, color, 0); + } + } + } + + /// + /// Get or set the title that will be shown on the tooltip. + /// + public string Title { + get { + return this.title; + } + set { + if (String.IsNullOrEmpty(value)) + this.title = String.Empty; + else + if (value.Length >= 100) + this.title = value.Substring(0, 99); + else + this.title = value; + NativeMethods.SendMessageString(this.Handle, TTM_SETTITLE, (int)this.standardIcon, this.title); + } + } + private string title; + + /// + /// Get or set the icon that will be shown on the tooltip. + /// + public StandardIcons StandardIcon { + get { + return this.standardIcon; + } + set { + this.standardIcon = value; + NativeMethods.SendMessageString(this.Handle, TTM_SETTITLE, (int)this.standardIcon, this.title); + } + } + private StandardIcons standardIcon; + + /// + /// Gets or sets the font that will be used to draw this control. + /// is still. + /// + /// Setting this to null reverts to the default font. + public Font Font { + get { + IntPtr hfont = NativeMethods.SendMessage(this.Handle, WM_GETFONT, 0, 0); + if (hfont == IntPtr.Zero) + return Control.DefaultFont; + else + return Font.FromHfont(hfont); + } + set { + Font newFont = value ?? Control.DefaultFont; + if (newFont == this.font) + return; + + this.font = newFont; + IntPtr hfont = this.font.ToHfont(); // THINK: When should we delete this hfont? + NativeMethods.SendMessage(this.Handle, WM_SETFONT, hfont, 0); + } + } + private Font font; + + /// + /// Gets or sets how many milliseconds the tooltip will remain visible while the mouse + /// is still. + /// + public int AutoPopDelay { + get { return this.GetDelayTime(TTDT_AUTOPOP); } + set { this.SetDelayTime(TTDT_AUTOPOP, value); } + } + + /// + /// Gets or sets how many milliseconds the mouse must be still before the tooltip is shown. + /// + public int InitialDelay { + get { return this.GetDelayTime(TTDT_INITIAL); } + set { this.SetDelayTime(TTDT_INITIAL, value); } + } + + /// + /// Gets or sets how many milliseconds the mouse must be still before the tooltip is shown again. + /// + public int ReshowDelay { + get { return this.GetDelayTime(TTDT_RESHOW); } + set { this.SetDelayTime(TTDT_RESHOW, value); } + } + + private int GetDelayTime(int which) { + return (int)NativeMethods.SendMessage(this.Handle, TTM_GETDELAYTIME, which, 0); + } + + private void SetDelayTime(int which, int value) { + NativeMethods.SendMessage(this.Handle, TTM_SETDELAYTIME, which, value); + } + + #endregion + + #region Commands + + /// + /// Create the underlying control. + /// + /// The parent of the tooltip + /// This does nothing if the control has already been created + public void Create(IntPtr parentHandle) { + if (this.Handle != IntPtr.Zero) + return; + + CreateParams cp = new CreateParams(); + cp.ClassName = "tooltips_class32"; + cp.Style = TTS_NOPREFIX; + cp.ExStyle = WS_EX_TOPMOST; + cp.Parent = parentHandle; + this.CreateHandle(cp); + + // Ensure that multiline tooltips work correctly + this.SetMaxWidth(); + } + + /// + /// Take a copy of the current settings and restore them when the + /// tooltip is popped. + /// + /// + /// This call cannot be nested. Subsequent calls to this method will be ignored + /// until PopSettings() is called. + /// + public void PushSettings() { + // Ignore any nested calls + if (this.settings != null) + return; + this.settings = new Hashtable(); + this.settings["IsBalloon"] = this.IsBalloon; + this.settings["HasBorder"] = this.HasBorder; + this.settings["BackColor"] = this.BackColor; + this.settings["ForeColor"] = this.ForeColor; + this.settings["Title"] = this.Title; + this.settings["StandardIcon"] = this.StandardIcon; + this.settings["AutoPopDelay"] = this.AutoPopDelay; + this.settings["InitialDelay"] = this.InitialDelay; + this.settings["ReshowDelay"] = this.ReshowDelay; + this.settings["Font"] = this.Font; + } + private Hashtable settings; + + /// + /// Restore the settings of the tooltip as they were when PushSettings() + /// was last called. + /// + public void PopSettings() { + if (this.settings == null) + return; + + this.IsBalloon = (bool)this.settings["IsBalloon"]; + this.HasBorder = (bool)this.settings["HasBorder"]; + this.BackColor = (Color)this.settings["BackColor"]; + this.ForeColor = (Color)this.settings["ForeColor"]; + this.Title = (string)this.settings["Title"]; + this.StandardIcon = (StandardIcons)this.settings["StandardIcon"]; + this.AutoPopDelay = (int)this.settings["AutoPopDelay"]; + this.InitialDelay = (int)this.settings["InitialDelay"]; + this.ReshowDelay = (int)this.settings["ReshowDelay"]; + this.Font = (Font)this.settings["Font"]; + + this.settings = null; + } + + /// + /// Add the given window to those for whom this tooltip will show tips + /// + /// The window + public void AddTool(IWin32Window window) { + NativeMethods.TOOLINFO lParam = this.MakeToolInfoStruct(window); + NativeMethods.SendMessageTOOLINFO(this.Handle, TTM_ADDTOOL, 0, lParam); + } + + /// + /// Hide any currently visible tooltip + /// + /// + public void PopToolTip(IWin32Window window) { + NativeMethods.SendMessage(this.Handle, TTM_POP, 0, 0); + } + + //public void Munge() { + // NativeMethods.TOOLINFO tool = new NativeMethods.TOOLINFO(); + // IntPtr result = NativeMethods.SendMessageTOOLINFO(this.Handle, TTM_GETCURRENTTOOL, 0, tool); + // System.Diagnostics.Trace.WriteLine("-"); + // System.Diagnostics.Trace.WriteLine(result); + // result = NativeMethods.SendMessageTOOLINFO(this.Handle, TTM_GETBUBBLESIZE, 0, tool); + // System.Diagnostics.Trace.WriteLine(String.Format("{0} {1}", result.ToInt32() >> 16, result.ToInt32() & 0xFFFF)); + // NativeMethods.ChangeSize(this, result.ToInt32() & 0xFFFF, result.ToInt32() >> 16); + // //NativeMethods.RECT r = new NativeMethods.RECT(); + // //r.right + // //IntPtr x = NativeMethods.SendMessageRECT(this.Handle, TTM_ADJUSTRECT, true, ref r); + + // //System.Diagnostics.Trace.WriteLine(String.Format("{0} {1} {2} {3}", r.left, r.top, r.right, r.bottom)); + //} + + /// + /// Remove the given window from those managed by this tooltip + /// + /// + public void RemoveToolTip(IWin32Window window) { + NativeMethods.TOOLINFO lParam = this.MakeToolInfoStruct(window); + NativeMethods.SendMessageTOOLINFO(this.Handle, TTM_DELTOOL, 0, lParam); + } + + /// + /// Set the maximum width of a tooltip string. + /// + public void SetMaxWidth() { + this.SetMaxWidth(SystemInformation.MaxWindowTrackSize.Width); + } + + /// + /// Set the maximum width of a tooltip string. + /// + /// Setting this ensures that line breaks in the tooltip are honoured. + public void SetMaxWidth(int maxWidth) { + NativeMethods.SendMessage(this.Handle, TTM_SETMAXTIPWIDTH, 0, maxWidth); + } + + #endregion + + #region Implementation + + /// + /// Make a TOOLINFO structure for the given window + /// + /// + /// A filled in TOOLINFO + private NativeMethods.TOOLINFO MakeToolInfoStruct(IWin32Window window) { + + NativeMethods.TOOLINFO toolinfo_tooltip = new NativeMethods.TOOLINFO(); + toolinfo_tooltip.hwnd = window.Handle; + toolinfo_tooltip.uFlags = TTF_IDISHWND | TTF_SUBCLASS; + toolinfo_tooltip.uId = window.Handle; + toolinfo_tooltip.lpszText = (IntPtr)(-1); // LPSTR_TEXTCALLBACK + + return toolinfo_tooltip; + } + + /// + /// Handle a WmNotify message + /// + /// The msg + /// True if the message has been handled + protected virtual bool HandleNotify(ref Message msg) { + + //THINK: What do we have to do here? Nothing it seems :) + + //NativeMethods.NMHEADER nmheader = (NativeMethods.NMHEADER)msg.GetLParam(typeof(NativeMethods.NMHEADER)); + //System.Diagnostics.Trace.WriteLine("HandleNotify"); + //System.Diagnostics.Trace.WriteLine(nmheader.nhdr.code); + + //switch (nmheader.nhdr.code) { + //} + + return false; + } + + /// + /// Handle a get display info message + /// + /// The msg + /// True if the message has been handled + public virtual bool HandleGetDispInfo(ref Message msg) { + //System.Diagnostics.Trace.WriteLine("HandleGetDispInfo"); + this.SetMaxWidth(); + ToolTipShowingEventArgs args = new ToolTipShowingEventArgs(); + args.ToolTipControl = this; + this.OnShowing(args); + if (String.IsNullOrEmpty(args.Text)) + return false; + + this.ApplyEventFormatting(args); + + NativeMethods.NMTTDISPINFO dispInfo = (NativeMethods.NMTTDISPINFO)msg.GetLParam(typeof(NativeMethods.NMTTDISPINFO)); + dispInfo.lpszText = args.Text; + dispInfo.hinst = IntPtr.Zero; + if (args.RightToLeft == RightToLeft.Yes) + dispInfo.uFlags |= TTF_RTLREADING; + Marshal.StructureToPtr(dispInfo, msg.LParam, false); + + return true; + } + + private void ApplyEventFormatting(ToolTipShowingEventArgs args) { + if (!args.IsBalloon.HasValue && + !args.BackColor.HasValue && + !args.ForeColor.HasValue && + args.Title == null && + !args.StandardIcon.HasValue && + !args.AutoPopDelay.HasValue && + args.Font == null) + return; + + this.PushSettings(); + if (args.IsBalloon.HasValue) + this.IsBalloon = args.IsBalloon.Value; + if (args.BackColor.HasValue) + this.BackColor = args.BackColor.Value; + if (args.ForeColor.HasValue) + this.ForeColor = args.ForeColor.Value; + if (args.StandardIcon.HasValue) + this.StandardIcon = args.StandardIcon.Value; + if (args.AutoPopDelay.HasValue) + this.AutoPopDelay = args.AutoPopDelay.Value; + if (args.Font != null) + this.Font = args.Font; + if (args.Title != null) + this.Title = args.Title; + } + + /// + /// Handle a TTN_LINKCLICK message + /// + /// The msg + /// True if the message has been handled + /// This cannot call base.WndProc() since the msg may have come from another control. + public virtual bool HandleLinkClick(ref Message msg) { + //System.Diagnostics.Trace.WriteLine("HandleLinkClick"); + return false; + } + + /// + /// Handle a TTN_POP message + /// + /// The msg + /// True if the message has been handled + /// This cannot call base.WndProc() since the msg may have come from another control. + public virtual bool HandlePop(ref Message msg) { + //System.Diagnostics.Trace.WriteLine("HandlePop"); + this.PopSettings(); + return true; + } + + /// + /// Handle a TTN_SHOW message + /// + /// The msg + /// True if the message has been handled + /// This cannot call base.WndProc() since the msg may have come from another control. + public virtual bool HandleShow(ref Message msg) { + //System.Diagnostics.Trace.WriteLine("HandleShow"); + return false; + } + + /// + /// Handle a reflected notify message + /// + /// The msg + /// True if the message has been handled + protected virtual bool HandleReflectNotify(ref Message msg) { + + NativeMethods.NMHEADER nmheader = (NativeMethods.NMHEADER)msg.GetLParam(typeof(NativeMethods.NMHEADER)); + switch (nmheader.nhdr.code) { + case TTN_SHOW: + //System.Diagnostics.Trace.WriteLine("reflect TTN_SHOW"); + if (this.HandleShow(ref msg)) + return true; + break; + case TTN_POP: + //System.Diagnostics.Trace.WriteLine("reflect TTN_POP"); + if (this.HandlePop(ref msg)) + return true; + break; + case TTN_LINKCLICK: + //System.Diagnostics.Trace.WriteLine("reflect TTN_LINKCLICK"); + if (this.HandleLinkClick(ref msg)) + return true; + break; + case TTN_GETDISPINFO: + //System.Diagnostics.Trace.WriteLine("reflect TTN_GETDISPINFO"); + if (this.HandleGetDispInfo(ref msg)) + return true; + break; + } + + return false; + } + + /// + /// Mess with the basic message pump of the tooltip + /// + /// + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + override protected void WndProc(ref Message msg) { + //System.Diagnostics.Trace.WriteLine(String.Format("xx {0:x}", msg.Msg)); + switch (msg.Msg) { + case 0x4E: // WM_NOTIFY + if (!this.HandleNotify(ref msg)) + return; + break; + + case 0x204E: // WM_REFLECT_NOTIFY + if (!this.HandleReflectNotify(ref msg)) + return; + break; + } + + base.WndProc(ref msg); + } + + #endregion + + #region Events + + /// + /// Tell the world that a tooltip is about to show + /// + public event EventHandler Showing; + + /// + /// Tell the world that a tooltip is about to disappear + /// + public event EventHandler Pop; + + /// + /// + /// + /// + protected virtual void OnShowing(ToolTipShowingEventArgs e) { + if (this.Showing != null) + this.Showing(this, e); + } + + /// + /// + /// + /// + protected virtual void OnPop(EventArgs e) { + if (this.Pop != null) + this.Pop(this, e); + } + + #endregion + } + +} \ No newline at end of file diff --git a/ObjectListView/TreeListView.cs b/ObjectListView/TreeListView.cs new file mode 100644 index 00000000..0b52c63e --- /dev/null +++ b/ObjectListView/TreeListView.cs @@ -0,0 +1,2269 @@ +/* + * TreeListView - A listview that can show a tree of objects in a column + * + * Author: Phillip Piper + * Date: 23/09/2008 11:15 AM + * + * Change log: + * 2018-05-03 JPP - Added ITreeModel to allow models to provide the required information to TreeListView. + * 2018-04-30 JPP - Fix small visual glitch where connecting lines were not correctly drawn when filters changed + * v2.9.2 + * 2016-06-02 JPP - Added bounds check to GetNthObject(). + * v2.9 + * 2015-08-02 JPP - Fixed buy with hierarchical checkboxes where setting the checkedness of a deeply + * nested object would sometimes not correctly calculate the changes in the hierarchy. SF #150. + * 2015-06-27 JPP - Corrected small UI glitch when focus was lost and HideSelection was false. SF #135. + * v2.8.1 + * 2014-11-28 JPP - Fixed issue in RefreshObject() where a model with less children than previous that could not + * longer be expanded would cause an exception. + * 2014-11-23 JPP - Fixed an issue where collapsing a branch could leave the internal object->index map out of date. + * v2.8 + * 2014-10-08 JPP - Fixed an issue where pre-expanded branches would not initially expand properly + * 2014-09-29 JPP - Fixed issue where RefreshObject() on a root object could cause exceptions + * - Fixed issue where CollapseAll() while filtering could cause exception + * 2014-03-09 JPP - Fixed issue where removing a branches only child and then calling RefreshObject() + * could throw an exception. + * v2.7 + * 2014-02-23 JPP - Added Reveal() method to show a deeply nested models. + * 2014-02-05 JPP - Fix issue where refreshing a non-root item would collapse all expanded children of that item + * 2014-02-01 JPP - ClearObjects() now actually, you know, clears objects :) + * - Corrected issue where Expanded event was being raised twice. + * - RebuildChildren() no longer checks if CanExpand is true before rebuilding. + * 2014-01-16 JPP - Corrected an off-by-1 error in hit detection, which meant that clicking in the last 16 pixels + * of an items label was being ignored. + * 2013-11-20 JPP - Moved event triggers into Collapse() and Expand() so that the events are always triggered. + * - CheckedObjects now includes objects that are in a branch that is currently collapsed + * - CollapseAll() and ExpandAll() now trigger cancellable events + * 2013-09-29 JPP - Added TreeFactory to allow the underlying Tree to be replaced by another implementation. + * 2013-09-23 JPP - Fixed long standing issue where RefreshObject() would not work on root objects + * which overrode Equals()/GetHashCode(). + * 2013-02-23 JPP - Added HierarchicalCheckboxes. When this is true, the checkedness of a parent + * is an synopsis of the checkedness of its children. When all children are checked, + * the parent is checked. When all children are unchecked, the parent is unchecked. + * If some children are checked and some are not, the parent is indeterminate. + * v2.6 + * 2012-10-25 JPP - Circumvent annoying issue in ListView control where changing + * selection would leave artefacts on the control. + * 2012-08-10 JPP - Don't trigger selection changed events during expands + * + * v2.5.1 + * 2012-04-30 JPP - Fixed issue where CheckedObjects would return model objects that had been filtered out. + * - Allow any column to render the tree, not just column 0 (still not sure about this one) + * v2.5.0 + * 2011-04-20 JPP - Added ExpandedObjects property and RebuildAll() method. + * 2011-04-09 JPP - Added Expanding, Collapsing, Expanded and Collapsed events. + * The ..ing events are cancellable. These are only fired in response + * to user actions. + * v2.4.1 + * 2010-06-15 JPP - Fixed issue in Tree.RemoveObjects() which resulted in removed objects + * being reported as still existing. + * v2.3 + * 2009-09-01 JPP - Fixed off-by-one error that was messing up hit detection + * 2009-08-27 JPP - Fixed issue when dragging a node from one place to another in the tree + * v2.2.1 + * 2009-07-14 JPP - Clicks to the left of the expander in tree cells are now ignored. + * v2.2 + * 2009-05-12 JPP - Added tree traverse operations: GetParent and GetChildren. + * - Added DiscardAllState() to completely reset the TreeListView. + * 2009-05-10 JPP - Removed all unsafe code + * 2009-05-09 JPP - Fixed issue where any command (Expand/Collapse/Refresh) on a model + * object that was once visible but that is currently in a collapsed branch + * would cause the control to crash. + * 2009-05-07 JPP - Fixed issue where RefreshObjects() would fail when none of the given + * objects were present/visible. + * 2009-04-20 JPP - Fixed issue where calling Expand() on an already expanded branch confused + * the display of the children (SF#2499313) + * 2009-03-06 JPP - Calculate edit rectangle on column 0 more accurately + * v2.1 + * 2009-02-24 JPP - All commands now work when the list is empty (SF #2631054) + * - TreeListViews can now be printed with ListViewPrinter + * 2009-01-27 JPP - Changed to use new Renderer and HitTest scheme + * 2009-01-22 JPP - Added RevealAfterExpand property. If this is true (the default), + * after expanding a branch, the control scrolls to reveal as much of the + * expanded branch as possible. + * 2009-01-13 JPP - Changed TreeRenderer to work with visual styles are disabled + * v2.0.1 + * 2009-01-07 JPP - Made all public and protected methods virtual + * - Changed some classes from 'internal' to 'protected' so that they + * can be accessed by subclasses of TreeListView. + * 2008-12-22 JPP - Added UseWaitCursorWhenExpanding property + * - Made TreeRenderer public so that it can be subclassed + * - Added LinePen property to TreeRenderer to allow the connection drawing + * pen to be changed + * - Fixed some rendering issues where the text highlight rect was miscalculated + * - Fixed connection line problem when there is only a single root + * v2.0 + * 2008-12-10 JPP - Expand/collapse with mouse now works when there is no SmallImageList. + * 2008-12-01 JPP - Search-by-typing now works. + * 2008-11-26 JPP - Corrected calculation of expand/collapse icon (SF#2338819) + * - Fixed ugliness with dotted lines in renderer (SF#2332889) + * - Fixed problem with custom selection colors (SF#2338805) + * 2008-11-19 JPP - Expand/collapse now preserve the selection -- more or less :) + * - Overrode RefreshObjects() to rebuild the given objects and their children + * 2008-11-05 JPP - Added ExpandAll() and CollapseAll() commands + * - CanExpand is no longer cached + * - Renamed InitialBranches to RootModels since it deals with model objects + * 2008-09-23 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A TreeListView combines an expandable tree structure with list view columns. + /// + /// + /// To support tree operations, two delegates must be provided: + /// + /// + /// + /// CanExpandGetter + /// + /// + /// This delegate must accept a model object and return a boolean indicating + /// if that model should be expandable. + /// + /// + /// + /// + /// ChildrenGetter + /// + /// + /// This delegate must accept a model object and return an IEnumerable of model + /// objects that will be displayed as children of the parent model. This delegate will only be called + /// for a model object if the CanExpandGetter has already returned true for that model. + /// + /// + /// + /// + /// ParentGetter + /// + /// + /// This delegate must accept a model object and return the parent model. + /// This delegate will only be called when HierarchicalCheckboxes is true OR when Reveal() is called. + /// + /// + /// + /// + /// The top level branches of the tree are set via the Roots property. SetObjects(), AddObjects() + /// and RemoveObjects() are interpreted as operations on this collection of roots. + /// + /// + /// To add new children to an existing branch, make changes to your model objects and then + /// call RefreshObject() on the parent. + /// + /// The tree must be a directed acyclic graph -- no cycles are allowed. Put more mundanely, + /// each model object must appear only once in the tree. If the same model object appears in two + /// places in the tree, the control will become confused. + /// + public partial class TreeListView : VirtualObjectListView + { + /// + /// Make a default TreeListView + /// + public TreeListView() { + this.OwnerDraw = true; + this.View = View.Details; + this.CheckedObjectsMustStillExistInList = false; + +// ReSharper disable DoNotCallOverridableMethodsInConstructor + this.RegenerateTree(); + this.TreeColumnRenderer = new TreeRenderer(); +// ReSharper restore DoNotCallOverridableMethodsInConstructor + + // This improves hit detection even if we don't have any state image + this.SmallImageList = new ImageList(); + // this.StateImageList.ImageSize = new Size(6, 6); + } + + //------------------------------------------------------------------------------------------ + // Properties + + /// + /// This is the delegate that will be used to decide if a model object can be expanded. + /// + /// + /// + /// This is called *often* -- on every mouse move when required. It must be fast. + /// Don't do database lookups, linear searches, or pi calculations. Just return the + /// value of a property. + /// + /// + /// When this delegate is called, the TreeListView is not in a stable state. Don't make + /// calls back into the control. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual CanExpandGetterDelegate CanExpandGetter { + get { return this.TreeModel.CanExpandGetter; } + set { this.TreeModel.CanExpandGetter = value; } + } + + /// + /// Gets whether or not this listview is capable of showing groups + /// + [Browsable(false)] + public override bool CanShowGroups { + get { + return false; + } + } + + /// + /// This is the delegate that will be used to fetch the children of a model object + /// + /// + /// + /// This delegate will only be called if the CanExpand delegate has + /// returned true for the model object. + /// + /// + /// When this delegate is called, the TreeListView is not in a stable state. Don't do anything + /// that will result in calls being made back into the control. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual ChildrenGetterDelegate ChildrenGetter { + get { return this.TreeModel.ChildrenGetter; } + set { this.TreeModel.ChildrenGetter = value; } + } + + /// + /// This is the delegate that will be used to fetch the parent of a model object + /// + /// The parent of the given model, or null if the model doesn't exist or + /// if the model is a root + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ParentGetterDelegate ParentGetter { + get { return parentGetter ?? Tree.DefaultParentGetter; } + set { parentGetter = value; } + } + private ParentGetterDelegate parentGetter; + + /// + /// Get or set the collection of model objects that are checked. + /// When setting this property, any row whose model object isn't + /// in the given collection will be unchecked. Setting to null is + /// equivalent to unchecking all. + /// + /// + /// + /// This property returns a simple collection. Changes made to the returned + /// collection do NOT affect the list. This is different to the behaviour of + /// CheckedIndicies collection. + /// + /// + /// When getting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects. + /// When setting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects plus + /// the number of objects to be checked. + /// + /// + /// If the ListView is not currently showing CheckBoxes, this property does nothing. It does + /// not remember any check box settings made. + /// + /// + public override IList CheckedObjects { + get { + return base.CheckedObjects; + } + set { + ArrayList objectsToRecalculate = new ArrayList(this.CheckedObjects); + if (value != null) + objectsToRecalculate.AddRange(value); + + base.CheckedObjects = value; + + if (this.HierarchicalCheckboxes) + RecalculateHierarchicalCheckBoxGraph(objectsToRecalculate); + } + } + + /// + /// Gets or sets the model objects that are expanded. + /// + /// + /// This can be used to expand model objects before they are seen. + /// + /// Setting this does *not* force the control to rebuild + /// its display. You need to call RebuildAll(true). + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IEnumerable ExpandedObjects { + get { + return this.TreeModel.mapObjectToExpanded.Keys; + } + set { + this.TreeModel.mapObjectToExpanded.Clear(); + if (value != null) { + foreach (object x in value) + this.TreeModel.SetModelExpanded(x, true); + } + } + } + + /// + /// Gets or sets the filter that is applied to our whole list of objects. + /// TreeListViews do not currently support whole list filters. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IListFilter ListFilter { + get { return null; } + set { + System.Diagnostics.Debug.Assert(value == null, "TreeListView do not support ListFilters"); + } + } + + /// + /// Gets or sets whether this tree list view will display hierarchical checkboxes. + /// Hierarchical checkboxes is when a parent's "checkedness" is calculated from + /// the "checkedness" of its children. If all children are checked, the parent + /// will be checked. If all children are unchecked, the parent will also be unchecked. + /// If some children are checked and others are not, the parent will be indeterminate. + /// + /// + /// Hierarchical checkboxes don't work with either CheckStateGetters or CheckedAspectName + /// (which is basically the same thing). This is because it is too expensive to build the + /// initial state of the control if these are installed, since the control would have to walk + /// *every* branch recursively since a single bottom level leaf could change the checkedness + /// of the top root. + /// + [Category("ObjectListView"), + Description("Show hierarchical checkboxes be enabled?"), + DefaultValue(false)] + public virtual bool HierarchicalCheckboxes { + get { return this.hierarchicalCheckboxes; } + set { + if (this.hierarchicalCheckboxes == value) + return; + + this.hierarchicalCheckboxes = value; + this.CheckBoxes = value; + if (value) + this.TriStateCheckBoxes = false; + } + } + private bool hierarchicalCheckboxes; + + /// + /// Gets or sets the collection of root objects of the tree + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable Objects { + get { return this.Roots; } + set { this.Roots = value; } + } + + /// + /// Gets the collection of objects that will be considered when creating clusters + /// (which are used to generate Excel-like column filters) + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable ObjectsForClustering { + get { + for (int i = 0; i < this.TreeModel.GetObjectCount(); i++) + yield return this.TreeModel.GetNthObject(i); + } + } + + /// + /// After expanding a branch, should the TreeListView attempts to show as much of the + /// revealed descendants as possible. + /// + [Category("ObjectListView"), + Description("Should the parent of an expand subtree be scrolled to the top revealing the children?"), + DefaultValue(true)] + public bool RevealAfterExpand { + get { return revealAfterExpand; } + set { revealAfterExpand = value; } + } + private bool revealAfterExpand = true; + + /// + /// The model objects that form the top level branches of the tree. + /// + /// Setting this does NOT reset the state of the control. + /// In particular, it does not collapse branches. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable Roots { + get { return this.TreeModel.RootObjects; } + set { + this.TreeColumnRenderer = this.TreeColumnRenderer; + this.TreeModel.RootObjects = value ?? new ArrayList(); + this.UpdateVirtualListSize(); + } + } + + /// + /// Make sure that at least one column is displaying a tree. + /// If no columns is showing the tree, make column 0 do it. + /// + protected virtual void EnsureTreeRendererPresent(TreeRenderer renderer) { + if (this.Columns.Count == 0) + return; + + foreach (OLVColumn col in this.Columns) { + if (col.Renderer is TreeRenderer) { + col.Renderer = renderer; + return; + } + } + + // No column held a tree renderer, so give column 0 one + OLVColumn columnZero = this.GetColumn(0); + columnZero.Renderer = renderer; + columnZero.WordWrap = columnZero.WordWrap; + } + + /// + /// Gets or sets the renderer that will be used to draw the tree structure. + /// Setting this to null resets the renderer to default. + /// + /// If a column is currently rendering the tree, the renderer + /// for that column will be replaced. If no column is rendering the tree, + /// column 0 will be given this renderer. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual TreeRenderer TreeColumnRenderer { + get { return treeRenderer ?? (treeRenderer = new TreeRenderer()); } + set { + treeRenderer = value ?? new TreeRenderer(); + EnsureTreeRendererPresent(treeRenderer); + } + } + private TreeRenderer treeRenderer; + + /// + /// This is the delegate that will be used to create the underlying Tree structure + /// that the TreeListView uses to manage the information about the tree. + /// + /// + /// The factory must not return null. + /// + /// Most users of TreeListView will never have to use this delegate. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public TreeFactoryDelegate TreeFactory { + get { return treeFactory; } + set { treeFactory = value; } + } + private TreeFactoryDelegate treeFactory; + + /// + /// Should a wait cursor be shown when a branch is being expanded? + /// + /// When this is true, the wait cursor will be shown whilst the children of the + /// branch are being fetched. If the children of the branch have already been cached, + /// the cursor will not change. + [Category("ObjectListView"), + Description("Should a wait cursor be shown when a branch is being expanded?"), + DefaultValue(true)] + public virtual bool UseWaitCursorWhenExpanding { + get { return useWaitCursorWhenExpanding; } + set { useWaitCursorWhenExpanding = value; } + } + private bool useWaitCursorWhenExpanding = true; + + /// + /// Gets the model that is used to manage the tree structure + /// + /// + /// Don't mess with this property unless you really know what you are doing. + /// If you don't already know what it's for, you don't need it. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Tree TreeModel { + get { return this.treeModel; } + protected set { this.treeModel = value; } + } + private Tree treeModel; + + //------------------------------------------------------------------------------------------ + // Accessing + + /// + /// Return true if the branch at the given model is expanded + /// + /// + /// + public virtual bool IsExpanded(Object model) { + Branch br = this.TreeModel.GetBranch(model); + return (br != null && br.IsExpanded); + } + + //------------------------------------------------------------------------------------------ + // Commands + + /// + /// Collapse the subtree underneath the given model + /// + /// + public virtual void Collapse(Object model) { + if (this.GetItemCount() == 0) + return; + + OLVListItem item = this.ModelToItem(model); + TreeBranchCollapsingEventArgs args = new TreeBranchCollapsingEventArgs(model, item); + this.OnCollapsing(args); + if (args.Canceled) + return; + + IList selection = this.SelectedObjects; + int index = this.TreeModel.Collapse(model); + if (index >= 0) { + this.UpdateVirtualListSize(); + this.SelectedObjects = selection; + if (index < this.GetItemCount()) + this.RedrawItems(index, this.GetItemCount() - 1, true); + this.OnCollapsed(new TreeBranchCollapsedEventArgs(model, item)); + } + } + + /// + /// Collapse all subtrees within this control + /// + public virtual void CollapseAll() { + if (this.GetItemCount() == 0) + return; + + TreeBranchCollapsingEventArgs args = new TreeBranchCollapsingEventArgs(null, null); + this.OnCollapsing(args); + if (args.Canceled) + return; + + IList selection = this.SelectedObjects; + int index = this.TreeModel.CollapseAll(); + if (index >= 0) { + this.UpdateVirtualListSize(); + this.SelectedObjects = selection; + if (index < this.GetItemCount()) + this.RedrawItems(index, this.GetItemCount() - 1, true); + this.OnCollapsed(new TreeBranchCollapsedEventArgs(null, null)); + } + } + + /// + /// Remove all items from this list + /// + /// This method can safely be called from background threads. + public override void ClearObjects() { + if (this.InvokeRequired) + this.Invoke(new MethodInvoker(this.ClearObjects)); + else { + this.Roots = null; + this.DiscardAllState(); + } + } + + /// + /// Collapse all roots and forget everything we know about all models + /// + public virtual void DiscardAllState() { + this.CheckStateMap.Clear(); + this.RebuildAll(false); + } + + /// + /// Expand the subtree underneath the given model object + /// + /// + public virtual void Expand(Object model) { + if (this.GetItemCount() == 0) + return; + + // Give the world a chance to cancel the expansion + OLVListItem item = this.ModelToItem(model); + TreeBranchExpandingEventArgs args = new TreeBranchExpandingEventArgs(model, item); + this.OnExpanding(args); + if (args.Canceled) + return; + + // Remember the selection so we can put it back later + IList selection = this.SelectedObjects; + + // Expand the model first + int index = this.TreeModel.Expand(model); + if (index < 0) + return; + + // Update the size of the list and restore the selection + this.UpdateVirtualListSize(); + using (this.SuspendSelectionEventsDuring()) + this.SelectedObjects = selection; + + // Redraw the items that were changed by the expand operation + this.RedrawItems(index, this.GetItemCount() - 1, true); + + this.OnExpanded(new TreeBranchExpandedEventArgs(model, item)); + + if (this.RevealAfterExpand && index > 0) { + // TODO: This should be a separate method + this.BeginUpdate(); + try { + int countPerPage = NativeMethods.GetCountPerPage(this); + int descedentCount = this.TreeModel.GetVisibleDescendentCount(model); + // If all of the descendants can be shown in the window, make sure that last one is visible. + // If all the descendants can't fit into the window, move the model to the top of the window + // (which will show as many of the descendants as possible) + if (descedentCount < countPerPage) { + this.EnsureVisible(index + descedentCount); + } else { + this.TopItemIndex = index; + } + } + finally { + this.EndUpdate(); + } + } + } + + /// + /// Expand all the branches within this tree recursively. + /// + /// Be careful: this method could take a long time for large trees. + public virtual void ExpandAll() { + if (this.GetItemCount() == 0) + return; + + // Give the world a chance to cancel the expansion + TreeBranchExpandingEventArgs args = new TreeBranchExpandingEventArgs(null, null); + this.OnExpanding(args); + if (args.Canceled) + return; + + IList selection = this.SelectedObjects; + int index = this.TreeModel.ExpandAll(); + if (index < 0) + return; + + this.UpdateVirtualListSize(); + using (this.SuspendSelectionEventsDuring()) + this.SelectedObjects = selection; + this.RedrawItems(index, this.GetItemCount() - 1, true); + this.OnExpanded(new TreeBranchExpandedEventArgs(null, null)); + } + + /// + /// Completely rebuild the tree structure + /// + /// If true, the control will try to preserve selection and expansion + public virtual void RebuildAll(bool preserveState) { + int previousTopItemIndex = preserveState ? this.TopItemIndex : -1; + + this.RebuildAll( + preserveState ? this.SelectedObjects : null, + preserveState ? this.ExpandedObjects : null, + preserveState ? this.CheckedObjects : null); + + if (preserveState) + this.TopItemIndex = previousTopItemIndex; + } + + /// + /// Completely rebuild the tree structure + /// + /// If not null, this list of objects will be selected after the tree is rebuilt + /// If not null, this collection of objects will be expanded after the tree is rebuilt + /// If not null, this collection of objects will be checked after the tree is rebuilt + protected virtual void RebuildAll(IList selected, IEnumerable expanded, IList checkedObjects) { + // Remember the bits of info we don't want to forget (anyone ever see Memento?) + IEnumerable roots = this.Roots; + CanExpandGetterDelegate canExpand = this.CanExpandGetter; + ChildrenGetterDelegate childrenGetter = this.ChildrenGetter; + + try { + this.BeginUpdate(); + + // Give ourselves a new data structure + this.RegenerateTree(); + + // Put back the bits we didn't want to forget + this.CanExpandGetter = canExpand; + this.ChildrenGetter = childrenGetter; + if (expanded != null) + this.ExpandedObjects = expanded; + this.Roots = roots; + if (selected != null) + this.SelectedObjects = selected; + if (checkedObjects != null) + this.CheckedObjects = checkedObjects; + } + finally { + this.EndUpdate(); + } + } + + /// + /// Unroll all the ancestors of the given model and make sure it is then visible. + /// + /// This works best when a ParentGetter is installed. + /// The object to be revealed + /// If true, the model will be selected and focused after being revealed + /// True if the object was found and revealed. False if it was not found. + public virtual void Reveal(object modelToReveal, bool selectAfterReveal) { + // Collect all the ancestors of the model + ArrayList ancestors = new ArrayList(); + foreach (object ancestor in this.GetAncestors(modelToReveal)) + ancestors.Add(ancestor); + + // Arrange them from root down to the model's immediate parent + ancestors.Reverse(); + try { + this.BeginUpdate(); + foreach (object ancestor in ancestors) + this.Expand(ancestor); + this.EnsureModelVisible(modelToReveal); + if (selectAfterReveal) + this.SelectObject(modelToReveal, true); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Update the rows that are showing the given objects + /// + public override void RefreshObjects(IList modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker) delegate { this.RefreshObjects(modelObjects); }); + return; + } + // There is no point in refreshing anything if the list is empty + if (this.GetItemCount() == 0) + return; + + // Remember the selection so we can put it back later + IList selection = this.SelectedObjects; + + // We actually need to refresh the parents. + // Refreshes on root objects have to be handled differently + ArrayList updatedRoots = new ArrayList(); + Hashtable modelsAndParents = new Hashtable(); + foreach (Object model in modelObjects) { + if (model == null) + continue; + modelsAndParents[model] = true; + object parent = GetParent(model); + if (parent == null) { + updatedRoots.Add(model); + } else { + modelsAndParents[parent] = true; + } + } + + // Update any changed roots + if (updatedRoots.Count > 0) { + ArrayList newRoots = ObjectListView.EnumerableToArray(this.Roots, false); + bool changed = false; + foreach (Object model in updatedRoots) { + int index = newRoots.IndexOf(model); + if (index >= 0 && !ReferenceEquals(newRoots[index], model)) { + newRoots[index] = model; + changed = true; + } + } + if (changed) + this.Roots = newRoots; + } + + // Refresh each object, remembering where the first update occurred + int firstChange = Int32.MaxValue; + foreach (Object model in modelsAndParents.Keys) { + if (model != null) { + int index = this.TreeModel.RebuildChildren(model); + if (index >= 0) + firstChange = Math.Min(firstChange, index); + } + } + + // If we didn't refresh any objects, don't do anything else + if (firstChange >= this.GetItemCount()) + return; + + this.ClearCachedInfo(); + this.UpdateVirtualListSize(); + this.SelectedObjects = selection; + + // Redraw everything from the first update to the end of the list + this.RedrawItems(firstChange, this.GetItemCount() - 1, true); + } + + /// + /// Change the check state of the given object to be the given state. + /// + /// + /// If the given model object isn't in the list, we still try to remember + /// its state, in case it is referenced in the future. + /// + /// + /// True if the checkedness of the model changed + protected override bool SetObjectCheckedness(object modelObject, CheckState state) { + // If the checkedness of the given model changes AND this tree has + // hierarchical checkboxes, then we need to update the checkedness of + // its children, and recalculate the checkedness of the parent (recursively) + if (!base.SetObjectCheckedness(modelObject, state)) + return false; + + if (!this.HierarchicalCheckboxes) + return true; + + // Give each child the same checkedness as the model + + CheckState? checkedness = this.GetCheckState(modelObject); + if (!checkedness.HasValue || checkedness.Value == CheckState.Indeterminate) + return true; + + foreach (object child in this.GetChildrenWithoutExpanding(modelObject)) { + this.SetObjectCheckedness(child, checkedness.Value); + } + + ArrayList args = new ArrayList(); + args.Add(modelObject); + this.RecalculateHierarchicalCheckBoxGraph(args); + + return true; + } + + + private IEnumerable GetChildrenWithoutExpanding(Object model) { + Branch br = this.TreeModel.GetBranch(model); + if (br == null || !br.CanExpand) + return new ArrayList(); + + return br.Children; + } + + /// + /// Toggle the expanded state of the branch at the given model object + /// + /// + public virtual void ToggleExpansion(Object model) { + if (this.IsExpanded(model)) + this.Collapse(model); + else + this.Expand(model); + } + + //------------------------------------------------------------------------------------------ + // Commands - Tree traversal + + /// + /// Return whether or not the given model can expand. + /// + /// + /// The given model must have already been seen in the tree + public virtual bool CanExpand(Object model) { + Branch br = this.TreeModel.GetBranch(model); + return (br != null && br.CanExpand); + } + + /// + /// Return the model object that is the parent of the given model object. + /// + /// + /// + /// The given model must have already been seen in the tree. + public virtual Object GetParent(Object model) { + Branch br = this.TreeModel.GetBranch(model); + return br == null || br.ParentBranch == null ? null : br.ParentBranch.Model; + } + + /// + /// Return the collection of model objects that are the children of the + /// given model as they exist in the tree at the moment. + /// + /// + /// + /// + /// This method returns the collection of children as the tree knows them. If the given + /// model has never been presented to the user (e.g. it belongs to a parent that has + /// never been expanded), then this method will return an empty collection. + /// + /// Because of this, if you want to traverse the whole tree, this is not the method to use. + /// It's better to traverse the your data model directly. + /// + /// + /// If the given model has not already been seen in the tree or + /// if it is not expandable, an empty collection will be returned. + /// + /// + public virtual IEnumerable GetChildren(Object model) { + Branch br = this.TreeModel.GetBranch(model); + if (br == null || !br.CanExpand) + return new ArrayList(); + + br.FetchChildren(); + + return br.Children; + } + + //------------------------------------------------------------------------------------------ + // Delegates + + /// + /// Delegates of this type are use to decide if the given model object can be expanded + /// + /// The model under consideration + /// Can the given model be expanded? + public delegate bool CanExpandGetterDelegate(Object model); + + /// + /// Delegates of this type are used to fetch the children of the given model object + /// + /// The parent whose children should be fetched + /// An enumerable over the children + public delegate IEnumerable ChildrenGetterDelegate(Object model); + + /// + /// Delegates of this type are used to fetch the parent of the given model object. + /// + /// The child whose parent should be fetched + /// The parent of the child or null if the child is a root + public delegate Object ParentGetterDelegate(Object model); + + /// + /// Delegates of this type are used to create a new underlying Tree structure. + /// + /// The view for which the Tree is being created + /// A subclass of Tree + public delegate Tree TreeFactoryDelegate(TreeListView view); + + //------------------------------------------------------------------------------------------ + #region Implementation + + /// + /// Handle a left button down event + /// + /// + /// + protected override bool ProcessLButtonDown(OlvListViewHitTestInfo hti) { + // Did they click in the expander? + if (hti.HitTestLocation == HitTestLocation.ExpandButton) { + this.PossibleFinishCellEditing(); + this.ToggleExpansion(hti.RowObject); + return true; + } + + return base.ProcessLButtonDown(hti); + } + + /// + /// Create a OLVListItem for given row index + /// + /// The index of the row that is needed + /// An OLVListItem + /// This differs from the base method by also setting up the IndentCount property. + public override OLVListItem MakeListViewItem(int itemIndex) { + OLVListItem olvItem = base.MakeListViewItem(itemIndex); + Branch br = this.TreeModel.GetBranch(olvItem.RowObject); + if (br != null) + olvItem.IndentCount = br.Level; + return olvItem; + } + + /// + /// Reinitialise the Tree structure + /// + protected virtual void RegenerateTree() { + this.TreeModel = this.TreeFactory == null ? new Tree(this) : this.TreeFactory(this); + Trace.Assert(this.TreeModel != null); + this.VirtualListDataSource = this.TreeModel; + } + + /// + /// Recalculate the state of the checkboxes of all the items in the given list + /// and their ancestors. + /// + /// This only makes sense when HierarchicalCheckboxes is true. + /// + protected virtual void RecalculateHierarchicalCheckBoxGraph(IList toCheck) { + if (toCheck == null || toCheck.Count == 0) + return; + + // Avoid recursive calculations + if (isRecalculatingHierarchicalCheckBox) + return; + + try { + isRecalculatingHierarchicalCheckBox = true; + foreach (object ancestor in CalculateDistinctAncestors(toCheck)) + this.RecalculateSingleHierarchicalCheckBox(ancestor); + } + finally { + isRecalculatingHierarchicalCheckBox = false; + } + + } + private bool isRecalculatingHierarchicalCheckBox; + + /// + /// Recalculate the hierarchy state of the given item and its ancestors + /// + /// This only makes sense when HierarchicalCheckboxes is true. + /// + protected virtual void RecalculateSingleHierarchicalCheckBox(object modelObject) { + + if (modelObject == null) + return; + + // Only branches have calculated check states. Leaf node checkedness is not calculated + if (!this.CanExpandUncached(modelObject)) + return; + + // Set the checkedness of the given model based on the state of its children. + CheckState? aggregate = null; + foreach (object child in this.GetChildrenUncached(modelObject)) { + CheckState? checkedness = this.GetCheckState(child); + if (!checkedness.HasValue) + continue; + + if (aggregate.HasValue) { + if (aggregate.Value != checkedness.Value) { + aggregate = CheckState.Indeterminate; + break; + } + } else + aggregate = checkedness; + } + + base.SetObjectCheckedness(modelObject, aggregate ?? CheckState.Indeterminate); + } + + private bool CanExpandUncached(object model) { + return this.CanExpandGetter != null && model != null && this.CanExpandGetter(model); + } + + private IEnumerable GetChildrenUncached(object model) { + return this.ChildrenGetter != null && model != null ? this.ChildrenGetter(model) : new ArrayList(); + } + + /// + /// Yield the unique ancestors of the given collection of objects. + /// The order of the ancestors is guaranteed to be deeper objects first. + /// Roots will always be last. + /// + /// + /// Unique ancestors of the given objects + protected virtual IEnumerable CalculateDistinctAncestors(IList toCheck) { + + if (toCheck.Count == 1) { + foreach (object ancestor in this.GetAncestors(toCheck[0])) { + yield return ancestor; + } + } else { + // WARNING - Clever code + + // Example: Root --> GP +--> P +--> A + // | +--> B + // | + // +--> Q +--> X + // +--> Y + // + // Calculate ancestors of A, B, X and Y + + // Build a list of all ancestors of all objects we need to check + ArrayList allAncestors = new ArrayList(); + foreach (object child in toCheck) { + foreach (object ancestor in this.GetAncestors(child)) { + allAncestors.Add(ancestor); + } + } + + // allAncestors = { P, GP, Root, P, GP, Root, Q, GP, Root, Q, GP, Root } + + // Reverse them so "higher" ancestors come first + allAncestors.Reverse(); + + // allAncestors = { Root, GP, Q, Root, GP, Q, Root, GP, P, Root, GP, P } + + ArrayList uniqueAncestors = new ArrayList(); + Dictionary alreadySeen = new Dictionary(); + foreach (object ancestor in allAncestors) { + if (!alreadySeen.ContainsKey(ancestor)) { + alreadySeen[ancestor] = true; + uniqueAncestors.Add(ancestor); + } + } + + // uniqueAncestors = { Root, GP, Q, P } + + uniqueAncestors.Reverse(); + foreach (object x in uniqueAncestors) + yield return x; + } + } + + /// + /// Return all the ancestors of the given model + /// + /// + /// + /// This uses ParentGetter if possible. + /// + /// If the given model is a root OR if the model doesn't exist, the collection will be empty + /// + /// The model whose ancestors should be calculated + /// Return a collection of ancestors of the given model. + protected virtual IEnumerable GetAncestors(object model) { + ParentGetterDelegate parentGetterDelegate = this.ParentGetter ?? this.GetParent; + + object parent = parentGetterDelegate(model); + while (parent != null) { + yield return parent; + parent = parentGetterDelegate(parent); + } + } + + #endregion + + //------------------------------------------------------------------------------------------ + #region Event handlers + + /// + /// The application is idle and a SelectionChanged event has been scheduled + /// + /// + /// + protected override void HandleApplicationIdle(object sender, EventArgs e) { + base.HandleApplicationIdle(sender, e); + + // There is an annoying redraw issue on ListViews that use indentation and + // that have full row select enabled. When the selection reduces to a subset + // of previously selected rows, or when the selection is extended using + // shift-pageup/down, then the space occupied by the indentation is not + // invalidated, and hence remains highlighted. + // Ideally we'd want to know exactly which rows were selected or deselected + // and then invalidate just the indentation region of those rows, + // but that's too much work. So just redraw the control. + // Actually... the selection issues show just slightly for non-full row select + // controls as well. So, always redraw the control after the selection + // changes. + this.Invalidate(); + } + + /// + /// Decide if the given key event should be handled as a normal key input to the control? + /// + /// + /// + protected override bool IsInputKey(Keys keyData) { + // We want to handle Left and Right keys within the control + Keys key = keyData & Keys.KeyCode; + if (key == Keys.Left || key == Keys.Right) + return true; + + return base.IsInputKey(keyData); + } + + /// + /// Handle focus being lost, including making sure that the whole control is redrawn. + /// + /// + protected override void OnLostFocus(EventArgs e) + { + // When this focus is lost, the normal invalidation logic doesn't invalid the region + // of the control created by the IndentLevel on each row. This makes the control + // look wrong when HideSelection is false, since part of the selected row's background + // correctly changes colour to the "inactive" colour, but the left part of the row + // created by IndentLevel doesn't change colour. + // SF #135. + + this.Invalidate(); + } + + /// + /// Handle the keyboard input to mimic a TreeView. + /// + /// + /// Was the key press handled? + protected override void OnKeyDown(KeyEventArgs e) { + OLVListItem focused = this.FocusedItem as OLVListItem; + if (focused == null) { + base.OnKeyDown(e); + return; + } + + Object modelObject = focused.RowObject; + Branch br = this.TreeModel.GetBranch(modelObject); + + switch (e.KeyCode) { + case Keys.Left: + // If the branch is expanded, collapse it. If it's collapsed, + // select the parent of the branch. + if (br.IsExpanded) + this.Collapse(modelObject); + else { + if (br.ParentBranch != null && br.ParentBranch.Model != null) + this.SelectObject(br.ParentBranch.Model, true); + } + e.Handled = true; + break; + + case Keys.Right: + // If the branch is expanded, select the first child. + // If it isn't expanded and can be, expand it. + if (br.IsExpanded) { + List filtered = br.FilteredChildBranches; + if (filtered.Count > 0) + this.SelectObject(filtered[0].Model, true); + } else { + if (br.CanExpand) + this.Expand(modelObject); + } + e.Handled = true; + break; + } + + base.OnKeyDown(e); + } + + #endregion + + //------------------------------------------------------------------------------------------ + // Support classes + + /// + /// A Tree object represents a tree structure data model that supports both + /// tree and flat list operations as well as fast access to branches. + /// + /// If you create a subclass of Tree, you must install it in the TreeListView + /// via the TreeFactory delegate. + public class Tree : IVirtualListDataSource, IFilterableDataSource + { + /// + /// Create a Tree + /// + /// + public Tree(TreeListView treeView) { + this.treeView = treeView; + this.trunk = new Branch(null, this, null); + this.trunk.IsExpanded = true; + } + + //------------------------------------------------------------------------------------------ + // Properties + + /// + /// This is the delegate that will be used to decide if a model object can be expanded. + /// + public CanExpandGetterDelegate CanExpandGetter { + get { return canExpandGetter ?? DefaultCanExpandGetter; } + set { canExpandGetter = value; } + } + private CanExpandGetterDelegate canExpandGetter; + + + /// + /// This is the delegate that will be used to fetch the children of a model object + /// + /// This delegate will only be called if the CanExpand delegate has + /// returned true for the model object. + public ChildrenGetterDelegate ChildrenGetter { + get { return childrenGetter ?? DefaultChildrenGetter; } + set { childrenGetter = value; } + } + private ChildrenGetterDelegate childrenGetter; + + /// + /// Get or return the top level model objects in the tree + /// + public IEnumerable RootObjects { + get { return this.trunk.Children; } + set { + this.trunk.Children = value; + foreach (Branch br in this.trunk.ChildBranches) + br.RefreshChildren(); + this.RebuildList(); + } + } + + /// + /// What tree view is this Tree the model for? + /// + public TreeListView TreeView { + get { return this.treeView; } + } + + //------------------------------------------------------------------------------------------ + // Commands + + /// + /// Collapse the subtree underneath the given model + /// + /// The model to be collapsed. If the model isn't in the tree, + /// or if it is already collapsed, the command does nothing. + /// The index of the model in flat list version of the tree + public virtual int Collapse(Object model) { + Branch br = this.GetBranch(model); + if (br == null || !br.IsExpanded) + return -1; + + // Remember that the branch is collapsed, even if it's currently not visible + if (!br.Visible) { + br.Collapse(); + return -1; + } + + int count = br.NumberVisibleDescendents; + br.Collapse(); + + // Remove the visible descendants from after the branch itself + int index = this.GetObjectIndex(model); + this.objectList.RemoveRange(index + 1, count); + this.RebuildObjectMap(0); + return index; + } + + /// + /// Collapse all branches in this tree + /// + /// Nothing useful + public virtual int CollapseAll() { + this.trunk.CollapseAll(); + this.RebuildList(); + return 0; + } + + /// + /// Expand the subtree underneath the given model object + /// + /// The model to be expanded. + /// The index of the model in flat list version of the tree + /// + /// If the model isn't in the tree, + /// if it cannot be expanded or if it is already expanded, the command does nothing. + /// + public virtual int Expand(Object model) { + Branch br = this.GetBranch(model); + if (br == null || !br.CanExpand || br.IsExpanded) + return -1; + + // Remember that the branch is expanded, even if it's currently not visible + br.Expand(); + if (!br.Visible) + { + return -1; + } + + int index = this.GetObjectIndex(model); + this.InsertChildren(br, index + 1); + return index; + } + + /// + /// Expand all branches in this tree + /// + /// Return the index of the first branch that was expanded + public virtual int ExpandAll() { + this.trunk.ExpandAll(); + this.Sort(this.lastSortColumn, this.lastSortOrder); + return 0; + } + + /// + /// Return the Branch object that represents the given model in the tree + /// + /// The model whose branches is to be returned + /// The branch that represents the given model, or null if the model + /// isn't in the tree. + public virtual Branch GetBranch(object model) { + if (model == null) + return null; + + Branch br; + this.mapObjectToBranch.TryGetValue(model, out br); + return br; + } + + /// + /// Return the number of visible descendants that are below the given model. + /// + /// The model whose descendent count is to be returned + /// The number of visible descendants. 0 if the model doesn't exist or is collapsed + public virtual int GetVisibleDescendentCount(object model) + { + Branch br = this.GetBranch(model); + return br == null || !br.IsExpanded ? 0 : br.NumberVisibleDescendents; + } + + /// + /// Rebuild the children of the given model, refreshing any cached information held about the given object + /// + /// + /// The index of the model in flat list version of the tree + public virtual int RebuildChildren(Object model) { + Branch br = this.GetBranch(model); + if (br == null || !br.Visible) + return -1; + + int count = br.NumberVisibleDescendents; + + // Remove the visible descendants from after the branch itself + int index = this.GetObjectIndex(model); + if (count > 0) + this.objectList.RemoveRange(index + 1, count); + + // Refresh our knowledge of our children (do this even if CanExpand is false, because + // the branch have already collected some children and that information could be stale) + br.RefreshChildren(); + + // Insert the refreshed children if the branch can expand and is expanded + if (br.CanExpand && br.IsExpanded) + this.InsertChildren(br, index + 1); + else + this.RebuildObjectMap(index); + + return index; + } + + //------------------------------------------------------------------------------------------ + // Implementation + + private static bool DefaultCanExpandGetter(object model) { + ITreeModelWithChildren treeModel = model as ITreeModelWithChildren; + return treeModel != null && treeModel.TreeCanExpand; + } + + private static IEnumerable DefaultChildrenGetter(object model) { + ITreeModelWithChildren treeModel = model as ITreeModelWithChildren; + return treeModel == null ? new ArrayList() : treeModel.TreeChildren; + } + + internal static object DefaultParentGetter(object model) { + ITreeModelWithParent treeModel = model as ITreeModelWithParent; + return treeModel == null ? null : treeModel.TreeParent; + } + + /// + /// Is the given model expanded? + /// + /// + /// + internal bool IsModelExpanded(object model) { + // Special case: model == null is the container for the roots. This is always expanded + if (model == null) + return true; + bool isExpanded; + this.mapObjectToExpanded.TryGetValue(model, out isExpanded); + return isExpanded; + } + + /// + /// Remember whether or not the given model was expanded + /// + /// + /// + internal void SetModelExpanded(object model, bool isExpanded) { + if (model == null) return; + + if (isExpanded) + this.mapObjectToExpanded[model] = true; + else + this.mapObjectToExpanded.Remove(model); + } + + /// + /// Insert the children of the given branch into the given position + /// + /// The branch whose children should be inserted + /// The index where the children should be inserted + protected virtual void InsertChildren(Branch br, int index) { + // Expand the branch + br.Expand(); + br.Sort(this.GetBranchComparer()); + + // Insert the branch's visible descendants after the branch itself + this.objectList.InsertRange(index, br.Flatten()); + this.RebuildObjectMap(index); + } + + /// + /// Rebuild our flat internal list of objects. + /// + protected virtual void RebuildList() { + this.objectList = ArrayList.Adapter(this.trunk.Flatten()); + List filtered = this.trunk.FilteredChildBranches; + if (filtered.Count > 0) { + filtered[0].IsFirstBranch = true; + filtered[0].IsOnlyBranch = (filtered.Count == 1); + } + this.RebuildObjectMap(0); + } + + /// + /// Rebuild our reverse index that maps an object to its location + /// in the filteredObjectList array. + /// + /// + protected virtual void RebuildObjectMap(int startIndex) { + if (startIndex == 0) + this.mapObjectToIndex.Clear(); + for (int i = startIndex; i < this.objectList.Count; i++) + this.mapObjectToIndex[this.objectList[i]] = i; + } + + /// + /// Create a new branch within this tree + /// + /// + /// + /// + internal Branch MakeBranch(Branch parent, object model) { + Branch br = new Branch(parent, this, model); + + // Remember that the given branch is part of this tree. + this.mapObjectToBranch[model] = br; + return br; + } + + //------------------------------------------------------------------------------------------ + + #region IVirtualListDataSource Members + + /// + /// + /// + /// + /// + public virtual object GetNthObject(int n) { + if (n >= 0 && n < this.objectList.Count) + return this.objectList[n]; + return null; + } + + /// + /// + /// + /// + public virtual int GetObjectCount() { + return this.trunk.NumberVisibleDescendents; + } + + /// + /// + /// + /// + /// + public virtual int GetObjectIndex(object model) + { + int index; + if (model != null && this.mapObjectToIndex.TryGetValue(model, out index)) + return index; + + return -1; + } + + /// + /// + /// + /// + /// + public virtual void PrepareCache(int first, int last) { + } + + /// + /// + /// + /// + /// + /// + /// + /// + public virtual int SearchText(string value, int first, int last, OLVColumn column) { + return AbstractVirtualListDataSource.DefaultSearchText(value, first, last, column, this); + } + + /// + /// Sort the tree on the given column and in the given order + /// + /// + /// + public virtual void Sort(OLVColumn column, SortOrder order) { + this.lastSortColumn = column; + this.lastSortOrder = order; + + // TODO: Need to raise an AboutToSortEvent here + + // Sorting is going to change the order of the branches so clear + // the "first branch" flag + foreach (Branch b in this.trunk.ChildBranches) + b.IsFirstBranch = false; + + this.trunk.Sort(this.GetBranchComparer()); + this.RebuildList(); + } + + /// + /// + /// + /// + protected virtual BranchComparer GetBranchComparer() { + if (this.lastSortColumn == null) + return null; + + return new BranchComparer(new ModelObjectComparer( + this.lastSortColumn, + this.lastSortOrder, + this.treeView.SecondarySortColumn ?? this.treeView.GetColumn(0), + this.treeView.SecondarySortColumn == null ? this.lastSortOrder : this.treeView.SecondarySortOrder)); + } + + /// + /// Add the given collection of objects to the roots of this tree + /// + /// + public virtual void AddObjects(ICollection modelObjects) { + ArrayList newRoots = ObjectListView.EnumerableToArray(this.treeView.Roots, true); + foreach (Object x in modelObjects) + newRoots.Add(x); + this.SetObjects(newRoots); + } + + /// + /// + /// + /// + /// + public void InsertObjects(int index, ICollection modelObjects) { + throw new NotImplementedException(); + } + + /// + /// Remove all of the given objects from the roots of the tree. + /// Any objects that is not already in the roots collection is ignored. + /// + /// + public virtual void RemoveObjects(ICollection modelObjects) { + ArrayList newRoots = new ArrayList(); + foreach (Object x in this.treeView.Roots) + newRoots.Add(x); + foreach (Object x in modelObjects) { + newRoots.Remove(x); + this.mapObjectToIndex.Remove(x); + } + this.SetObjects(newRoots); + } + + /// + /// Set the roots of this tree to be the given collection + /// + /// + public virtual void SetObjects(IEnumerable collection) { + // We interpret a SetObjects() call as setting the roots of the tree + this.treeView.Roots = collection; + } + + /// + /// Update/replace the nth object with the given object + /// + /// + /// + public void UpdateObject(int index, object modelObject) { + ArrayList newRoots = ObjectListView.EnumerableToArray(this.treeView.Roots, false); + if (index < newRoots.Count) + newRoots[index] = modelObject; + SetObjects(newRoots); + } + + #endregion + + #region IFilterableDataSource Members + + /// + /// + /// + /// + /// + public void ApplyFilters(IModelFilter mFilter, IListFilter lFilter) { + this.modelFilter = mFilter; + this.listFilter = lFilter; + this.RebuildList(); + } + + /// + /// Is this list currently being filtered? + /// + internal bool IsFiltering { + get { + return this.treeView.UseFiltering && (this.modelFilter != null || this.listFilter != null); + } + } + + /// + /// Should the given model be included in this control? + /// + /// The model to consider + /// True if it will be included + internal bool IncludeModel(object model) { + if (!this.treeView.UseFiltering) + return true; + + if (this.modelFilter == null) + return true; + + return this.modelFilter.Filter(model); + } + + #endregion + + //------------------------------------------------------------------------------------------ + // Private instance variables + + private OLVColumn lastSortColumn; + private SortOrder lastSortOrder; + private readonly Dictionary mapObjectToBranch = new Dictionary(); +// ReSharper disable once InconsistentNaming + internal Dictionary mapObjectToExpanded = new Dictionary(); + private readonly Dictionary mapObjectToIndex = new Dictionary(); + private ArrayList objectList = new ArrayList(); + private readonly TreeListView treeView; + private readonly Branch trunk; + + /// + /// + /// +// ReSharper disable once InconsistentNaming + protected IModelFilter modelFilter; + /// + /// + /// +// ReSharper disable once InconsistentNaming + protected IListFilter listFilter; + } + + /// + /// A Branch represents a sub-tree within a tree + /// + public class Branch + { + /// + /// Indicators for branches + /// + [Flags] + public enum BranchFlags + { + /// + /// FirstBranch of tree + /// + FirstBranch = 1, + + /// + /// LastChild of parent + /// + LastChild = 2, + + /// + /// OnlyBranch of tree + /// + OnlyBranch = 4 + } + + #region Life and death + + /// + /// Create a Branch + /// + /// + /// + /// + public Branch(Branch parent, Tree tree, Object model) { + this.ParentBranch = parent; + this.Tree = tree; + this.Model = model; + } + + #endregion + + #region Public properties + + //------------------------------------------------------------------------------------------ + // Properties + + /// + /// Get the ancestor branches of this branch, with the 'oldest' ancestor first. + /// + public virtual IList Ancestors { + get { + List ancestors = new List(); + if (this.ParentBranch != null) + this.ParentBranch.PushAncestors(ancestors); + return ancestors; + } + } + + private void PushAncestors(IList list) { + // This is designed to ignore the trunk (which has no parent) + if (this.ParentBranch != null) { + this.ParentBranch.PushAncestors(list); + list.Add(this); + } + } + + /// + /// Can this branch be expanded? + /// + public virtual bool CanExpand { + get { + if (this.Tree.CanExpandGetter == null || this.Model == null) + return false; + + return this.Tree.CanExpandGetter(this.Model); + } + } + + /// + /// Gets or sets our children + /// + public List ChildBranches { + get { return this.childBranches; } + set { this.childBranches = value; } + } + private List childBranches = new List(); + + /// + /// Get/set the model objects that are beneath this branch + /// + public virtual IEnumerable Children { + get { + ArrayList children = new ArrayList(); + foreach (Branch x in this.ChildBranches) + children.Add(x.Model); + return children; + } + set { + this.ChildBranches.Clear(); + + TreeListView treeListView = this.Tree.TreeView; + CheckState? checkedness = null; + if (treeListView != null && treeListView.HierarchicalCheckboxes) + checkedness = treeListView.GetCheckState(this.Model); + foreach (Object x in value) { + this.AddChild(x); + + // If the tree view is showing hierarchical checkboxes, then + // when a child object is first added, it has the same checkedness as this branch + if (checkedness.HasValue && checkedness.Value == CheckState.Checked) + treeListView.SetObjectCheckedness(x, checkedness.Value); + } + } + } + + private void AddChild(object childModel) { + Branch br = this.Tree.GetBranch(childModel); + if (br == null) + br = this.Tree.MakeBranch(this, childModel); + else { + br.ParentBranch = this; + br.Model = childModel; + br.ClearCachedInfo(); + } + this.ChildBranches.Add(br); + } + + /// + /// Gets a list of all the branches that survive filtering + /// + public List FilteredChildBranches { + get { + if (!this.IsExpanded) + return new List(); + + if (!this.Tree.IsFiltering) + return this.ChildBranches; + + List filtered = new List(); + foreach (Branch b in this.ChildBranches) { + if (this.Tree.IncludeModel(b.Model)) + filtered.Add(b); + else { + // Also include this branch if it has any filtered branches (yes, its recursive) + if (b.FilteredChildBranches.Count > 0) + filtered.Add(b); + } + } + return filtered; + } + } + + /// + /// Gets or set whether this branch is expanded + /// + public bool IsExpanded { + get { return this.Tree.IsModelExpanded(this.Model); } + set { this.Tree.SetModelExpanded(this.Model, value); } + } + + /// + /// Return true if this branch is the first branch of the entire tree + /// + public virtual bool IsFirstBranch { + get { + return ((this.flags & Branch.BranchFlags.FirstBranch) != 0); + } + set { + if (value) + this.flags |= Branch.BranchFlags.FirstBranch; + else + this.flags &= ~Branch.BranchFlags.FirstBranch; + } + } + + /// + /// Return true if this branch is the last child of its parent + /// + public virtual bool IsLastChild { + get { + return ((this.flags & Branch.BranchFlags.LastChild) != 0); + } + set { + if (value) + this.flags |= Branch.BranchFlags.LastChild; + else + this.flags &= ~Branch.BranchFlags.LastChild; + } + } + + /// + /// Return true if this branch is the only top level branch + /// + public virtual bool IsOnlyBranch { + get { + return ((this.flags & Branch.BranchFlags.OnlyBranch) != 0); + } + set { + if (value) + this.flags |= Branch.BranchFlags.OnlyBranch; + else + this.flags &= ~Branch.BranchFlags.OnlyBranch; + } + } + + /// + /// Gets the depth level of this branch + /// + public int Level { + get { + if (this.ParentBranch == null) + return 0; + + return this.ParentBranch.Level + 1; + } + } + + /// + /// Gets or sets which model is represented by this branch + /// + public Object Model { + get { return model; } + set { model = value; } + } + private Object model; + + /// + /// Return the number of descendants of this branch that are currently visible + /// + /// + public virtual int NumberVisibleDescendents { + get { + if (!this.IsExpanded) + return 0; + + List filtered = this.FilteredChildBranches; + int count = filtered.Count; + foreach (Branch br in filtered) + count += br.NumberVisibleDescendents; + return count; + } + } + + /// + /// Gets or sets our parent branch + /// + public Branch ParentBranch { + get { return parentBranch; } + set { parentBranch = value; } + } + private Branch parentBranch; + + /// + /// Gets or sets our overall tree + /// + public Tree Tree { + get { return tree; } + set { tree = value; } + } + private Tree tree; + + /// + /// Is this branch currently visible? A branch is visible + /// if it has no parent (i.e. it's a root), or its parent + /// is visible and expanded. + /// + public virtual bool Visible { + get { + if (this.ParentBranch == null) + return true; + + return this.ParentBranch.IsExpanded && this.ParentBranch.Visible; + } + } + + #endregion + + #region Commands + + //------------------------------------------------------------------------------------------ + // Commands + + /// + /// Clear any cached information that this branch is holding + /// + public virtual void ClearCachedInfo() { + this.Children = new ArrayList(); + this.alreadyHasChildren = false; + } + + /// + /// Collapse this branch + /// + public virtual void Collapse() { + this.IsExpanded = false; + } + + /// + /// Expand this branch + /// + public virtual void Expand() { + if (this.CanExpand) { + this.IsExpanded = true; + this.FetchChildren(); + } + } + + /// + /// Expand this branch recursively + /// + public virtual void ExpandAll() { + this.Expand(); + foreach (Branch br in this.ChildBranches) { + if (br.CanExpand) + br.ExpandAll(); + } + } + + /// + /// Collapse all branches in this tree + /// + /// Nothing useful + public virtual void CollapseAll() + { + this.Collapse(); + foreach (Branch br in this.ChildBranches) { + if (br.IsExpanded) + br.CollapseAll(); + } + } + + /// + /// Fetch the children of this branch. + /// + /// This should only be called when CanExpand is true. + public virtual void FetchChildren() { + if (this.alreadyHasChildren) + return; + + this.alreadyHasChildren = true; + + if (this.Tree.ChildrenGetter == null) + return; + + Cursor previous = Cursor.Current; + try { + if (this.Tree.TreeView.UseWaitCursorWhenExpanding) + Cursor.Current = Cursors.WaitCursor; + this.Children = this.Tree.ChildrenGetter(this.Model); + } + finally { + Cursor.Current = previous; + } + } + + /// + /// Collapse the visible descendants of this branch into list of model objects + /// + /// + public virtual IList Flatten() { + ArrayList flatList = new ArrayList(); + if (this.IsExpanded) + this.FlattenOnto(flatList); + return flatList; + } + + /// + /// Flatten this branch's visible descendants onto the given list. + /// + /// + /// The branch itself is not included in the list. + public virtual void FlattenOnto(IList flatList) { + Branch lastBranch = null; + foreach (Branch br in this.FilteredChildBranches) { + lastBranch = br; + br.IsFirstBranch = br.IsOnlyBranch = br.IsLastChild = false; + flatList.Add(br.Model); + if (br.IsExpanded) { + br.FetchChildren(); // make sure we have the branches children + br.FlattenOnto(flatList); + } + } + if (lastBranch != null) + lastBranch.IsLastChild = true; + } + + /// + /// Force a refresh of all children recursively + /// + public virtual void RefreshChildren() { + + // Forget any previous children. We always do this so that if + // IsExpanded or CanExpand have changed, we aren't left with stale information. + this.ClearCachedInfo(); + + if (!this.IsExpanded || !this.CanExpand) + return; + + this.FetchChildren(); + foreach (Branch br in this.ChildBranches) + br.RefreshChildren(); + } + + /// + /// Sort the sub-branches and their descendants so they are ordered according + /// to the given comparer. + /// + /// The comparer that orders the branches + public virtual void Sort(BranchComparer comparer) { + if (this.ChildBranches.Count == 0) + return; + + if (comparer != null) + this.ChildBranches.Sort(comparer); + + foreach (Branch br in this.ChildBranches) + br.Sort(comparer); + } + + #endregion + + + //------------------------------------------------------------------------------------------ + // Private instance variables + + private bool alreadyHasChildren; + private BranchFlags flags; + } + + /// + /// This class sorts branches according to how their respective model objects are sorted + /// + public class BranchComparer : IComparer + { + /// + /// Create a BranchComparer + /// + /// + public BranchComparer(IComparer actualComparer) { + this.actualComparer = actualComparer; + } + + /// + /// Order the two branches + /// + /// + /// + /// + public int Compare(Branch x, Branch y) { + return this.actualComparer.Compare(x.Model, y.Model); + } + + private readonly IComparer actualComparer; + } + + } + + /// + /// This interface should be implemented by model objects that can provide children, + /// but that don't have a parent. This is either because the model objects are always + /// root level, or because they are used in TreeListView that never uses parent + /// calculations. Parent calculations are only used when HierarchicalCheckBoxes is true. + /// + public interface ITreeModelWithChildren { + /// + /// Get whether this this model can be expanded? If true, an expand glyph will be drawn next to it. + /// + /// This is called often! It must be fast. Don’t do a database lookup, calculate pi, or do linear searches – just return a property value. + bool TreeCanExpand { get; } + + /// + /// Get the models that will be shown under this model when it is expanded. + /// + /// This is only called when CanExpand returns true. + IEnumerable TreeChildren { get; } + } + + /// + /// This interface should be implemented by model objects that can never have children, + /// but that are used in a TreeListView that uses parent calculations. + /// Parent calculations are only used when HierarchicalCheckBoxes is true. + /// + public interface ITreeModelWithParent { + + /// + /// Get the hierarchical parent of this model. + /// + object TreeParent { get; } + } + + /// + /// ITreeModel allows model objects to provide the required information to TreeListView + /// without using the normal Getter delegates. + /// + public interface ITreeModel: ITreeModelWithChildren, ITreeModelWithParent { + + } +} diff --git a/ObjectListView/Utilities/ColumnSelectionForm.Designer.cs b/ObjectListView/Utilities/ColumnSelectionForm.Designer.cs new file mode 100644 index 00000000..e8e520e6 --- /dev/null +++ b/ObjectListView/Utilities/ColumnSelectionForm.Designer.cs @@ -0,0 +1,190 @@ +namespace BrightIdeasSoftware +{ + partial class ColumnSelectionForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.buttonMoveUp = new System.Windows.Forms.Button(); + this.buttonMoveDown = new System.Windows.Forms.Button(); + this.buttonShow = new System.Windows.Forms.Button(); + this.buttonHide = new System.Windows.Forms.Button(); + this.label1 = new System.Windows.Forms.Label(); + this.buttonOK = new System.Windows.Forms.Button(); + this.buttonCancel = new System.Windows.Forms.Button(); + this.objectListView1 = new BrightIdeasSoftware.ObjectListView(); + this.olvColumn1 = new BrightIdeasSoftware.OLVColumn(); + ((System.ComponentModel.ISupportInitialize)(this.objectListView1)).BeginInit(); + this.SuspendLayout(); + // + // buttonMoveUp + // + this.buttonMoveUp.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonMoveUp.Location = new System.Drawing.Point(295, 31); + this.buttonMoveUp.Name = "buttonMoveUp"; + this.buttonMoveUp.Size = new System.Drawing.Size(87, 23); + this.buttonMoveUp.TabIndex = 1; + this.buttonMoveUp.Text = "Move &Up"; + this.buttonMoveUp.UseVisualStyleBackColor = true; + this.buttonMoveUp.Click += new System.EventHandler(this.buttonMoveUp_Click); + // + // buttonMoveDown + // + this.buttonMoveDown.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonMoveDown.Location = new System.Drawing.Point(295, 60); + this.buttonMoveDown.Name = "buttonMoveDown"; + this.buttonMoveDown.Size = new System.Drawing.Size(87, 23); + this.buttonMoveDown.TabIndex = 2; + this.buttonMoveDown.Text = "Move &Down"; + this.buttonMoveDown.UseVisualStyleBackColor = true; + this.buttonMoveDown.Click += new System.EventHandler(this.buttonMoveDown_Click); + // + // buttonShow + // + this.buttonShow.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonShow.Location = new System.Drawing.Point(295, 89); + this.buttonShow.Name = "buttonShow"; + this.buttonShow.Size = new System.Drawing.Size(87, 23); + this.buttonShow.TabIndex = 3; + this.buttonShow.Text = "&Show"; + this.buttonShow.UseVisualStyleBackColor = true; + this.buttonShow.Click += new System.EventHandler(this.buttonShow_Click); + // + // buttonHide + // + this.buttonHide.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonHide.Location = new System.Drawing.Point(295, 118); + this.buttonHide.Name = "buttonHide"; + this.buttonHide.Size = new System.Drawing.Size(87, 23); + this.buttonHide.TabIndex = 4; + this.buttonHide.Text = "&Hide"; + this.buttonHide.UseVisualStyleBackColor = true; + this.buttonHide.Click += new System.EventHandler(this.buttonHide_Click); + // + // label1 + // + this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.label1.BackColor = System.Drawing.SystemColors.Control; + this.label1.Location = new System.Drawing.Point(13, 9); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(366, 19); + this.label1.TabIndex = 5; + this.label1.Text = "Choose the columns you want to see in this list. "; + // + // buttonOK + // + this.buttonOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.buttonOK.Location = new System.Drawing.Point(198, 304); + this.buttonOK.Name = "buttonOK"; + this.buttonOK.Size = new System.Drawing.Size(87, 23); + this.buttonOK.TabIndex = 6; + this.buttonOK.Text = "&OK"; + this.buttonOK.UseVisualStyleBackColor = true; + this.buttonOK.Click += new System.EventHandler(this.buttonOK_Click); + // + // buttonCancel + // + this.buttonCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.buttonCancel.Location = new System.Drawing.Point(295, 304); + this.buttonCancel.Name = "buttonCancel"; + this.buttonCancel.Size = new System.Drawing.Size(87, 23); + this.buttonCancel.TabIndex = 7; + this.buttonCancel.Text = "&Cancel"; + this.buttonCancel.UseVisualStyleBackColor = true; + this.buttonCancel.Click += new System.EventHandler(this.buttonCancel_Click); + // + // objectListView1 + // + this.objectListView1.AllColumns.Add(this.olvColumn1); + this.objectListView1.AlternateRowBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192))))); + this.objectListView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.objectListView1.CellEditActivation = BrightIdeasSoftware.ObjectListView.CellEditActivateMode.SingleClick; + this.objectListView1.CheckBoxes = true; + this.objectListView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.olvColumn1}); + this.objectListView1.FullRowSelect = true; + this.objectListView1.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None; + this.objectListView1.HideSelection = false; + this.objectListView1.Location = new System.Drawing.Point(12, 31); + this.objectListView1.MultiSelect = false; + this.objectListView1.Name = "objectListView1"; + this.objectListView1.ShowGroups = false; + this.objectListView1.ShowSortIndicators = false; + this.objectListView1.Size = new System.Drawing.Size(273, 259); + this.objectListView1.TabIndex = 0; + this.objectListView1.UseCompatibleStateImageBehavior = false; + this.objectListView1.View = System.Windows.Forms.View.Details; + this.objectListView1.SelectionChanged += new System.EventHandler(this.objectListView1_SelectionChanged); + // + // olvColumn1 + // + this.olvColumn1.AspectName = "Text"; + this.olvColumn1.IsVisible = true; + this.olvColumn1.Text = "Column"; + this.olvColumn1.Width = 267; + // + // ColumnSelectionForm + // + this.AcceptButton = this.buttonOK; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.buttonCancel; + this.ClientSize = new System.Drawing.Size(391, 339); + this.Controls.Add(this.buttonCancel); + this.Controls.Add(this.buttonOK); + this.Controls.Add(this.label1); + this.Controls.Add(this.buttonHide); + this.Controls.Add(this.buttonShow); + this.Controls.Add(this.buttonMoveDown); + this.Controls.Add(this.buttonMoveUp); + this.Controls.Add(this.objectListView1); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "ColumnSelectionForm"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.Text = "Column Selection"; + ((System.ComponentModel.ISupportInitialize)(this.objectListView1)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private BrightIdeasSoftware.ObjectListView objectListView1; + private System.Windows.Forms.Button buttonMoveUp; + private System.Windows.Forms.Button buttonMoveDown; + private System.Windows.Forms.Button buttonShow; + private System.Windows.Forms.Button buttonHide; + private BrightIdeasSoftware.OLVColumn olvColumn1; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Button buttonOK; + private System.Windows.Forms.Button buttonCancel; + } +} \ No newline at end of file diff --git a/ObjectListView/Utilities/ColumnSelectionForm.cs b/ObjectListView/Utilities/ColumnSelectionForm.cs new file mode 100644 index 00000000..6b8e7e0d --- /dev/null +++ b/ObjectListView/Utilities/ColumnSelectionForm.cs @@ -0,0 +1,263 @@ +/* + * ColumnSelectionForm - A utility form that allows columns to be rearranged and/or hidden + * + * Author: Phillip Piper + * Date: 1/04/2011 11:15 AM + * + * Change log: + * 2013-04-21 JPP - Fixed obscure bug in column re-ordered. Thanks to Edwin Chen. + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Text; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// This form is an example of how an application could allows the user to select which columns + /// an ObjectListView will display, as well as select which order the columns are displayed in. + /// + /// + /// In Tile view, ColumnHeader.DisplayIndex does nothing. To reorder the columns you have + /// to change the order of objects in the Columns property. + /// Remember that the first column is special! + /// It has to remain the first column. + /// + public partial class ColumnSelectionForm : Form + { + /// + /// Make a new ColumnSelectionForm + /// + public ColumnSelectionForm() + { + InitializeComponent(); + } + + /// + /// Open this form so it will edit the columns that are available in the listview's current view + /// + /// The ObjectListView whose columns are to be altered + public void OpenOn(ObjectListView olv) + { + this.OpenOn(olv, olv.View); + } + + /// + /// Open this form so it will edit the columns that are available in the given listview + /// when the listview is showing the given type of view. + /// + /// The ObjectListView whose columns are to be altered + /// The view that is to be altered. Must be View.Details or View.Tile + public void OpenOn(ObjectListView olv, View view) + { + if (view != View.Details && view != View.Tile) + return; + + this.InitializeForm(olv, view); + if (this.ShowDialog() == DialogResult.OK) + this.Apply(olv, view); + } + + /// + /// Initialize the form to show the columns of the given view + /// + /// + /// + protected void InitializeForm(ObjectListView olv, View view) + { + this.AllColumns = olv.AllColumns; + this.RearrangableColumns = new List(this.AllColumns); + foreach (OLVColumn col in this.RearrangableColumns) { + if (view == View.Details) + this.MapColumnToVisible[col] = col.IsVisible; + else + this.MapColumnToVisible[col] = col.IsTileViewColumn; + } + this.RearrangableColumns.Sort(new SortByDisplayOrder(this)); + + this.objectListView1.BooleanCheckStateGetter = delegate(Object rowObject) { + return this.MapColumnToVisible[(OLVColumn)rowObject]; + }; + + this.objectListView1.BooleanCheckStatePutter = delegate(Object rowObject, bool newValue) { + // Some columns should always be shown, so ignore attempts to hide them + OLVColumn column = (OLVColumn)rowObject; + if (!column.CanBeHidden) + return true; + + this.MapColumnToVisible[column] = newValue; + EnableControls(); + return newValue; + }; + + this.objectListView1.SetObjects(this.RearrangableColumns); + this.EnableControls(); + } + private List AllColumns = null; + private List RearrangableColumns = new List(); + private Dictionary MapColumnToVisible = new Dictionary(); + + /// + /// The user has pressed OK. Do what's required. + /// + /// + /// + protected void Apply(ObjectListView olv, View view) + { + olv.Freeze(); + + // Update the column definitions to reflect whether they have been hidden + if (view == View.Details) { + foreach (OLVColumn col in olv.AllColumns) + col.IsVisible = this.MapColumnToVisible[col]; + } else { + foreach (OLVColumn col in olv.AllColumns) + col.IsTileViewColumn = this.MapColumnToVisible[col]; + } + + // Collect the columns are still visible + List visibleColumns = this.RearrangableColumns.FindAll( + delegate(OLVColumn x) { return this.MapColumnToVisible[x]; }); + + // Detail view and Tile view have to be handled in different ways. + if (view == View.Details) { + // Of the still visible columns, change DisplayIndex to reflect their position in the rearranged list + olv.ChangeToFilteredColumns(view); + foreach (OLVColumn col in visibleColumns) { + col.DisplayIndex = visibleColumns.IndexOf((OLVColumn)col); + col.LastDisplayIndex = col.DisplayIndex; + } + } else { + // In Tile view, DisplayOrder does nothing. So to change the display order, we have to change the + // order of the columns in the Columns property. + // Remember, the primary column is special and has to remain first! + OLVColumn primaryColumn = this.AllColumns[0]; + visibleColumns.Remove(primaryColumn); + + olv.Columns.Clear(); + olv.Columns.Add(primaryColumn); + olv.Columns.AddRange(visibleColumns.ToArray()); + olv.CalculateReasonableTileSize(); + } + + olv.Unfreeze(); + } + + #region Event handlers + + private void buttonMoveUp_Click(object sender, EventArgs e) + { + int selectedIndex = this.objectListView1.SelectedIndices[0]; + OLVColumn col = this.RearrangableColumns[selectedIndex]; + this.RearrangableColumns.RemoveAt(selectedIndex); + this.RearrangableColumns.Insert(selectedIndex-1, col); + + this.objectListView1.BuildList(); + + EnableControls(); + } + + private void buttonMoveDown_Click(object sender, EventArgs e) + { + int selectedIndex = this.objectListView1.SelectedIndices[0]; + OLVColumn col = this.RearrangableColumns[selectedIndex]; + this.RearrangableColumns.RemoveAt(selectedIndex); + this.RearrangableColumns.Insert(selectedIndex + 1, col); + + this.objectListView1.BuildList(); + + EnableControls(); + } + + private void buttonShow_Click(object sender, EventArgs e) + { + this.objectListView1.SelectedItem.Checked = true; + } + + private void buttonHide_Click(object sender, EventArgs e) + { + this.objectListView1.SelectedItem.Checked = false; + } + + private void buttonOK_Click(object sender, EventArgs e) + { + this.DialogResult = DialogResult.OK; + this.Close(); + } + + private void buttonCancel_Click(object sender, EventArgs e) + { + this.DialogResult = DialogResult.Cancel; + this.Close(); + } + + private void objectListView1_SelectionChanged(object sender, EventArgs e) + { + EnableControls(); + } + + #endregion + + #region Control enabling + + /// + /// Enable the controls on the dialog to match the current state + /// + protected void EnableControls() + { + if (this.objectListView1.SelectedIndices.Count == 0) { + this.buttonMoveUp.Enabled = false; + this.buttonMoveDown.Enabled = false; + this.buttonShow.Enabled = false; + this.buttonHide.Enabled = false; + } else { + // Can't move the first row up or the last row down + this.buttonMoveUp.Enabled = (this.objectListView1.SelectedIndices[0] != 0); + this.buttonMoveDown.Enabled = (this.objectListView1.SelectedIndices[0] < (this.objectListView1.GetItemCount() - 1)); + + OLVColumn selectedColumn = (OLVColumn)this.objectListView1.SelectedObject; + + // Some columns cannot be hidden (and hence cannot be Shown) + this.buttonShow.Enabled = !this.MapColumnToVisible[selectedColumn] && selectedColumn.CanBeHidden; + this.buttonHide.Enabled = this.MapColumnToVisible[selectedColumn] && selectedColumn.CanBeHidden; + } + } + #endregion + + /// + /// A Comparer that will sort a list of columns so that visible ones come before hidden ones, + /// and that are ordered by their display order. + /// + private class SortByDisplayOrder : IComparer + { + public SortByDisplayOrder(ColumnSelectionForm form) + { + this.Form = form; + } + private ColumnSelectionForm Form; + + #region IComparer Members + + int IComparer.Compare(OLVColumn x, OLVColumn y) + { + if (this.Form.MapColumnToVisible[x] && !this.Form.MapColumnToVisible[y]) + return -1; + + if (!this.Form.MapColumnToVisible[x] && this.Form.MapColumnToVisible[y]) + return 1; + + if (x.DisplayIndex == y.DisplayIndex) + return x.Text.CompareTo(y.Text); + else + return x.DisplayIndex - y.DisplayIndex; + } + + #endregion + } + } +} diff --git a/ObjectListView/Utilities/ColumnSelectionForm.resx b/ObjectListView/Utilities/ColumnSelectionForm.resx new file mode 100644 index 00000000..19dc0dd8 --- /dev/null +++ b/ObjectListView/Utilities/ColumnSelectionForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ObjectListView/Utilities/Generator.cs b/ObjectListView/Utilities/Generator.cs new file mode 100644 index 00000000..705ed30e --- /dev/null +++ b/ObjectListView/Utilities/Generator.cs @@ -0,0 +1,563 @@ +/* + * Generator - Utility methods that generate columns or methods + * + * Author: Phillip Piper + * Date: 15/08/2009 22:37 + * + * Change log: + * 2015-06-17 JPP - Columns without [OLVColumn] now auto size + * 2012-08-16 JPP - Generator now considers [OLVChildren] and [OLVIgnore] attributes. + * 2012-06-14 JPP - Allow columns to be generated even if they are not marked with [OLVColumn] + * - Converted class from static to instance to allow it to be subclassed. + * Also, added IGenerator to allow it to be completely reimplemented. + * v2.5.1 + * 2010-11-01 JPP - DisplayIndex is now set correctly for columns that lack that attribute + * v2.4.1 + * 2010-08-25 JPP - Generator now also resets sort columns + * v2.4 + * 2010-04-14 JPP - Allow Name property to be set + * - Don't double set the Text property + * v2.3 + * 2009-08-15 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using System.Reflection.Emit; +using System.Text.RegularExpressions; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// An object that implements the IGenerator interface provides the ability + /// to dynamically create columns + /// for an ObjectListView based on the characteristics of a given collection + /// of model objects. + /// + public interface IGenerator { + /// + /// Generate columns into the given ObjectListView that come from the given + /// model object type. + /// + /// The ObjectListView to modify + /// The model type whose attributes will be considered. + /// Will columns be generated for properties that are not marked with [OLVColumn]. + void GenerateAndReplaceColumns(ObjectListView olv, Type type, bool allProperties); + + /// + /// Generate a list of OLVColumns based on the attributes of the given type + /// If allProperties to true, all public properties will have a matching column generated. + /// If allProperties is false, only properties that have a OLVColumn attribute will have a column generated. + /// + /// + /// Will columns be generated for properties that are not marked with [OLVColumn]. + /// A collection of OLVColumns matching the attributes of Type that have OLVColumnAttributes. + IList GenerateColumns(Type type, bool allProperties); + } + + /// + /// The Generator class provides methods to dynamically create columns + /// for an ObjectListView based on the characteristics of a given collection + /// of model objects. + /// + /// + /// For a given type, a Generator can create columns to match the public properties + /// of that type. The generator can consider all public properties or only those public properties marked with + /// [OLVColumn] attribute. + /// + public class Generator : IGenerator { + #region Static convenience methods + + /// + /// Gets or sets the actual generator used by the static convenience methods. + /// + /// If you subclass the standard generator or implement IGenerator yourself, + /// you should install an instance of your subclass/implementation here. + public static IGenerator Instance { + get { return Generator.instance ?? (Generator.instance = new Generator()); } + set { Generator.instance = value; } + } + private static IGenerator instance; + + /// + /// Replace all columns of the given ObjectListView with columns generated + /// from the first member of the given enumerable. If the enumerable is + /// empty or null, the ObjectListView will be cleared. + /// + /// The ObjectListView to modify + /// The collection whose first element will be used to generate columns. + static public void GenerateColumns(ObjectListView olv, IEnumerable enumerable) { + Generator.GenerateColumns(olv, enumerable, false); + } + + /// + /// Replace all columns of the given ObjectListView with columns generated + /// from the first member of the given enumerable. If the enumerable is + /// empty or null, the ObjectListView will be cleared. + /// + /// The ObjectListView to modify + /// The collection whose first element will be used to generate columns. + /// Will columns be generated for properties that are not marked with [OLVColumn]. + static public void GenerateColumns(ObjectListView olv, IEnumerable enumerable, bool allProperties) { + // Generate columns based on the type of the first model in the collection and then quit + if (enumerable != null) { + foreach (object model in enumerable) { + Generator.Instance.GenerateAndReplaceColumns(olv, model.GetType(), allProperties); + return; + } + } + + // If we reach here, the collection was empty, so we clear the list + Generator.Instance.GenerateAndReplaceColumns(olv, null, allProperties); + } + + /// + /// Generate columns into the given ObjectListView that come from the public properties of the given + /// model object type. + /// + /// The ObjectListView to modify + /// The model type whose attributes will be considered. + static public void GenerateColumns(ObjectListView olv, Type type) { + Generator.Instance.GenerateAndReplaceColumns(olv, type, false); + } + + /// + /// Generate columns into the given ObjectListView that come from the public properties of the given + /// model object type. + /// + /// The ObjectListView to modify + /// The model type whose attributes will be considered. + /// Will columns be generated for properties that are not marked with [OLVColumn]. + static public void GenerateColumns(ObjectListView olv, Type type, bool allProperties) { + Generator.Instance.GenerateAndReplaceColumns(olv, type, allProperties); + } + + /// + /// Generate a list of OLVColumns based on the public properties of the given type + /// that have a OLVColumn attribute. + /// + /// + /// A collection of OLVColumns matching the attributes of Type that have OLVColumnAttributes. + static public IList GenerateColumns(Type type) { + return Generator.Instance.GenerateColumns(type, false); + } + + #endregion + + #region Public interface + + /// + /// Generate columns into the given ObjectListView that come from the given + /// model object type. + /// + /// The ObjectListView to modify + /// The model type whose attributes will be considered. + /// Will columns be generated for properties that are not marked with [OLVColumn]. + public virtual void GenerateAndReplaceColumns(ObjectListView olv, Type type, bool allProperties) { + IList columns = this.GenerateColumns(type, allProperties); + TreeListView tlv = olv as TreeListView; + if (tlv != null) + this.TryGenerateChildrenDelegates(tlv, type); + this.ReplaceColumns(olv, columns); + } + + /// + /// Generate a list of OLVColumns based on the attributes of the given type + /// If allProperties to true, all public properties will have a matching column generated. + /// If allProperties is false, only properties that have a OLVColumn attribute will have a column generated. + /// + /// + /// Will columns be generated for properties that are not marked with [OLVColumn]. + /// A collection of OLVColumns matching the attributes of Type that have OLVColumnAttributes. + public virtual IList GenerateColumns(Type type, bool allProperties) { + List columns = new List(); + + // Sanity + if (type == null) + return columns; + + // Iterate all public properties in the class and build columns from those that have + // an OLVColumn attribute and that are not ignored. + foreach (PropertyInfo pinfo in type.GetProperties()) { + if (Attribute.GetCustomAttribute(pinfo, typeof(OLVIgnoreAttribute)) != null) + continue; + + OLVColumnAttribute attr = Attribute.GetCustomAttribute(pinfo, typeof(OLVColumnAttribute)) as OLVColumnAttribute; + if (attr == null) { + if (allProperties) + columns.Add(this.MakeColumnFromPropertyInfo(pinfo)); + } else { + columns.Add(this.MakeColumnFromAttribute(pinfo, attr)); + } + } + + // How many columns have DisplayIndex specifically set? + int countPositiveDisplayIndex = 0; + foreach (OLVColumn col in columns) { + if (col.DisplayIndex >= 0) + countPositiveDisplayIndex += 1; + } + + // Give columns that don't have a DisplayIndex an incremental index + int columnIndex = countPositiveDisplayIndex; + foreach (OLVColumn col in columns) + if (col.DisplayIndex < 0) + col.DisplayIndex = (columnIndex++); + + columns.Sort(delegate(OLVColumn x, OLVColumn y) { + return x.DisplayIndex.CompareTo(y.DisplayIndex); + }); + + return columns; + } + + #endregion + + #region Implementation + + /// + /// Replace all the columns in the given listview with the given list of columns. + /// + /// + /// + protected virtual void ReplaceColumns(ObjectListView olv, IList columns) { + olv.Reset(); + + // Are there new columns to add? + if (columns == null || columns.Count == 0) + return; + + // Setup the columns + olv.AllColumns.AddRange(columns); + this.PostCreateColumns(olv); + } + + /// + /// Post process columns after creating them and adding them to the AllColumns collection. + /// + /// + public virtual void PostCreateColumns(ObjectListView olv) { + if (olv.AllColumns.Exists(delegate(OLVColumn x) { return x.CheckBoxes; })) + olv.UseSubItemCheckBoxes = true; + if (olv.AllColumns.Exists(delegate(OLVColumn x) { return x.Index > 0 && (x.ImageGetter != null || !String.IsNullOrEmpty(x.ImageAspectName)); })) + olv.ShowImagesOnSubItems = true; + olv.RebuildColumns(); + olv.AutoSizeColumns(); + } + + /// + /// Create a column from the given PropertyInfo and OLVColumn attribute + /// + /// + /// + /// + protected virtual OLVColumn MakeColumnFromAttribute(PropertyInfo pinfo, OLVColumnAttribute attr) { + return MakeColumn(pinfo.Name, DisplayNameToColumnTitle(pinfo.Name), pinfo.CanWrite, pinfo.PropertyType, attr); + } + + /// + /// Make a column from the given PropertyInfo + /// + /// + /// + protected virtual OLVColumn MakeColumnFromPropertyInfo(PropertyInfo pinfo) { + return MakeColumn(pinfo.Name, DisplayNameToColumnTitle(pinfo.Name), pinfo.CanWrite, pinfo.PropertyType, null); + } + + /// + /// Make a column from the given PropertyDescriptor + /// + /// + /// + public virtual OLVColumn MakeColumnFromPropertyDescriptor(PropertyDescriptor pd) { + OLVColumnAttribute attr = pd.Attributes[typeof(OLVColumnAttribute)] as OLVColumnAttribute; + return MakeColumn(pd.Name, DisplayNameToColumnTitle(pd.DisplayName), !pd.IsReadOnly, pd.PropertyType, attr); + } + + /// + /// Create a column with all the given information + /// + /// + /// + /// + /// + /// + /// + protected virtual OLVColumn MakeColumn(string aspectName, string title, bool editable, Type propertyType, OLVColumnAttribute attr) { + + OLVColumn column = this.MakeColumn(aspectName, title, attr); + column.Name = (attr == null || String.IsNullOrEmpty(attr.Name)) ? aspectName : attr.Name; + this.ConfigurePossibleBooleanColumn(column, propertyType); + + if (attr == null) { + column.IsEditable = editable; + column.Width = -1; // Auto size + return column; + } + + column.AspectToStringFormat = attr.AspectToStringFormat; + if (attr.IsCheckBoxesSet) + column.CheckBoxes = attr.CheckBoxes; + column.DisplayIndex = attr.DisplayIndex; + column.FillsFreeSpace = attr.FillsFreeSpace; + if (attr.IsFreeSpaceProportionSet) + column.FreeSpaceProportion = attr.FreeSpaceProportion; + column.GroupWithItemCountFormat = attr.GroupWithItemCountFormat; + column.GroupWithItemCountSingularFormat = attr.GroupWithItemCountSingularFormat; + column.Hyperlink = attr.Hyperlink; + column.ImageAspectName = attr.ImageAspectName; + column.IsEditable = attr.IsEditableSet ? attr.IsEditable : editable; + column.IsTileViewColumn = attr.IsTileViewColumn; + column.IsVisible = attr.IsVisible; + column.MaximumWidth = attr.MaximumWidth; + column.MinimumWidth = attr.MinimumWidth; + column.Tag = attr.Tag; + if (attr.IsTextAlignSet) + column.TextAlign = attr.TextAlign; + column.ToolTipText = attr.ToolTipText; + if (attr.IsTriStateCheckBoxesSet) + column.TriStateCheckBoxes = attr.TriStateCheckBoxes; + column.UseInitialLetterForGroup = attr.UseInitialLetterForGroup; + column.Width = attr.Width; + if (attr.GroupCutoffs != null && attr.GroupDescriptions != null) + column.MakeGroupies(attr.GroupCutoffs, attr.GroupDescriptions); + return column; + } + + /// + /// Create a column. + /// + /// + /// + /// + /// + protected virtual OLVColumn MakeColumn(string aspectName, string title, OLVColumnAttribute attr) { + string columnTitle = (attr == null || String.IsNullOrEmpty(attr.Title)) ? title : attr.Title; + return new OLVColumn(columnTitle, aspectName); + } + + /// + /// Convert a property name to a displayable title. + /// + /// + /// + protected virtual string DisplayNameToColumnTitle(string displayName) { + string title = displayName.Replace("_", " "); + // Put a space between a lower-case letter that is followed immediately by an upper case letter + title = Regex.Replace(title, @"(\p{Ll})(\p{Lu})", @"$1 $2"); + return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(title); + } + + /// + /// Configure the given column to show a checkbox if appropriate + /// + /// + /// + protected virtual void ConfigurePossibleBooleanColumn(OLVColumn column, Type propertyType) { + if (propertyType != typeof(bool) && propertyType != typeof(bool?) && propertyType != typeof(CheckState)) + return; + + column.CheckBoxes = true; + column.TextAlign = HorizontalAlignment.Center; + column.Width = 32; + column.TriStateCheckBoxes = (propertyType == typeof(bool?) || propertyType == typeof(CheckState)); + } + + /// + /// If this given type has an property marked with [OLVChildren], make delegates that will + /// traverse that property as the children of an instance of the model + /// + /// + /// + protected virtual void TryGenerateChildrenDelegates(TreeListView tlv, Type type) { + foreach (PropertyInfo pinfo in type.GetProperties()) { + OLVChildrenAttribute attr = Attribute.GetCustomAttribute(pinfo, typeof(OLVChildrenAttribute)) as OLVChildrenAttribute; + if (attr != null) { + this.GenerateChildrenDelegates(tlv, pinfo); + return; + } + } + } + + /// + /// Generate CanExpand and ChildrenGetter delegates from the given property. + /// + /// + /// + protected virtual void GenerateChildrenDelegates(TreeListView tlv, PropertyInfo pinfo) { + Munger childrenGetter = new Munger(pinfo.Name); + tlv.CanExpandGetter = delegate(object x) { + try { + IEnumerable result = childrenGetter.GetValueEx(x) as IEnumerable; + return !ObjectListView.IsEnumerableEmpty(result); + } + catch (MungerException ex) { + System.Diagnostics.Debug.WriteLine(ex); + return false; + } + }; + tlv.ChildrenGetter = delegate(object x) { + try { + return childrenGetter.GetValueEx(x) as IEnumerable; + } + catch (MungerException ex) { + System.Diagnostics.Debug.WriteLine(ex); + return null; + } + }; + } + #endregion + + /* + #region Dynamic methods + + /// + /// Generate methods so that reflection is not needed. + /// + /// + /// + public static void GenerateMethods(ObjectListView olv, Type type) { + foreach (OLVColumn column in olv.Columns) { + GenerateColumnMethods(column, type); + } + } + + public static void GenerateColumnMethods(OLVColumn column, Type type) { + if (column.AspectGetter == null && !String.IsNullOrEmpty(column.AspectName)) + column.AspectGetter = Generator.GenerateAspectGetter(type, column.AspectName); + } + + /// + /// Generates an aspect getter method dynamically. The method will execute + /// the given dotted chain of selectors against a model object given at runtime. + /// + /// The type of model object to be passed to the generated method + /// A dotted chain of selectors. Each selector can be the name of a + /// field, property or parameter-less method. + /// A typed delegate + /// + /// + /// If you have an AspectName of "Owner.Address.Postcode", this will generate + /// the equivalent of: this.AspectGetter = delegate (object x) { + /// return x.Owner.Address.Postcode; + /// } + /// + /// + /// + private static AspectGetterDelegate GenerateAspectGetter(Type type, string path) { + DynamicMethod getter = new DynamicMethod(String.Empty, typeof(Object), new Type[] { type }, type, true); + Generator.GenerateIL(type, path, getter.GetILGenerator()); + return (AspectGetterDelegate)getter.CreateDelegate(typeof(AspectGetterDelegate)); + } + + /// + /// This method generates the actual IL for the method. + /// + /// + /// + /// + private static void GenerateIL(Type modelType, string path, ILGenerator il) { + // Push our model object onto the stack + il.Emit(OpCodes.Ldarg_0); + OpCodes.Castclass + // Generate the IL to access each part of the dotted chain + Type type = modelType; + string[] parts = path.Split('.'); + for (int i = 0; i < parts.Length; i++) { + type = Generator.GeneratePart(il, type, parts[i], (i == parts.Length - 1)); + if (type == null) + break; + } + + // If the object to be returned is a value type (e.g. int, bool), it + // must be boxed, since the delegate returns an Object + if (type != null && type.IsValueType && !modelType.IsValueType) + il.Emit(OpCodes.Box, type); + + il.Emit(OpCodes.Ret); + } + + private static Type GeneratePart(ILGenerator il, Type type, string pathPart, bool isLastPart) { + // TODO: Generate check for null + + // Find the first member with the given name that is a field, property, or parameter-less method + List infos = new List(type.GetMember(pathPart)); + MemberInfo info = infos.Find(delegate(MemberInfo x) { + if (x.MemberType == MemberTypes.Field || x.MemberType == MemberTypes.Property) + return true; + if (x.MemberType == MemberTypes.Method) + return ((MethodInfo)x).GetParameters().Length == 0; + else + return false; + }); + + // If we couldn't find anything with that name, pop the current result and return an error + if (info == null) { + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, String.Format("'{0}' is not a parameter-less method, property or field of type '{1}'", pathPart, type.FullName)); + return null; + } + + // Generate the correct IL to access the member. We remember the type of object that is going to be returned + // so that we can do a method lookup on it at the next iteration + Type resultType = null; + switch (info.MemberType) { + case MemberTypes.Method: + MethodInfo mi = (MethodInfo)info; + if (mi.IsVirtual) + il.Emit(OpCodes.Callvirt, mi); + else + il.Emit(OpCodes.Call, mi); + resultType = mi.ReturnType; + break; + case MemberTypes.Property: + PropertyInfo pi = (PropertyInfo)info; + il.Emit(OpCodes.Call, pi.GetGetMethod()); + resultType = pi.PropertyType; + break; + case MemberTypes.Field: + FieldInfo fi = (FieldInfo)info; + il.Emit(OpCodes.Ldfld, fi); + resultType = fi.FieldType; + break; + } + + // If the method returned a value type, and something is going to call a method on that value, + // we need to load its address onto the stack, rather than the object itself. + if (resultType.IsValueType && !isLastPart) { + LocalBuilder lb = il.DeclareLocal(resultType); + il.Emit(OpCodes.Stloc, lb); + il.Emit(OpCodes.Ldloca, lb); + } + + return resultType; + } + + #endregion + */ + } +} diff --git a/ObjectListView/Utilities/OLVExporter.cs b/ObjectListView/Utilities/OLVExporter.cs new file mode 100644 index 00000000..1400ba16 --- /dev/null +++ b/ObjectListView/Utilities/OLVExporter.cs @@ -0,0 +1,277 @@ +/* + * OLVExporter - Export the contents of an ObjectListView into various text-based formats + * + * Author: Phillip Piper + * Date: 7 August 2012, 10:35pm + * + * Change log: + * 2012-08-07 JPP Initial code + * + * Copyright (C) 2012 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace BrightIdeasSoftware { + /// + /// An OLVExporter converts a collection of rows from an ObjectListView + /// into a variety of textual formats. + /// + public class OLVExporter { + + /// + /// What format will be used for exporting + /// + public enum ExportFormat { + + /// + /// Tab separated values, according to http://www.iana.org/assignments/media-types/text/tab-separated-values + /// + TabSeparated = 1, + + /// + /// Alias for TabSeparated + /// + TSV = 1, + + /// + /// Comma separated values, according to http://www.ietf.org/rfc/rfc4180.txt + /// + CSV, + + /// + /// HTML table, according to me + /// + HTML + } + + #region Life and death + + /// + /// Create an empty exporter + /// + public OLVExporter() {} + + /// + /// Create an exporter that will export all the rows of the given ObjectListView + /// + /// + public OLVExporter(ObjectListView olv) : this(olv, olv.Objects) {} + + /// + /// Create an exporter that will export all the given rows from the given ObjectListView + /// + /// + /// + public OLVExporter(ObjectListView olv, IEnumerable objectsToExport) { + if (olv == null) throw new ArgumentNullException("olv"); + if (objectsToExport == null) throw new ArgumentNullException("objectsToExport"); + + this.ListView = olv; + this.ModelObjects = ObjectListView.EnumerableToArray(objectsToExport, true); + } + + #endregion + + #region Properties + + /// + /// Gets or sets whether hidden columns will also be included in the textual + /// representation. If this is false (the default), only visible columns will + /// be included. + /// + public bool IncludeHiddenColumns { + get { return includeHiddenColumns; } + set { includeHiddenColumns = value; } + } + private bool includeHiddenColumns; + + /// + /// Gets or sets whether column headers will also be included in the text + /// and HTML representation. Default is true. + /// + public bool IncludeColumnHeaders { + get { return includeColumnHeaders; } + set { includeColumnHeaders = value; } + } + private bool includeColumnHeaders = true; + + /// + /// Gets the ObjectListView that is being used as the source of the data + /// to be exported + /// + public ObjectListView ListView { + get { return objectListView; } + set { objectListView = value; } + } + private ObjectListView objectListView; + + /// + /// Gets the model objects that are to be placed in the data object + /// + public IList ModelObjects { + get { return modelObjects; } + set { modelObjects = value; } + } + private IList modelObjects = new ArrayList(); + + #endregion + + #region Commands + + /// + /// Export the nominated rows from the nominated ObjectListView. + /// Returns the result in the expected format. + /// + /// + /// + /// This will perform only one conversion, even if called multiple times with different formats. + public string ExportTo(ExportFormat format) { + if (results == null) + this.Convert(); + + return results[format]; + } + + /// + /// Convert + /// + public void Convert() { + + IList columns = this.IncludeHiddenColumns ? this.ListView.AllColumns : this.ListView.ColumnsInDisplayOrder; + + StringBuilder sbText = new StringBuilder(); + StringBuilder sbCsv = new StringBuilder(); + StringBuilder sbHtml = new StringBuilder(""); + + // Include column headers + if (this.IncludeColumnHeaders) { + List strings = new List(); + foreach (OLVColumn col in columns) + strings.Add(col.Text); + + WriteOneRow(sbText, strings, "", "\t", "", null); + WriteOneRow(sbHtml, strings, "", HtmlEncode); + WriteOneRow(sbCsv, strings, "", ",", "", CsvEncode); + } + + foreach (object modelObject in this.ModelObjects) { + List strings = new List(); + foreach (OLVColumn col in columns) + strings.Add(col.GetStringValue(modelObject)); + + WriteOneRow(sbText, strings, "", "\t", "", null); + WriteOneRow(sbHtml, strings, "", HtmlEncode); + WriteOneRow(sbCsv, strings, "", ",", "", CsvEncode); + } + sbHtml.AppendLine("
", "", "
", "", "
"); + + results = new Dictionary(); + results[ExportFormat.TabSeparated] = sbText.ToString(); + results[ExportFormat.CSV] = sbCsv.ToString(); + results[ExportFormat.HTML] = sbHtml.ToString(); + } + + private delegate string StringToString(string str); + + private void WriteOneRow(StringBuilder sb, IEnumerable strings, string startRow, string betweenCells, string endRow, StringToString encoder) { + sb.Append(startRow); + bool first = true; + foreach (string s in strings) { + if (!first) + sb.Append(betweenCells); + sb.Append(encoder == null ? s : encoder(s)); + first = false; + } + sb.AppendLine(endRow); + } + + private Dictionary results; + + #endregion + + #region Encoding + + /// + /// Encode a string such that it can be used as a value in a CSV file. + /// This basically means replacing any quote mark with two quote marks, + /// and enclosing the whole string in quotes. + /// + /// + /// + private static string CsvEncode(string text) { + if (text == null) + return null; + + const string DOUBLEQUOTE = @""""; // one double quote + const string TWODOUBEQUOTES = @""""""; // two double quotes + + StringBuilder sb = new StringBuilder(DOUBLEQUOTE); + sb.Append(text.Replace(DOUBLEQUOTE, TWODOUBEQUOTES)); + sb.Append(DOUBLEQUOTE); + + return sb.ToString(); + } + + /// + /// HTML-encodes a string and returns the encoded string. + /// + /// The text string to encode. + /// The HTML-encoded text. + /// Taken from http://www.west-wind.com/weblog/posts/2009/Feb/05/Html-and-Uri-String-Encoding-without-SystemWeb + private static string HtmlEncode(string text) { + if (text == null) + return null; + + StringBuilder sb = new StringBuilder(text.Length); + + int len = text.Length; + for (int i = 0; i < len; i++) { + switch (text[i]) { + case '<': + sb.Append("<"); + break; + case '>': + sb.Append(">"); + break; + case '"': + sb.Append("""); + break; + case '&': + sb.Append("&"); + break; + default: + if (text[i] > 159) { + // decimal numeric entity + sb.Append("&#"); + sb.Append(((int)text[i]).ToString(CultureInfo.InvariantCulture)); + sb.Append(";"); + } else + sb.Append(text[i]); + break; + } + } + return sb.ToString(); + } + #endregion + } +} \ No newline at end of file diff --git a/ObjectListView/Utilities/TypedObjectListView.cs b/ObjectListView/Utilities/TypedObjectListView.cs new file mode 100644 index 00000000..8eb6bd09 --- /dev/null +++ b/ObjectListView/Utilities/TypedObjectListView.cs @@ -0,0 +1,561 @@ +/* + * TypedObjectListView - A wrapper around an ObjectListView that provides type-safe delegates. + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * v2.6 + * 2012-10-26 JPP - Handle rare case where a null model object was passed into aspect getters. + * v2.3 + * 2009-03-31 JPP - Added Objects property + * 2008-11-26 JPP - Added tool tip getting methods + * 2008-11-05 JPP - Added CheckState handling methods + * 2008-10-24 JPP - Generate dynamic methods MkII. This one handles value types + * 2008-10-21 JPP - Generate dynamic methods + * 2008-09-27 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Reflection; +using System.Reflection.Emit; + +namespace BrightIdeasSoftware +{ + /// + /// A TypedObjectListView is a type-safe wrapper around an ObjectListView. + /// + /// + /// VCS does not support generics on controls. It can be faked to some degree, but it + /// cannot be completely overcome. In our case in particular, there is no way to create + /// the custom OLVColumn's that we need to truly be generic. So this wrapper is an + /// experiment in providing some type-safe access in a way that is useful and available today. + /// A TypedObjectListView is not more efficient than a normal ObjectListView. + /// Underneath, the same name of casts are performed. But it is easier to use since you + /// do not have to write the casts yourself. + /// + /// + /// The class of model object that the list will manage + /// + /// To use a TypedObjectListView, you write code like this: + /// + /// TypedObjectListView<Person> tlist = new TypedObjectListView<Person>(this.listView1); + /// tlist.CheckStateGetter = delegate(Person x) { return x.IsActive; }; + /// tlist.GetColumn(0).AspectGetter = delegate(Person x) { return x.Name; }; + /// ... + /// + /// To iterate over the selected objects, you can write something elegant like this: + /// + /// foreach (Person x in tlist.SelectedObjects) { + /// x.GrantSalaryIncrease(); + /// } + /// + /// + public class TypedObjectListView where T : class + { + /// + /// Create a typed wrapper around the given list. + /// + /// The listview to be wrapped + public TypedObjectListView(ObjectListView olv) { + this.olv = olv; + } + + //-------------------------------------------------------------------------------------- + // Properties + + /// + /// Return the model object that is checked, if only one row is checked. + /// If zero rows are checked, or more than one row, null is returned. + /// + public virtual T CheckedObject { + get { return (T)this.olv.CheckedObject; } + } + + /// + /// Return the list of all the checked model objects + /// + public virtual IList CheckedObjects { + get { + IList checkedObjects = this.olv.CheckedObjects; + List objects = new List(checkedObjects.Count); + foreach (object x in checkedObjects) + objects.Add((T)x); + + return objects; + } + set { this.olv.CheckedObjects = (IList)value; } + } + + /// + /// The ObjectListView that is being wrapped + /// + public virtual ObjectListView ListView { + get { return olv; } + set { olv = value; } + } + private ObjectListView olv; + + /// + /// Get or set the list of all model objects + /// + public virtual IList Objects { + get { + List objects = new List(this.olv.GetItemCount()); + for (int i = 0; i < this.olv.GetItemCount(); i++) + objects.Add(this.GetModelObject(i)); + + return objects; + } + set { this.olv.SetObjects(value); } + } + + /// + /// Return the model object that is selected, if only one row is selected. + /// If zero rows are selected, or more than one row, null is returned. + /// + public virtual T SelectedObject { + get { return (T)this.olv.SelectedObject; } + set { this.olv.SelectedObject = value; } + } + + /// + /// The list of model objects that are selected. + /// + public virtual IList SelectedObjects { + get { + List objects = new List(this.olv.SelectedIndices.Count); + foreach (int index in this.olv.SelectedIndices) + objects.Add((T)this.olv.GetModelObject(index)); + + return objects; + } + set { this.olv.SelectedObjects = (IList)value; } + } + + //-------------------------------------------------------------------------------------- + // Accessors + + /// + /// Return a typed wrapper around the column at the given index + /// + /// The index of the column + /// A typed column or null + public virtual TypedColumn GetColumn(int i) { + return new TypedColumn(this.olv.GetColumn(i)); + } + + /// + /// Return a typed wrapper around the column with the given name + /// + /// The name of the column + /// A typed column or null + public virtual TypedColumn GetColumn(string name) { + return new TypedColumn(this.olv.GetColumn(name)); + } + + /// + /// Return the model object at the given index + /// + /// The index of the model object + /// The model object or null + public virtual T GetModelObject(int index) { + return (T)this.olv.GetModelObject(index); + } + + //-------------------------------------------------------------------------------------- + // Delegates + + /// + /// CheckStateGetter + /// + /// + /// + public delegate CheckState TypedCheckStateGetterDelegate(T rowObject); + + /// + /// Gets or sets the check state getter + /// + public virtual TypedCheckStateGetterDelegate CheckStateGetter { + get { return checkStateGetter; } + set { + this.checkStateGetter = value; + if (value == null) + this.olv.CheckStateGetter = null; + else + this.olv.CheckStateGetter = delegate(object x) { + return this.checkStateGetter((T)x); + }; + } + } + private TypedCheckStateGetterDelegate checkStateGetter; + + /// + /// BooleanCheckStateGetter + /// + /// + /// + public delegate bool TypedBooleanCheckStateGetterDelegate(T rowObject); + + /// + /// Gets or sets the boolean check state getter + /// + public virtual TypedBooleanCheckStateGetterDelegate BooleanCheckStateGetter { + set { + if (value == null) + this.olv.BooleanCheckStateGetter = null; + else + this.olv.BooleanCheckStateGetter = delegate(object x) { + return value((T)x); + }; + } + } + + /// + /// CheckStatePutter + /// + /// + /// + /// + public delegate CheckState TypedCheckStatePutterDelegate(T rowObject, CheckState newValue); + + /// + /// Gets or sets the check state putter delegate + /// + public virtual TypedCheckStatePutterDelegate CheckStatePutter { + get { return checkStatePutter; } + set { + this.checkStatePutter = value; + if (value == null) + this.olv.CheckStatePutter = null; + else + this.olv.CheckStatePutter = delegate(object x, CheckState newValue) { + return this.checkStatePutter((T)x, newValue); + }; + } + } + private TypedCheckStatePutterDelegate checkStatePutter; + + /// + /// BooleanCheckStatePutter + /// + /// + /// + /// + public delegate bool TypedBooleanCheckStatePutterDelegate(T rowObject, bool newValue); + + /// + /// Gets or sets the boolean check state putter + /// + public virtual TypedBooleanCheckStatePutterDelegate BooleanCheckStatePutter { + set { + if (value == null) + this.olv.BooleanCheckStatePutter = null; + else + this.olv.BooleanCheckStatePutter = delegate(object x, bool newValue) { + return value((T)x, newValue); + }; + } + } + + /// + /// ToolTipGetter + /// + /// + /// + /// + public delegate String TypedCellToolTipGetterDelegate(OLVColumn column, T modelObject); + + /// + /// Gets or sets the cell tooltip getter + /// + public virtual TypedCellToolTipGetterDelegate CellToolTipGetter { + set { + if (value == null) + this.olv.CellToolTipGetter = null; + else + this.olv.CellToolTipGetter = delegate(OLVColumn col, Object x) { + return value(col, (T)x); + }; + } + } + + /// + /// Gets or sets the header tool tip getter + /// + public virtual HeaderToolTipGetterDelegate HeaderToolTipGetter { + get { return this.olv.HeaderToolTipGetter; } + set { this.olv.HeaderToolTipGetter = value; } + } + + //-------------------------------------------------------------------------------------- + // Commands + + /// + /// This method will generate AspectGetters for any column that has an AspectName. + /// + public virtual void GenerateAspectGetters() { + for (int i = 0; i < this.ListView.Columns.Count; i++) + this.GetColumn(i).GenerateAspectGetter(); + } + } + + /// + /// A type-safe wrapper around an OLVColumn + /// + /// + public class TypedColumn where T : class + { + /// + /// Creates a TypedColumn + /// + /// + public TypedColumn(OLVColumn column) { + this.column = column; + } + private OLVColumn column; + + /// + /// + /// + /// + /// + public delegate Object TypedAspectGetterDelegate(T rowObject); + + /// + /// + /// + /// + /// + public delegate void TypedAspectPutterDelegate(T rowObject, Object newValue); + + /// + /// + /// + /// + /// + public delegate Object TypedGroupKeyGetterDelegate(T rowObject); + + /// + /// + /// + /// + /// + public delegate Object TypedImageGetterDelegate(T rowObject); + + /// + /// + /// + public TypedAspectGetterDelegate AspectGetter { + get { return this.aspectGetter; } + set { + this.aspectGetter = value; + if (value == null) + this.column.AspectGetter = null; + else + this.column.AspectGetter = delegate(object x) { + return x == null ? null : this.aspectGetter((T)x); + }; + } + } + private TypedAspectGetterDelegate aspectGetter; + + /// + /// + /// + public TypedAspectPutterDelegate AspectPutter { + get { return aspectPutter; } + set { + this.aspectPutter = value; + if (value == null) + this.column.AspectPutter = null; + else + this.column.AspectPutter = delegate(object x, object newValue) { + this.aspectPutter((T)x, newValue); + }; + } + } + private TypedAspectPutterDelegate aspectPutter; + + /// + /// + /// + public TypedImageGetterDelegate ImageGetter { + get { return imageGetter; } + set { + this.imageGetter = value; + if (value == null) + this.column.ImageGetter = null; + else + this.column.ImageGetter = delegate(object x) { + return this.imageGetter((T)x); + }; + } + } + private TypedImageGetterDelegate imageGetter; + + /// + /// + /// + public TypedGroupKeyGetterDelegate GroupKeyGetter { + get { return groupKeyGetter; } + set { + this.groupKeyGetter = value; + if (value == null) + this.column.GroupKeyGetter = null; + else + this.column.GroupKeyGetter = delegate(object x) { + return this.groupKeyGetter((T)x); + }; + } + } + private TypedGroupKeyGetterDelegate groupKeyGetter; + + #region Dynamic methods + + /// + /// Generate an aspect getter that does the same thing as the AspectName, + /// except without using reflection. + /// + /// + /// + /// If you have an AspectName of "Owner.Address.Postcode", this will generate + /// the equivalent of: this.AspectGetter = delegate (object x) { + /// return x.Owner.Address.Postcode; + /// } + /// + /// + /// + /// If AspectName is empty, this method will do nothing, otherwise + /// this will replace any existing AspectGetter. + /// + /// + public void GenerateAspectGetter() { + if (!String.IsNullOrEmpty(this.column.AspectName)) + this.AspectGetter = this.GenerateAspectGetter(typeof(T), this.column.AspectName); + } + + /// + /// Generates an aspect getter method dynamically. The method will execute + /// the given dotted chain of selectors against a model object given at runtime. + /// + /// The type of model object to be passed to the generated method + /// A dotted chain of selectors. Each selector can be the name of a + /// field, property or parameter-less method. + /// A typed delegate + private TypedAspectGetterDelegate GenerateAspectGetter(Type type, string path) { + DynamicMethod getter = new DynamicMethod(String.Empty, + typeof(Object), new Type[] { type }, type, true); + this.GenerateIL(type, path, getter.GetILGenerator()); + return (TypedAspectGetterDelegate)getter.CreateDelegate(typeof(TypedAspectGetterDelegate)); + } + + /// + /// This method generates the actual IL for the method. + /// + /// + /// + /// + private void GenerateIL(Type type, string path, ILGenerator il) { + // Push our model object onto the stack + il.Emit(OpCodes.Ldarg_0); + + // Generate the IL to access each part of the dotted chain + string[] parts = path.Split('.'); + for (int i = 0; i < parts.Length; i++) { + type = this.GeneratePart(il, type, parts[i], (i == parts.Length - 1)); + if (type == null) + break; + } + + // If the object to be returned is a value type (e.g. int, bool), it + // must be boxed, since the delegate returns an Object + if (type != null && type.IsValueType && !typeof(T).IsValueType) + il.Emit(OpCodes.Box, type); + + il.Emit(OpCodes.Ret); + } + + private Type GeneratePart(ILGenerator il, Type type, string pathPart, bool isLastPart) { + // TODO: Generate check for null + + // Find the first member with the given name that is a field, property, or parameter-less method + List infos = new List(type.GetMember(pathPart)); + MemberInfo info = infos.Find(delegate(MemberInfo x) { + if (x.MemberType == MemberTypes.Field || x.MemberType == MemberTypes.Property) + return true; + if (x.MemberType == MemberTypes.Method) + return ((MethodInfo)x).GetParameters().Length == 0; + else + return false; + }); + + // If we couldn't find anything with that name, pop the current result and return an error + if (info == null) { + il.Emit(OpCodes.Pop); + if (Munger.IgnoreMissingAspects) + il.Emit(OpCodes.Ldnull); + else + il.Emit(OpCodes.Ldstr, String.Format("'{0}' is not a parameter-less method, property or field of type '{1}'", pathPart, type.FullName)); + return null; + } + + // Generate the correct IL to access the member. We remember the type of object that is going to be returned + // so that we can do a method lookup on it at the next iteration + Type resultType = null; + switch (info.MemberType) { + case MemberTypes.Method: + MethodInfo mi = (MethodInfo)info; + if (mi.IsVirtual) + il.Emit(OpCodes.Callvirt, mi); + else + il.Emit(OpCodes.Call, mi); + resultType = mi.ReturnType; + break; + case MemberTypes.Property: + PropertyInfo pi = (PropertyInfo)info; + il.Emit(OpCodes.Call, pi.GetGetMethod()); + resultType = pi.PropertyType; + break; + case MemberTypes.Field: + FieldInfo fi = (FieldInfo)info; + il.Emit(OpCodes.Ldfld, fi); + resultType = fi.FieldType; + break; + } + + // If the method returned a value type, and something is going to call a method on that value, + // we need to load its address onto the stack, rather than the object itself. + if (resultType.IsValueType && !isLastPart) { + LocalBuilder lb = il.DeclareLocal(resultType); + il.Emit(OpCodes.Stloc, lb); + il.Emit(OpCodes.Ldloca, lb); + } + + return resultType; + } + + #endregion + } +} diff --git a/ObjectListView/VirtualObjectListView.cs b/ObjectListView/VirtualObjectListView.cs new file mode 100644 index 00000000..8c3ba28a --- /dev/null +++ b/ObjectListView/VirtualObjectListView.cs @@ -0,0 +1,1255 @@ +/* + * VirtualObjectListView - A virtual listview delays fetching model objects until they are actually displayed. + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * 2015-06-14 JPP - Moved handling of CheckBoxes on virtual lists into base class (ObjectListView). + * This allows the property to be set correctly, even when set via an upcast reference. + * 2015-03-25 JPP - Subscribe to change notifications when objects are added + * v2.8 + * 2014-09-26 JPP - Correct an incorrect use of checkStateMap when setting CheckedObjects + * and a CheckStateGetter is installed + * v2.6 + * 2012-06-13 JPP - Corrected several bugs related to groups on virtual lists. + * - Added EnsureNthGroupVisible() since EnsureGroupVisible() can't work on virtual lists. + * v2.5.1 + * 2012-05-04 JPP - Avoid bug/feature in ListView.VirtalListSize setter that causes flickering + * when the size of the list changes. + * 2012-04-24 JPP - Fixed bug that occurred when adding/removing item while the view was grouped. + * v2.5 + * 2011-05-31 JPP - Setting CheckedObjects is more efficient on large collections + * 2011-04-05 JPP - CheckedObjects now only returns objects that are currently in the list. + * ClearObjects() now resets all check state info. + * 2011-03-31 JPP - Filtering on grouped virtual lists no longer behaves strangely. + * 2011-03-17 JPP - Virtual lists can (finally) set CheckBoxes back to false if it has been set to true. + * (this is a little hacky and may not work reliably). + * - GetNextItem() and GetPreviousItem() now work on grouped virtual lists. + * 2011-03-08 JPP - BREAKING CHANGE: 'DataSource' was renamed to 'VirtualListDataSource'. This was necessary + * to allow FastDataListView which is both a DataListView AND a VirtualListView -- + * which both used a 'DataSource' property :( + * v2.4 + * 2010-04-01 JPP - Support filtering + * v2.3 + * 2009-08-28 JPP - BIG CHANGE. Virtual lists can now have groups! + * - Objects property now uses "yield return" -- much more efficient for big lists + * 2009-08-07 JPP - Use new scheme for formatting rows/cells + * v2.2.1 + * 2009-07-24 JPP - Added specialised version of RefreshSelectedObjects() which works efficiently with virtual lists + * (thanks to chriss85 for finding this bug) + * 2009-07-03 JPP - Standardized code format + * v2.2 + * 2009-04-06 JPP - ClearObjects() now works again + * v2.1 + * 2009-02-24 JPP - Removed redundant OnMouseDown() since checkbox + * handling is now handled in the base class + * 2009-01-07 JPP - Made all public and protected methods virtual + * 2008-12-07 JPP - Trigger Before/AfterSearching events + * 2008-11-15 JPP - Fixed some caching issues + * 2008-11-05 JPP - Rewrote handling of check boxes + * 2008-10-28 JPP - Handle SetSelectedObjects(null) + * 2008-10-02 JPP - MAJOR CHANGE: Use IVirtualListDataSource + * 2008-09-27 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; +using System.Runtime.InteropServices; + +namespace BrightIdeasSoftware +{ + /// + /// A virtual object list view operates in virtual mode, that is, it only gets model objects for + /// a row when it is needed. This gives it the ability to handle very large numbers of rows with + /// minimal resources. + /// + /// A listview is not a great user interface for a large number of items. But if you've + /// ever wanted to have a list with 10 million items, go ahead, knock yourself out. + /// Virtual lists can never iterate their contents. That would defeat the whole purpose. + /// Animated GIFs should not be used in virtual lists. Animated GIFs require some state + /// information to be stored for each animation, but virtual lists specifically do not keep any state information. + /// In any case, you really do not want to keep state information for 10 million animations! + /// + /// Although it isn't documented, .NET virtual lists cannot have checkboxes. This class codes around this limitation, + /// but you must use the functions provided by ObjectListView: CheckedObjects, CheckObject(), UncheckObject() and their friends. + /// If you use the normal check box properties (CheckedItems or CheckedIndicies), they will throw an exception, since the + /// list is in virtual mode, and .NET "knows" it can't handle checkboxes in virtual mode. + /// + /// Due to the limits of the underlying Windows control, virtual lists do not trigger ItemCheck/ItemChecked events. + /// Use a CheckStatePutter instead. + /// To enable grouping, you must provide an implementation of IVirtualGroups interface, via the GroupingStrategy property. + /// Similarly, to enable filtering on the list, your VirtualListDataSource must also implement the IFilterableDataSource interface. + /// + public class VirtualObjectListView : ObjectListView + { + /// + /// Create a VirtualObjectListView + /// + public VirtualObjectListView() + : base() { + this.VirtualMode = true; // Virtual lists have to be virtual -- no prizes for guessing that :) + + this.CacheVirtualItems += new CacheVirtualItemsEventHandler(this.HandleCacheVirtualItems); + this.RetrieveVirtualItem += new RetrieveVirtualItemEventHandler(this.HandleRetrieveVirtualItem); + this.SearchForVirtualItem += new SearchForVirtualItemEventHandler(this.HandleSearchForVirtualItem); + + // At the moment, we don't need to handle this event. But we'll keep this comment to remind us about it. + this.VirtualItemsSelectionRangeChanged += new ListViewVirtualItemsSelectionRangeChangedEventHandler(this.HandleVirtualItemsSelectionRangeChanged); + + this.VirtualListDataSource = new VirtualListVersion1DataSource(this); + + // Virtual lists have to manage their own check state, since the normal ListView control + // doesn't even allow checkboxes on virtual lists + this.PersistentCheckBoxes = true; + } + + #region Public Properties + + /// + /// Gets whether or not this listview is capable of showing groups + /// + [Browsable(false)] + public override bool CanShowGroups { + get { + // Virtual lists need Vista and a grouping strategy to show groups + return (ObjectListView.IsVistaOrLater && this.GroupingStrategy != null); + } + } + + /// + /// Get or set the collection of model objects that are checked. + /// When setting this property, any row whose model object isn't + /// in the given collection will be unchecked. Setting to null is + /// equivalent to unchecking all. + /// + /// + /// + /// This property returns a simple collection. Changes made to the returned + /// collection do NOT affect the list. This is different to the behaviour of + /// CheckedIndicies collection. + /// + /// + /// When getting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects. + /// When setting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects plus + /// the number of objects to be checked. + /// + /// + /// If the ListView is not currently showing CheckBoxes, this property does nothing. It does + /// not remember any check box settings made. + /// + /// + /// This class optimizes the management of CheckStates so that it will work efficiently even on + /// large lists of item. However, those optimizations are impossible if you install a CheckStateGetter. + /// With a CheckStateGetter installed, the performance of this method is O(n) where n is the size + /// of the list. This could be painfully slow. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IList CheckedObjects { + get { + // If we aren't should checkboxes, then no objects can be checked + if (!this.CheckBoxes) + return new ArrayList(); + + // If the data source has somehow vanished, we can't do anything + if (this.VirtualListDataSource == null) + return new ArrayList(); + + // If a custom check state getter is install, we can't use our check state management + // We have to use the (slower) base version. + if (this.CheckStateGetter != null) + return base.CheckedObjects; + + // Collect items that are checked AND that still exist in the list. + ArrayList objects = new ArrayList(); + foreach (KeyValuePair kvp in this.CheckStateMap) + { + if (kvp.Value == CheckState.Checked && + (!this.CheckedObjectsMustStillExistInList || + this.VirtualListDataSource.GetObjectIndex(kvp.Key) >= 0)) + objects.Add(kvp.Key); + } + return objects; + } + set { + if (!this.CheckBoxes) + return; + + // If a custom check state getter is install, we can't use our check state management + // We have to use the (slower) base version. + if (this.CheckStateGetter != null) { + base.CheckedObjects = value; + return; + } + + Stopwatch sw = Stopwatch.StartNew(); + + // Set up an efficient way of testing for the presence of a particular model + Hashtable table = new Hashtable(this.GetItemCount()); + if (value != null) { + foreach (object x in value) + table[x] = true; + } + + this.BeginUpdate(); + + // Uncheck anything that is no longer checked + Object[] keys = new Object[this.CheckStateMap.Count]; + this.CheckStateMap.Keys.CopyTo(keys, 0); + foreach (Object key in keys) { + if (!table.Contains(key)) + this.SetObjectCheckedness(key, CheckState.Unchecked); + } + + // Check all the new checked objects + foreach (Object x in table.Keys) + this.SetObjectCheckedness(x, CheckState.Checked); + + this.EndUpdate(); + + // Debug.WriteLine(String.Format("PERF - Setting virtual CheckedObjects on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); + } + } + + /// + /// Gets or sets whether or not an object will be included in the CheckedObjects + /// collection, even if it is not present in the control at the moment + /// + /// + /// This property is an implementation detail and should not be altered. + /// + protected internal bool CheckedObjectsMustStillExistInList { + get { return checkedObjectsMustStillExistInList; } + set { checkedObjectsMustStillExistInList = value; } + } + private bool checkedObjectsMustStillExistInList = true; + + /// + /// Gets the collection of objects that survive any filtering that may be in place. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable FilteredObjects { + get { + for (int i = 0; i < this.GetItemCount(); i++) + yield return this.GetModelObject(i); + } + } + + /// + /// Gets or sets the strategy that will be used to create groups + /// + /// + /// This must be provided for a virtual list to show groups. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IVirtualGroups GroupingStrategy { + get { return this.groupingStrategy; } + set { this.groupingStrategy = value; } + } + private IVirtualGroups groupingStrategy; + + /// + /// Gets whether or not the current list is filtering its contents + /// + /// + /// This is only possible if our underlying data source supports filtering. + /// + public override bool IsFiltering { + get { + return base.IsFiltering && (this.VirtualListDataSource is IFilterableDataSource); + } + } + + /// + /// Get/set the collection of objects that this list will show + /// + /// + /// + /// The contents of the control will be updated immediately after setting this property. + /// + /// Setting this property preserves selection, if possible. Use SetObjects() if + /// you do not want to preserve the selection. Preserving selection is the slowest part of this + /// code -- performance is O(n) where n is the number of selected rows. + /// This method is not thread safe. + /// The property DOES work on virtual lists, but if you try to iterate through a list + /// of 10 million objects, it may take some time :) + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable Objects { + get { + IFilterableDataSource filterable = this.VirtualListDataSource as IFilterableDataSource; + try { + // If we are filtering, we have to temporarily disable filtering so we get + // the whole collection + if (filterable != null && this.UseFiltering) + filterable.ApplyFilters(null, null); + return this.FilteredObjects; + } finally { + if (filterable != null && this.UseFiltering) + filterable.ApplyFilters(this.ModelFilter, this.ListFilter); + } + } + set { base.Objects = value; } + } + + /// + /// This delegate is used to fetch a rowObject, given it's index within the list + /// + /// Only use this property if you are not using a VirtualListDataSource. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual RowGetterDelegate RowGetter { + get { return ((VirtualListVersion1DataSource)this.virtualListDataSource).RowGetter; } + set { ((VirtualListVersion1DataSource)this.virtualListDataSource).RowGetter = value; } + } + + /// + /// Should this list show its items in groups? + /// + [Category("Appearance"), + Description("Should the list view show items in groups?"), + DefaultValue(true)] + override public bool ShowGroups { + get { + // Pre-Vista, virtual lists cannot show groups + return ObjectListView.IsVistaOrLater && this.showGroups; + } + set { + this.showGroups = value; + if (this.Created && !value) + this.DisableVirtualGroups(); + } + } + private bool showGroups; + + + /// + /// Get/set the data source that is behind this virtual list + /// + /// Setting this will cause the list to redraw. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IVirtualListDataSource VirtualListDataSource { + get { + return this.virtualListDataSource; + } + set { + this.virtualListDataSource = value; + this.CustomSorter = delegate(OLVColumn column, SortOrder sortOrder) { + this.ClearCachedInfo(); + this.virtualListDataSource.Sort(column, sortOrder); + }; + this.BuildList(false); + } + } + private IVirtualListDataSource virtualListDataSource; + + /// + /// Gets or sets the number of rows in this virtual list. + /// + /// + /// There is an annoying feature/bug in the .NET ListView class. + /// When you change the VirtualListSize property, it always scrolls so + /// that the focused item is the top item. This is annoying since it makes + /// the virtual list seem to flicker as the control scrolls to show the focused + /// item and then scrolls back to where ObjectListView wants it to be. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + protected new virtual int VirtualListSize { + get { return base.VirtualListSize; } + set { + if (value == this.VirtualListSize || value < 0) + return; + + // Get around the 'private' marker on 'virtualListSize' field using reflection + if (virtualListSizeFieldInfo == null) { + virtualListSizeFieldInfo = typeof(ListView).GetField("virtualListSize", BindingFlags.NonPublic | BindingFlags.Instance); + System.Diagnostics.Debug.Assert(virtualListSizeFieldInfo != null); + } + + // Set the base class private field so that it keeps on working + virtualListSizeFieldInfo.SetValue(this, value); + + // Send a raw message to change the virtual list size *without* changing the scroll position + if (this.IsHandleCreated && !this.DesignMode) + NativeMethods.SetItemCount(this, value); + } + } + static private FieldInfo virtualListSizeFieldInfo; + + #endregion + + #region OLV accessing + + /// + /// Return the number of items in the list + /// + /// the number of items in the list + public override int GetItemCount() { + return this.VirtualListSize; + } + + /// + /// Return the model object at the given index + /// + /// Index of the model object to be returned + /// A model object + public override object GetModelObject(int index) { + if (this.VirtualListDataSource != null && index >= 0 && index < this.GetItemCount()) + return this.VirtualListDataSource.GetNthObject(index); + else + return null; + } + + /// + /// Find the given model object within the listview and return its index + /// + /// The model object to be found + /// The index of the object. -1 means the object was not present + public override int IndexOf(Object modelObject) { + if (this.VirtualListDataSource == null || modelObject == null) + return -1; + + return this.VirtualListDataSource.GetObjectIndex(modelObject); + } + + /// + /// Return the OLVListItem that displays the given model object + /// + /// The modelObject whose item is to be found + /// The OLVListItem that displays the model, or null + /// This method has O(n) performance. + public override OLVListItem ModelToItem(object modelObject) { + if (this.VirtualListDataSource == null || modelObject == null) + return null; + + int index = this.VirtualListDataSource.GetObjectIndex(modelObject); + return index >= 0 ? this.GetItem(index) : null; + } + + #endregion + + #region Object manipulation + + /// + /// Add the given collection of model objects to this control. + /// + /// A collection of model objects + /// + /// The added objects will appear in their correct sort position, if sorting + /// is active. Otherwise, they will appear at the end of the list. + /// No check is performed to see if any of the objects are already in the ListView. + /// Null objects are silently ignored. + /// + public override void AddObjects(ICollection modelObjects) { + if (this.VirtualListDataSource == null) + return; + + // Give the world a chance to cancel or change the added objects + ItemsAddingEventArgs args = new ItemsAddingEventArgs(modelObjects); + this.OnItemsAdding(args); + if (args.Canceled) + return; + + try + { + this.BeginUpdate(); + this.VirtualListDataSource.AddObjects(args.ObjectsToAdd); + this.BuildList(); + this.SubscribeNotifications(args.ObjectsToAdd); + } + finally + { + this.EndUpdate(); + } + } + + /// + /// Remove all items from this list + /// + /// This method can safely be called from background threads. + public override void ClearObjects() { + if (this.InvokeRequired) + this.Invoke(new MethodInvoker(this.ClearObjects)); + else { + this.CheckStateMap.Clear(); + this.SetObjects(new ArrayList()); + } + } + + /// + /// Scroll the listview so that the given group is at the top. + /// + /// The index of the group to be revealed + /// + /// If the group is already visible, the list will still be scrolled to move + /// the group to the top, if that is possible. + /// + /// This only works when the list is showing groups (obviously). + /// + public virtual void EnsureNthGroupVisible(int groupIndex) { + if (!this.ShowGroups) + return; + + if (groupIndex <= 0 || groupIndex >= this.OLVGroups.Count) { + // There is no easy way to scroll back to the beginning of the list + int delta = 0 - NativeMethods.GetScrollPosition(this, false); + NativeMethods.Scroll(this, 0, delta); + } else { + // Find the display rectangle of the last item in the previous group + OLVGroup previousGroup = this.OLVGroups[groupIndex - 1]; + int lastItemInGroup = this.GroupingStrategy.GetGroupMember(previousGroup, previousGroup.VirtualItemCount - 1); + Rectangle r = this.GetItemRect(lastItemInGroup); + + // Scroll so that the last item of the previous group is just out of sight, + // which will make the desired group header visible. + int delta = r.Y + r.Height / 2; + NativeMethods.Scroll(this, 0, delta); + } + } + + /// + /// Inserts the given collection of model objects to this control at hte given location + /// + /// The index where the new objects will be inserted + /// A collection of model objects + /// + /// The added objects will appear in their correct sort position, if sorting + /// is active. Otherwise, they will appear at the given position of the list. + /// No check is performed to see if any of the objects are already in the ListView. + /// Null objects are silently ignored. + /// + public override void InsertObjects(int index, ICollection modelObjects) + { + if (this.VirtualListDataSource == null) + return; + + // Give the world a chance to cancel or change the added objects + ItemsAddingEventArgs args = new ItemsAddingEventArgs(index, modelObjects); + this.OnItemsAdding(args); + if (args.Canceled) + return; + + try + { + this.BeginUpdate(); + this.VirtualListDataSource.InsertObjects(index, args.ObjectsToAdd); + this.BuildList(); + this.SubscribeNotifications(args.ObjectsToAdd); + } + finally + { + this.EndUpdate(); + } + } + + /// + /// Update the rows that are showing the given objects + /// + /// This method does not resort the items. + public override void RefreshObjects(IList modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.RefreshObjects(modelObjects); }); + return; + } + + // Without a data source, we can't do this. + if (this.VirtualListDataSource == null) + return; + + try { + this.BeginUpdate(); + this.ClearCachedInfo(); + foreach (object modelObject in modelObjects) { + int index = this.VirtualListDataSource.GetObjectIndex(modelObject); + if (index >= 0) { + this.VirtualListDataSource.UpdateObject(index, modelObject); + this.RedrawItems(index, index, true); + } + } + } + finally { + this.EndUpdate(); + } + } + + /// + /// Update the rows that are selected + /// + /// This method does not resort or regroup the view. + public override void RefreshSelectedObjects() { + foreach (int index in this.SelectedIndices) + this.RedrawItems(index, index, true); + } + + /// + /// Remove all of the given objects from the control + /// + /// Collection of objects to be removed + /// + /// Nulls and model objects that are not in the ListView are silently ignored. + /// Due to problems in the underlying ListView, if you remove all the objects from + /// the control using this method and the list scroll vertically when you do so, + /// then when you subsequently add more objects to the control, + /// the vertical scroll bar will become confused and the control will draw one or more + /// blank lines at the top of the list. + /// + public override void RemoveObjects(ICollection modelObjects) { + if (this.VirtualListDataSource == null) + return; + + // Give the world a chance to cancel or change the removed objects + ItemsRemovingEventArgs args = new ItemsRemovingEventArgs(modelObjects); + this.OnItemsRemoving(args); + if (args.Canceled) + return; + + try { + this.BeginUpdate(); + this.VirtualListDataSource.RemoveObjects(args.ObjectsToRemove); + this.BuildList(); + this.UnsubscribeNotifications(args.ObjectsToRemove); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Select the row that is displaying the given model object. All other rows are deselected. + /// + /// Model object to select + /// Should the object be focused as well? + public override void SelectObject(object modelObject, bool setFocus) { + // Without a data source, we can't do this. + if (this.VirtualListDataSource == null) + return; + + // Check that the object is in the list (plus not all data sources can locate objects) + int index = this.VirtualListDataSource.GetObjectIndex(modelObject); + if (index < 0 || index >= this.VirtualListSize) + return; + + // If the given model is already selected, don't do anything else (prevents an flicker) + if (this.SelectedIndices.Count == 1 && this.SelectedIndices[0] == index) + return; + + // Finally, select the row + this.SelectedIndices.Clear(); + this.SelectedIndices.Add(index); + if (setFocus && this.SelectedItem != null) + this.SelectedItem.Focused = true; + } + + /// + /// Select the rows that is displaying any of the given model object. All other rows are deselected. + /// + /// A collection of model objects + /// This method has O(n) performance where n is the number of model objects passed. + /// Do not use this to select all the rows in the list -- use SelectAll() for that. + public override void SelectObjects(IList modelObjects) { + // Without a data source, we can't do this. + if (this.VirtualListDataSource == null) + return; + + this.SelectedIndices.Clear(); + + if (modelObjects == null) + return; + + foreach (object modelObject in modelObjects) { + int index = this.VirtualListDataSource.GetObjectIndex(modelObject); + if (index >= 0 && index < this.VirtualListSize) + this.SelectedIndices.Add(index); + } + } + + /// + /// Set the collection of objects that this control will show. + /// + /// + /// Should the state of the list be preserved as far as is possible. + public override void SetObjects(IEnumerable collection, bool preserveState) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.SetObjects(collection, preserveState); }); + return; + } + + if (this.VirtualListDataSource == null) + return; + + // Give the world a chance to cancel or change the assigned collection + ItemsChangingEventArgs args = new ItemsChangingEventArgs(null, collection); + this.OnItemsChanging(args); + if (args.Canceled) + return; + + this.BeginUpdate(); + try { + this.VirtualListDataSource.SetObjects(args.NewObjects); + this.BuildList(); + this.UpdateNotificationSubscriptions(args.NewObjects); + } + finally { + this.EndUpdate(); + } + } + + #endregion + + #region Check boxes +// +// /// +// /// Check all rows +// /// +// /// The performance of this method is O(n) where n is the number of rows in the control. +// public override void CheckAll() +// { +// if (!this.CheckBoxes) +// return; +// +// Stopwatch sw = Stopwatch.StartNew(); +// +// this.BeginUpdate(); +// +// foreach (Object x in this.Objects) +// this.SetObjectCheckedness(x, CheckState.Checked); +// +// this.EndUpdate(); +// +// Debug.WriteLine(String.Format("PERF - CheckAll() on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); +// +// } +// +// /// +// /// Uncheck all rows +// /// +// /// The performance of this method is O(n) where n is the number of rows in the control. +// public override void UncheckAll() +// { +// if (!this.CheckBoxes) +// return; +// +// Stopwatch sw = Stopwatch.StartNew(); +// +// this.BeginUpdate(); +// +// foreach (Object x in this.Objects) +// this.SetObjectCheckedness(x, CheckState.Unchecked); +// +// this.EndUpdate(); +// +// Debug.WriteLine(String.Format("PERF - UncheckAll() on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); +// } + + /// + /// Get the checkedness of an object from the model. Returning null means the + /// model does know and the value from the control will be used. + /// + /// + /// + protected override CheckState? GetCheckState(object modelObject) + { + if (this.CheckStateGetter != null) + return base.GetCheckState(modelObject); + + CheckState state; + if (modelObject != null && this.CheckStateMap.TryGetValue(modelObject, out state)) + return state; + return CheckState.Unchecked; + } + + #endregion + + #region Implementation + + /// + /// Rebuild the list with its current contents. + /// + /// + /// Invalidate any cached information when we rebuild the list. + /// + public override void BuildList(bool shouldPreserveSelection) { + this.UpdateVirtualListSize(); + this.ClearCachedInfo(); + if (this.ShowGroups) + this.BuildGroups(); + else + this.Sort(); + this.Invalidate(); + } + + /// + /// Clear any cached info this list may have been using + /// + public override void ClearCachedInfo() { + this.lastRetrieveVirtualItemIndex = -1; + } + + /// + /// Do the work of creating groups for this control + /// + /// + protected override void CreateGroups(IEnumerable groups) { + + // In a virtual list, we cannot touch the Groups property. + // It was obviously not written for virtual list and often throws exceptions. + + NativeMethods.ClearGroups(this); + + this.EnableVirtualGroups(); + + foreach (OLVGroup group in groups) { + System.Diagnostics.Debug.Assert(group.Items.Count == 0, "Groups in virtual lists cannot set Items. Use VirtualItemCount instead."); + System.Diagnostics.Debug.Assert(group.VirtualItemCount > 0, "VirtualItemCount must be greater than 0."); + + group.InsertGroupNewStyle(this); + } + } + + /// + /// Do the plumbing to disable groups on a virtual list + /// + protected void DisableVirtualGroups() { + NativeMethods.ClearGroups(this); + //System.Diagnostics.Debug.WriteLine(err); + + const int LVM_ENABLEGROUPVIEW = 0x1000 + 157; + IntPtr x = NativeMethods.SendMessage(this.Handle, LVM_ENABLEGROUPVIEW, 0, 0); + //System.Diagnostics.Debug.WriteLine(x); + + const int LVM_SETOWNERDATACALLBACK = 0x10BB; + IntPtr x2 = NativeMethods.SendMessage(this.Handle, LVM_SETOWNERDATACALLBACK, 0, 0); + //System.Diagnostics.Debug.WriteLine(x2); + } + + /// + /// Do the plumbing to enable groups on a virtual list + /// + protected void EnableVirtualGroups() { + + // We need to implement the IOwnerDataCallback interface + if (this.ownerDataCallbackImpl == null) + this.ownerDataCallbackImpl = new OwnerDataCallbackImpl(this); + + const int LVM_SETOWNERDATACALLBACK = 0x10BB; + IntPtr ptr = Marshal.GetComInterfaceForObject(ownerDataCallbackImpl, typeof(IOwnerDataCallback)); + IntPtr x = NativeMethods.SendMessage(this.Handle, LVM_SETOWNERDATACALLBACK, ptr, 0); + //System.Diagnostics.Debug.WriteLine(x); + Marshal.Release(ptr); + + const int LVM_ENABLEGROUPVIEW = 0x1000 + 157; + x = NativeMethods.SendMessage(this.Handle, LVM_ENABLEGROUPVIEW, 1, 0); + //System.Diagnostics.Debug.WriteLine(x); + } + private OwnerDataCallbackImpl ownerDataCallbackImpl; + + /// + /// Return the position of the given itemIndex in the list as it currently shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + /// + public override int GetDisplayOrderOfItemIndex(int itemIndex) { + if (!this.ShowGroups) + return itemIndex; + + int groupIndex = this.GroupingStrategy.GetGroup(itemIndex); + int displayIndex = 0; + for (int i = 0; i < groupIndex - 1; i++) + displayIndex += this.OLVGroups[i].VirtualItemCount; + displayIndex += this.GroupingStrategy.GetIndexWithinGroup(this.OLVGroups[groupIndex], itemIndex); + + return displayIndex; + } + + /// + /// Return the last item in the order they are shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + public override OLVListItem GetLastItemInDisplayOrder() { + if (!this.ShowGroups) + return base.GetLastItemInDisplayOrder(); + + if (this.OLVGroups.Count > 0) { + OLVGroup lastGroup = this.OLVGroups[this.OLVGroups.Count - 1]; + if (lastGroup.VirtualItemCount > 0) + return this.GetItem(this.GroupingStrategy.GetGroupMember(lastGroup, lastGroup.VirtualItemCount - 1)); + } + + return null; + } + + /// + /// Return the n'th item (0-based) in the order they are shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + /// + public override OLVListItem GetNthItemInDisplayOrder(int n) { + if (!this.ShowGroups || this.OLVGroups == null || this.OLVGroups.Count == 0) + return this.GetItem(n); + + foreach (OLVGroup group in this.OLVGroups) { + if (n < group.VirtualItemCount) + return this.GetItem(this.GroupingStrategy.GetGroupMember(group, n)); + + n -= group.VirtualItemCount; + } + + return null; + } + + /// + /// Return the ListViewItem that appears immediately after the given item. + /// If the given item is null, the first item in the list will be returned. + /// Return null if the given item is the last item. + /// + /// The item that is before the item that is returned, or null + /// A OLVListItem + public override OLVListItem GetNextItem(OLVListItem itemToFind) { + if (!this.ShowGroups) + return base.GetNextItem(itemToFind); + + // Sanity + if (this.OLVGroups == null || this.OLVGroups.Count == 0) + return null; + + // If the given item is null, return the first member of the first group + if (itemToFind == null) { + return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[0], 0)); + } + + // Find where this item occurs (which group and where in that group) + int groupIndex = this.GroupingStrategy.GetGroup(itemToFind.Index); + int indexWithinGroup = this.GroupingStrategy.GetIndexWithinGroup(this.OLVGroups[groupIndex], itemToFind.Index); + + // If it's not the last member, just return the next member + if (indexWithinGroup < this.OLVGroups[groupIndex].VirtualItemCount - 1) + return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[groupIndex], indexWithinGroup + 1)); + + // The item is the last member of its group. Return the first member of the next group + // (unless there isn't a next group) + if (groupIndex < this.OLVGroups.Count - 1) + return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[groupIndex + 1], 0)); + + return null; + } + + /// + /// Return the ListViewItem that appears immediately before the given item. + /// If the given item is null, the last item in the list will be returned. + /// Return null if the given item is the first item. + /// + /// The item that is before the item that is returned + /// A ListViewItem + public override OLVListItem GetPreviousItem(OLVListItem itemToFind) { + if (!this.ShowGroups) + return base.GetPreviousItem(itemToFind); + + // Sanity + if (this.OLVGroups == null || this.OLVGroups.Count == 0) + return null; + + // If the given items is null, return the last member of the last group + if (itemToFind == null) { + OLVGroup lastGroup = this.OLVGroups[this.OLVGroups.Count - 1]; + return this.GetItem(this.GroupingStrategy.GetGroupMember(lastGroup, lastGroup.VirtualItemCount - 1)); + } + + // Find where this item occurs (which group and where in that group) + int groupIndex = this.GroupingStrategy.GetGroup(itemToFind.Index); + int indexWithinGroup = this.GroupingStrategy.GetIndexWithinGroup(this.OLVGroups[groupIndex], itemToFind.Index); + + // If it's not the first member of the group, just return the previous member + if (indexWithinGroup > 0) + return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[groupIndex], indexWithinGroup - 1)); + + // The item is the first member of its group. Return the last member of the previous group + // (if there is one) + if (groupIndex > 0) { + OLVGroup previousGroup = this.OLVGroups[groupIndex - 1]; + return this.GetItem(this.GroupingStrategy.GetGroupMember(previousGroup, previousGroup.VirtualItemCount - 1)); + } + + return null; + } + + /// + /// Make a list of groups that should be shown according to the given parameters + /// + /// + /// + protected override IList MakeGroups(GroupingParameters parms) { + if (this.GroupingStrategy == null) + return new List(); + else + return this.GroupingStrategy.GetGroups(parms); + } + + /// + /// Create a OLVListItem for given row index + /// + /// The index of the row that is needed + /// An OLVListItem + public virtual OLVListItem MakeListViewItem(int itemIndex) { + OLVListItem olvi = new OLVListItem(this.GetModelObject(itemIndex)); + this.FillInValues(olvi, olvi.RowObject); + + this.PostProcessOneRow(itemIndex, this.GetDisplayOrderOfItemIndex(itemIndex), olvi); + + if (this.HotRowIndex == itemIndex) + this.UpdateHotRow(olvi); + + return olvi; + } + + /// + /// On virtual lists, this cannot work. + /// + protected override void PostProcessRows() { + } + + /// + /// Record the change of checkstate for the given object in the model. + /// This does not update the UI -- only the model + /// + /// + /// + /// The check state that was recorded and that should be used to update + /// the control. + protected override CheckState PutCheckState(object modelObject, CheckState state) { + state = base.PutCheckState(modelObject, state); + this.CheckStateMap[modelObject] = state; + return state; + } + + /// + /// Refresh the given item in the list + /// + /// The item to refresh + public override void RefreshItem(OLVListItem olvi) { + this.ClearCachedInfo(); + this.RedrawItems(olvi.Index, olvi.Index, true); + } + + /// + /// Change the size of the list + /// + /// + protected virtual void SetVirtualListSize(int newSize) { + if (newSize < 0 || this.VirtualListSize == newSize) + return; + + int oldSize = this.VirtualListSize; + + this.ClearCachedInfo(); + + // There is a bug in .NET when a virtual ListView is cleared + // (i.e. VirtuaListSize set to 0) AND it is scrolled vertically: the scroll position + // is wrong when the list is next populated. To avoid this, before + // clearing a virtual list, we make sure the list is scrolled to the top. + // [6 weeks later] Damn this is a pain! There are cases where this can also throw exceptions! + try { + if (newSize == 0 && this.TopItemIndex > 0) + this.TopItemIndex = 0; + } + catch (Exception) { + // Ignore any failures + } + + // In strange cases, this can throw the exceptions too. The best we can do is ignore them :( + try { + this.VirtualListSize = newSize; + } + catch (ArgumentOutOfRangeException) { + // pass + } + catch (NullReferenceException) { + // pass + } + + // Tell the world that the size of the list has changed + this.OnItemsChanged(new ItemsChangedEventArgs(oldSize, this.VirtualListSize)); + } + + /// + /// Take ownership of the 'objects' collection. This separates our collection from the source. + /// + /// + /// + /// This method + /// separates the 'objects' instance variable from its source, so that any AddObject/RemoveObject + /// calls will modify our collection and not the original collection. + /// + /// + /// VirtualObjectListViews always own their collections, so this is a no-op. + /// + /// + protected override void TakeOwnershipOfObjects() { + } + + /// + /// Change the state of the control to reflect changes in filtering + /// + protected override void UpdateFiltering() { + IFilterableDataSource filterable = this.VirtualListDataSource as IFilterableDataSource; + if (filterable == null) + return; + + this.BeginUpdate(); + try { + int originalSize = this.VirtualListSize; + filterable.ApplyFilters(this.ModelFilter, this.ListFilter); + this.BuildList(); + + //// If the filtering actually did something, rebuild the groups if they are being shown + //if (originalSize != this.VirtualListSize && this.ShowGroups) + // this.BuildGroups(); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Change the size of the virtual list so that it matches its data source + /// + public virtual void UpdateVirtualListSize() { + if (this.VirtualListDataSource != null) + this.SetVirtualListSize(this.VirtualListDataSource.GetObjectCount()); + } + + #endregion + + #region Event handlers + + /// + /// Handle the CacheVirtualItems event + /// + /// + /// + protected virtual void HandleCacheVirtualItems(object sender, CacheVirtualItemsEventArgs e) { + if (this.VirtualListDataSource != null) + this.VirtualListDataSource.PrepareCache(e.StartIndex, e.EndIndex); + } + + /// + /// Handle a RetrieveVirtualItem + /// + /// + /// + protected virtual void HandleRetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) { + // .NET 2.0 seems to generate a lot of these events. Before drawing *each* sub-item, + // this event is triggered 4-8 times for the same index. So we save lots of CPU time + // by caching the last result. + //System.Diagnostics.Debug.WriteLine(String.Format("HandleRetrieveVirtualItem({0})", e.ItemIndex)); + + if (this.lastRetrieveVirtualItemIndex != e.ItemIndex) { + this.lastRetrieveVirtualItemIndex = e.ItemIndex; + this.lastRetrieveVirtualItem = this.MakeListViewItem(e.ItemIndex); + } + e.Item = this.lastRetrieveVirtualItem; + } + + /// + /// Handle the SearchForVirtualList event, which is called when the user types into a virtual list + /// + /// + /// + protected virtual void HandleSearchForVirtualItem(object sender, SearchForVirtualItemEventArgs e) { + // The event has e.IsPrefixSearch, but as far as I can tell, this is always false (maybe that's different under Vista) + // So we ignore IsPrefixSearch and IsTextSearch and always to a case insensitive prefix match. + + // We can't do anything if we don't have a data source + if (this.VirtualListDataSource == null) + return; + + // Where should we start searching? If the last row is focused, the SearchForVirtualItemEvent starts searching + // from the next row, which is actually an invalidate index -- so we make sure we never go past the last object. + int start = Math.Min(e.StartIndex, this.VirtualListDataSource.GetObjectCount() - 1); + + // Give the world a chance to fiddle with or completely avoid the searching process + BeforeSearchingEventArgs args = new BeforeSearchingEventArgs(e.Text, start); + this.OnBeforeSearching(args); + if (args.Canceled) + return; + + // Do the search + int i = this.FindMatchingRow(args.StringToFind, args.StartSearchFrom, e.Direction); + + // Tell the world that a search has occurred + AfterSearchingEventArgs args2 = new AfterSearchingEventArgs(args.StringToFind, i); + this.OnAfterSearching(args2); + + // If we found a match, tell the event + if (i != -1) + e.Index = i; + } + + /// + /// Handle the VirtualItemsSelectionRangeChanged event, which is called "when the selection state of a range of items has changed" + /// + /// + /// + /// This method is not called whenever the selection changes on a virtual list. It only seems to be triggered when + /// the user uses Shift-Ctrl-Click to change the selection + protected virtual void HandleVirtualItemsSelectionRangeChanged(object sender, ListViewVirtualItemsSelectionRangeChangedEventArgs e) { + // System.Diagnostics.Debug.WriteLine(string.Format("HandleVirtualItemsSelectionRangeChanged: {0}->{1}, selected: {2}", e.StartIndex, e.EndIndex, e.IsSelected)); + this.TriggerDeferredSelectionChangedEvent(); + } + + /// + /// Find the first row in the given range of rows that prefix matches the string value of the given column. + /// + /// + /// + /// + /// + /// The index of the matched row, or -1 + protected override int FindMatchInRange(string text, int first, int last, OLVColumn column) { + return this.VirtualListDataSource.SearchText(text, first, last, column); + } + + #endregion + + #region Variable declarations + + private OLVListItem lastRetrieveVirtualItem; + private int lastRetrieveVirtualItemIndex = -1; + + #endregion + } +} diff --git a/ObjectListView/olv-keyfile.snk b/ObjectListView/olv-keyfile.snk new file mode 100644 index 0000000000000000000000000000000000000000..2658a0adcf122aaa2a591535b53464d516af3378 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa500986W$cIN!~W%bg8iKqt=v+03$~e~mJs!s zjxU8c(azlkAM^?lBdSg#@Xo86g5b(px(_u*|NRInNPMa(oZ7FOGX3y+S9*R?1vg*78kM6LjIK||X zWAU4blpG9GP}*;3w5@>NY`@4N-#_U$uE!7hH-#IbZu}s16Lpq{mQ;i43=-|@nVZDs zG|+jdK4Cm@V#c*g=H%5QNh=MIIeg0)t4qN7H3-6RuS70^Bde3q0o`&}OfWbuURm=Y zn5*%bskV-f%^ClA~ zN7~lW00)-QecLE7reqRZ{s)OjgHOfCAWI2#-kPm(z2TWw^;P~&q24T=-IOLtj~kL+ zUJ}~BbjP&wIAX literal 0 HcmV?d00001 diff --git a/Sanford.Multimedia.Midi.Core/Icon_-_General_MIDI.png b/Sanford.Multimedia.Midi.Core/Icon_-_General_MIDI.png new file mode 100644 index 0000000000000000000000000000000000000000..8c91a450ef4a9a1e28fca3e3c9b0c5333ee7828a GIT binary patch literal 928 zcmV;R17G}!P)H9LIl`2ieq#FUCMnCi6|fuG^Tj$#jkhhK~87qgb$`?I2~&jTY>DD5a*^WVB@u ztyP>-i^y0DrQ0f&8A=~EWwkaZ>BQMVW5R5^SsP=UG*6e@efLjxgyfFU9r|J~^5Kqe ze*gd7hx_rnkP!YO3PE0w7vu%K)|#F$;HhGK5Omcq02h9M_ia`-++D6?@mUzaeAR6L zy#q=??7iYKcXLe)Khq_*`%dCpY69v$$7Jy&D+TY)@7*&8+5mh>C&6|;R0_s6US*a~ z{BY8*U|cSK?FQcM!BpuA7@pTXK-W<~C~vhyfa4!Q20BVHy|S0?%)j+CD?V*$8l=RQ zqwO(+MEW_qt??ou=J~_LmlI70jnXyBL>5Q@b=WNlR=lvaXfu&e{!F(KgeMD%wm(P4 zf4WYzPw0sTd}sWFlm$|J_tXf0vJjl~I~e^3FjKn!8%*sBgW<8v0mn{ZZjb$R%dQ0+ zKcSq0P+558TIcgoOj4)A@uSdJ3UqTln0&F<;TT)?nr;E7kKlX-AX>UnU+!>RU(FOC z&~~U{d{D$j>hum&_%V{!BqTDdGCzNv0icjtD?C(60Eag?&aM)_Ct!1aR_TWZRV#p9wRR1&- zC3>Z0_EIRV1#S8wUHtuqkJ6}H5Gf|IkVwkaSOU48Kt``uPlJ4&Oj}Y$YSaQrA~h?g zSXgri6jR_hQUc;0V_~F+5&)z`R)RD1b^@J#q>}F-HrTyt+A44=rDU1j$+A9vq&{n(km4^V&+|S6 zT`U$5xykcRFJe54dk}u%(>Pu82|)5VdFL(R|M_(`$Wxltkv60aJB%4)?0=uMQaHLv zF$L{BQi3>XCXCGQ!nP{X%CqZJ@u>XG%M0>?yx=}%I7Hi=jeU0j0000 + + + Sanford.Multimedia.Midi + 6.1.2 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme + Leslie Sanford + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + false + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Merged github pull request: +Can now load midi files from a stream +Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.2.0.nuspec b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.2.0.nuspec new file mode 100644 index 00000000..457c1aa1 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.2.0.nuspec @@ -0,0 +1,33 @@ + + + + Sanford.Multimedia.Midi + 6.2.0 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme,Andres Fernandez de Prado + Leslie Sanford + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + false + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Merged github pull request: +Fixed many P/Invoke signatures +Can now load midi files from a stream +Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.2.1.nuspec b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.2.1.nuspec new file mode 100644 index 00000000..691f4fe3 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.2.1.nuspec @@ -0,0 +1,33 @@ + + + + Sanford.Multimedia.Midi + 6.2.1 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme,Andres Fernandez de Prado + Leslie Sanford + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + false + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Merged github pull request: +Fixed many P/Invoke signatures +Can now load midi files from a stream +Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.3.0.nuspec b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.3.0.nuspec new file mode 100644 index 00000000..f9fb56a5 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.3.0.nuspec @@ -0,0 +1,31 @@ + + + + Sanford.Multimedia.Midi + 6.3.0 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme,Andres Fernandez de Prado + Leslie Sanford + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + false + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Added source/sink interface for events +Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.0.nuspec b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.0.nuspec new file mode 100644 index 00000000..af998aac --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.0.nuspec @@ -0,0 +1,31 @@ + + + + Sanford.Multimedia.Midi + 6.4.0 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme,Andres Fernandez de Prado + Leslie Sanford + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + false + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Reworked messages and events a bit. there is now an event for all messages as well as all ShortMessage (was Raw and might break some code if you used it before). +General Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.1.nuspec b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.1.nuspec new file mode 100644 index 00000000..8700266e --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.1.nuspec @@ -0,0 +1,31 @@ + + + + Sanford.Multimedia.Midi + 6.4.1 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme,Andres Fernandez de Prado + Leslie Sanford + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + false + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Minor dispose fix. Reworked messages and events a bit. there is now an event for all messages as well as all ShortMessage (was Raw and might break some code if you used it before). +General Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.2.nuspec b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.2.nuspec new file mode 100644 index 00000000..c2f74b6b --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.2.nuspec @@ -0,0 +1,31 @@ + + + + Sanford.Multimedia.Midi + 6.4.2 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme,Andres Fernandez de Prado + Leslie Sanford + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + false + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Another fix for some drivers that send weird sysex messages on close. Reworked messages and events a bit. there is now an event for all messages as well as all ShortMessage (was Raw and might break some code if you used it before). +General Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.3.nuspec b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.3.nuspec new file mode 100644 index 00000000..200e65d9 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.4.3.nuspec @@ -0,0 +1,31 @@ + + + + Sanford.Multimedia.Midi + 6.4.3 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme,Andres Fernandez de Prado + Leslie Sanford + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + false + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Another fix for some drivers that send weird sysex messages on close. Reworked messages and events a bit. there is now an event for all messages as well as all ShortMessage (was Raw and might break some code if you used it before). +General Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.5.0.nuspec b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.5.0.nuspec new file mode 100644 index 00000000..8a5491bf --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.5.0.nuspec @@ -0,0 +1,32 @@ + + + + Sanford.Multimedia.Midi + 6.5.0 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme,Andres Fernandez de Prado + Leslie Sanford + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + false + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Fixed PPQM exception, now then only requirement for midi files is PPQM > 24 +Another fix for some drivers that send weird sysex messages on close. Reworked messages and events a bit. there is now an event for all messages as well as all ShortMessage (was Raw and might break some code if you used it before). +General Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.6.0.nuspec b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.6.0.nuspec new file mode 100644 index 00000000..86755b30 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Nuget/Sanford.Multimedia.Midi.6.6.0.nuspec @@ -0,0 +1,33 @@ + + + + Sanford.Multimedia.Midi + 6.6.0 + MIDI Toolkit + Leslie Sanford,Tebjan Halm,Andreas Grimme,Andres Fernandez de Prado + Leslie Sanford + false + https://opensource.org/licenses/MIT + http://www.codeproject.com/Articles/6228/C-MIDI-Toolkit + https://raw.githubusercontent.com/tebjan/Sanford.Multimedia.Midi/master/Source/Icon_-_General_MIDI.png + A toolkit for creating MIDI applications with .NET +Source code on github: https://github.com/tebjan/Sanford.Multimedia.Midi + A toolkit for creating MIDI applications with .NET + Midi messages now have the input driver timestamp attached. +Fixed PPQM exception, now then only requirement for midi files is PPQM > 24 +Another fix for some drivers that send weird sysex messages on close. Reworked messages and events a bit. there is now an event for all messages as well as all ShortMessage (was Raw and might break some code if you used it before). +General Improvements: +Now also works on mono +64-bit compatible +Windows 8 and 10 compatible +Does not require additional assemblies +Faster midi file reading in Release build + Leslie Sanford 2006 + en + + + + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Deque.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Deque.cs new file mode 100644 index 00000000..814f8fec --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Deque.cs @@ -0,0 +1,935 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; +using System.Diagnostics; + +namespace Sanford.Collections +{ + /// + /// Represents a simple double-ended-queue collection of objects. + /// + [Serializable()] + public class Deque : ICollection, IEnumerable, ICloneable + { + #region Deque Members + + #region Fields + + // The node at the front of the deque. + private Node front = null; + + // The node at the back of the deque. + private Node back = null; + + // The number of elements in the deque. + private int count = 0; + + // The version of the deque. + private long version = 0; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the Deque class. + /// + public Deque() + { + } + + /// + /// Initializes a new instance of the Deque class that contains + /// elements copied from the specified collection. + /// + /// + /// The ICollection to copy elements from. + /// + public Deque(ICollection col) + { + #region Require + + if(col == null) + { + throw new ArgumentNullException("col"); + } + + #endregion + + foreach(object obj in col) + { + PushBack(obj); + } + } + + #endregion + + #region Methods + + /// + /// Removes all objects from the Deque. + /// + public virtual void Clear() + { + count = 0; + + front = back = null; + + version++; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Determines whether or not an element is in the Deque. + /// + /// + /// The Object to locate in the Deque. + /// + /// + /// true if obj if found in the Deque; otherwise, + /// false. + /// + public virtual bool Contains(object obj) + { + foreach(object o in this) + { + if(o == null && obj == null) + { + return true; + } + else if(o.Equals(obj)) + { + return true; + } + } + + return false; + } + + /// + /// Inserts an object at the front of the Deque. + /// + /// + /// The object to push onto the deque; + /// + public virtual void PushFront(object obj) + { + // The new node to add to the front of the deque. + Node newNode = new Node(obj); + + // Link the new node to the front node. The current front node at + // the front of the deque is now the second node in the deque. + newNode.Next = front; + + // If the deque isn't empty. + if(Count > 0) + { + // Link the current front to the new node. + front.Previous = newNode; + } + + // Make the new node the front of the deque. + front = newNode; + + // Keep track of the number of elements in the deque. + count++; + + // If this is the first element in the deque. + if(Count == 1) + { + // The front and back nodes are the same. + back = front; + } + + version++; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Inserts an object at the back of the Deque. + /// + /// + /// The object to push onto the deque; + /// + public virtual void PushBack(object obj) + { + // The new node to add to the back of the deque. + Node newNode = new Node(obj); + + // Link the new node to the back node. The current back node at + // the back of the deque is now the second to the last node in the + // deque. + newNode.Previous = back; + + // If the deque is not empty. + if(Count > 0) + { + // Link the current back node to the new node. + back.Next = newNode; + } + + // Make the new node the back of the deque. + back = newNode; + + // Keep track of the number of elements in the deque. + count++; + + // If this is the first element in the deque. + if(Count == 1) + { + // The front and back nodes are the same. + front = back; + } + + version++; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Removes and returns the object at the front of the Deque. + /// + /// + /// The object at the front of the Deque. + /// + /// + /// The Deque is empty. + /// + public virtual object PopFront() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException("Deque is empty."); + } + + #endregion + + // Get the object at the front of the deque. + object obj = front.Value; + + // Move the front back one node. + front = front.Next; + + // Keep track of the number of nodes in the deque. + count--; + + // If the deque is not empty. + if(Count > 0) + { + // Tie off the previous link in the front node. + front.Previous = null; + } + // Else the deque is empty. + else + { + // Indicate that there is no back node. + back = null; + } + + version++; + + #region Invariant + + AssertValid(); + + #endregion + + return obj; + } + + /// + /// Removes and returns the object at the back of the Deque. + /// + /// + /// The object at the back of the Deque. + /// + /// + /// The Deque is empty. + /// + public virtual object PopBack() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException("Deque is empty."); + } + + #endregion + + // Get the object at the back of the deque. + object obj = back.Value; + + // Move back node forward one node. + back = back.Previous; + + // Keep track of the number of nodes in the deque. + count--; + + // If the deque is not empty. + if(Count > 0) + { + // Tie off the next link in the back node. + back.Next = null; + } + // Else the deque is empty. + else + { + // Indicate that there is no front node. + front = null; + } + + version++; + + #region Invariant + + AssertValid(); + + #endregion + + return obj; + } + + /// + /// Returns the object at the front of the Deque without removing it. + /// + /// + /// The object at the front of the Deque. + /// + /// + /// The Deque is empty. + /// + public virtual object PeekFront() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException("Deque is empty."); + } + + #endregion + + return front.Value; + } + + /// + /// Returns the object at the back of the Deque without removing it. + /// + /// + /// The object at the back of the Deque. + /// + /// + /// The Deque is empty. + /// + public virtual object PeekBack() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException("Deque is empty."); + } + + #endregion + + return back.Value; + } + + /// + /// Copies the Deque to a new array. + /// + /// + /// A new array containing copies of the elements of the Deque. + /// + public virtual object[] ToArray() + { + object[] array = new object[Count]; + int index = 0; + + foreach(object obj in this) + { + array[index] = obj; + index++; + } + + return array; + } + + /// + /// Returns a synchronized (thread-safe) wrapper for the Deque. + /// + /// + /// The Deque to synchronize. + /// + /// + /// A synchronized wrapper around the Deque. + /// + public static Deque Synchronized(Deque deque) + { + #region Require + + if(deque == null) + { + throw new ArgumentNullException("deque"); + } + + #endregion + + return new SynchronizedDeque(deque); + } + + [Conditional("DEBUG")] + private void AssertValid() + { + int n = 0; + Node current = front; + + while(current != null) + { + n++; + current = current.Next; + } + + Debug.Assert(n == Count); + + if(Count > 0) + { + Debug.Assert(front != null && back != null, "Front/Back Null Test - Count > 0"); + + Node f = front; + Node b = back; + + while(f.Next != null && b.Previous != null) + { + f = f.Next; + b = b.Previous; + } + + Debug.Assert(f.Next == null && b.Previous == null, "Front/Back Termination Test"); + Debug.Assert(f == back && b == front, "Front/Back Equality Test"); + } + else + { + Debug.Assert(front == null && back == null, "Front/Back Null Test - Count == 0"); + } + } + + #endregion + + #region Node Class + + // Represents a node in the deque. + [Serializable()] + private class Node + { + private object value; + + private Node previous = null; + + private Node next = null; + + public Node(object value) + { + this.value = value; + } + + public object Value + { + get + { + return value; + } + } + + public Node Previous + { + get + { + return previous; + } + set + { + previous = value; + } + } + + public Node Next + { + get + { + return next; + } + set + { + next = value; + } + } + } + + #endregion + + #region DequeEnumerator Class + + [Serializable()] + private class DequeEnumerator : IEnumerator + { + private Deque owner; + + private Node currentNode; + + private object current = null; + + private bool moveResult = false; + + private long version; + + public DequeEnumerator(Deque owner) + { + this.owner = owner; + currentNode = owner.front; + this.version = owner.version; + } + + #region IEnumerator Members + + public void Reset() + { + #region Require + + if(version != owner.version) + { + throw new InvalidOperationException( + "The Deque was modified after the enumerator was created."); + } + + #endregion + + currentNode = owner.front; + moveResult = false; + } + + public object Current + { + get + { + #region Require + + if(!moveResult) + { + throw new InvalidOperationException( + "The enumerator is positioned before the first " + + "element of the Deque or after the last element."); + } + + #endregion + + return current; + } + } + + public bool MoveNext() + { + #region Require + + if(version != owner.version) + { + throw new InvalidOperationException( + "The Deque was modified after the enumerator was created."); + } + + #endregion + + if(currentNode != null) + { + current = currentNode.Value; + currentNode = currentNode.Next; + + moveResult = true; + } + else + { + moveResult = false; + } + + return moveResult; + } + + #endregion + } + + #endregion + + #region SynchronizedDeque Class + + // Implements a synchronization wrapper around a deque. + [Serializable()] + private class SynchronizedDeque : Deque + { + #region SynchronziedDeque Members + + #region Fields + + // The wrapped deque. + private Deque deque; + + // The object to lock on. + private object root; + + #endregion + + #region Construction + + public SynchronizedDeque(Deque deque) + { + #region Require + + if(deque == null) + { + throw new ArgumentNullException("deque"); + } + + #endregion + + this.deque = deque; + this.root = deque.SyncRoot; + } + + #endregion + + #region Methods + + public override void Clear() + { + lock(root) + { + deque.Clear(); + } + } + + public override bool Contains(object obj) + { + bool result; + + lock(root) + { + result = deque.Contains(obj); + } + + return result; + } + + public override void PushFront(object obj) + { + lock(root) + { + deque.PushFront(obj); + } + } + + public override void PushBack(object obj) + { + lock(root) + { + deque.PushBack(obj); + } + } + + public override object PopFront() + { + object obj; + + lock(root) + { + obj = deque.PopFront(); + } + + return obj; + } + + public override object PopBack() + { + object obj; + + lock(root) + { + obj = deque.PopBack(); + } + + return obj; + } + + public override object PeekFront() + { + object obj; + + lock(root) + { + obj = deque.PeekFront(); + } + + return obj; + } + + public override object PeekBack() + { + object obj; + + lock(root) + { + obj = deque.PeekBack(); + } + + return obj; + } + + public override object[] ToArray() + { + object[] array; + + lock(root) + { + array = deque.ToArray(); + } + + return array; + } + + public override object Clone() + { + object clone; + + lock(root) + { + clone = deque.Clone(); + } + + return clone; + } + + public override void CopyTo(Array array, int index) + { + lock(root) + { + deque.CopyTo(array, index); + } + } + + public override IEnumerator GetEnumerator() + { + IEnumerator e; + + lock(root) + { + e = deque.GetEnumerator(); + } + + return e; + } + + #endregion + + #region Properties + + public override int Count + { + get + { + lock(root) + { + return deque.Count; + } + } + } + + public override bool IsSynchronized + { + get + { + return true; + } + } + + #endregion + + #endregion + } + + #endregion + + #endregion + + #region ICollection Members + + /// + /// Gets a value indicating whether access to the Deque is synchronized + /// (thread-safe). + /// + public virtual bool IsSynchronized + { + get + { + return false; + } + } + + /// + /// Gets the number of elements contained in the Deque. + /// + public virtual int Count + { + get + { + return count; + } + } + + /// + /// Copies the Deque elements to an existing one-dimensional Array, + /// starting at the specified array index. + /// + /// + /// The one-dimensional Array that is the destination of the elements + /// copied from Deque. The Array must have zero-based indexing. + /// + /// + /// The zero-based index in array at which copying begins. + /// + public virtual void CopyTo(Array array, int index) + { + #region Require + + if(array == null) + { + throw new ArgumentNullException("array"); + } + else if(index < 0) + { + throw new ArgumentOutOfRangeException("index", index, + "Index is less than zero."); + } + else if(array.Rank > 1) + { + throw new ArgumentException("Array is multidimensional."); + } + else if(index >= array.Length) + { + throw new ArgumentException("Index is equal to or greater " + + "than the length of array."); + } + else if(Count > array.Length - index) + { + throw new ArgumentException( + "The number of elements in the source Deque is greater " + + "than the available space from index to the end of the " + + "destination array."); + } + + #endregion + + int i = index; + + foreach(object obj in this) + { + array.SetValue(obj, i); + i++; + } + } + + /// + /// Gets an object that can be used to synchronize access to the Deque. + /// + public virtual object SyncRoot + { + get + { + return this; + } + } + + #endregion + + #region IEnumerable Members + + /// + /// Returns an enumerator that can iterate through the Deque. + /// + /// + /// An IEnumerator for the Deque. + /// + public virtual IEnumerator GetEnumerator() + { + return new DequeEnumerator(this); + } + + #endregion + + #region ICloneable Members + + /// + /// Creates a shallow copy of the Deque. + /// + /// + /// A shallow copy of the Deque. + /// + public virtual object Clone() + { + Deque clone = new Deque(this); + + clone.version = this.version; + + return clone; + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Enumerator.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Enumerator.cs new file mode 100644 index 00000000..5f01e34d --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Enumerator.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Collections.Generic +{ + public partial class Deque + { + #region Enumerator Class + + [Serializable()] + private class Enumerator : IEnumerator + { + private Deque owner; + + private Node currentNode; + + private T current = default(T); + + private bool moveResult = false; + + private long version; + + // A value indicating whether the enumerator has been disposed. + private bool disposed = false; + + public Enumerator(Deque owner) + { + this.owner = owner; + currentNode = owner.front; + this.version = owner.version; + } + + #region IEnumerator Members + + public void Reset() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + else if(version != owner.version) + { + throw new InvalidOperationException( + "The Deque was modified after the enumerator was created."); + } + + #endregion + + currentNode = owner.front; + moveResult = false; + } + + public object Current + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + else if(!moveResult) + { + throw new InvalidOperationException( + "The enumerator is positioned before the first " + + "element of the Deque or after the last element."); + } + + #endregion + + return current; + } + } + + public bool MoveNext() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + else if(version != owner.version) + { + throw new InvalidOperationException( + "The Deque was modified after the enumerator was created."); + } + + #endregion + + if(currentNode != null) + { + current = currentNode.Value; + currentNode = currentNode.Next; + + moveResult = true; + } + else + { + moveResult = false; + } + + return moveResult; + } + + #endregion + + #region IEnumerator Members + + T IEnumerator.Current + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + else if(!moveResult) + { + throw new InvalidOperationException( + "The enumerator is positioned before the first " + + "element of the Deque or after the last element."); + } + + #endregion + + return current; + } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + disposed = true; + } + + #endregion + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Node.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Node.cs new file mode 100644 index 00000000..aa80fccb --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Node.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Collections.Generic +{ + public partial class Deque + { + #region Node Class + + // Represents a node in the deque. + [Serializable()] + private class Node + { + private T value; + + private Node previous = null; + + private Node next = null; + + public Node(T value) + { + this.value = value; + } + + public T Value + { + get + { + return value; + } + } + + public Node Previous + { + get + { + return previous; + } + set + { + previous = value; + } + } + + public Node Next + { + get + { + return next; + } + set + { + next = value; + } + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Synchronized.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Synchronized.cs new file mode 100644 index 00000000..a68249ae --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.Synchronized.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Sanford.Collections.Generic +{ + public partial class Deque + { + #region SynchronizedDeque Class + + // Implements a synchronization wrapper around a deque. + [Serializable()] + private class SynchronizedDeque : Deque, IEnumerable + { + #region SynchronziedDeque Members + + #region Fields + + // The wrapped deque. + private Deque deque; + + // The object to lock on. + private object root; + + #endregion + + #region Construction + + public SynchronizedDeque(Deque deque) + { + #region Require + + if(deque == null) + { + throw new ArgumentNullException("deque"); + } + + #endregion + + this.deque = deque; + this.root = deque.SyncRoot; + } + + #endregion + + #region Methods + + public override void Clear() + { + lock(root) + { + deque.Clear(); + } + } + + public override bool Contains(T item) + { + lock(root) + { + return deque.Contains(item); + } + } + + public override void PushFront(T item) + { + lock(root) + { + deque.PushFront(item); + } + } + + public override void PushBack(T item) + { + lock(root) + { + deque.PushBack(item); + } + } + + public override T PopFront() + { + lock(root) + { + return deque.PopFront(); + } + } + + public override T PopBack() + { + lock(root) + { + return deque.PopBack(); + } + } + + public override T PeekFront() + { + lock(root) + { + return deque.PeekFront(); + } + } + + public override T PeekBack() + { + lock(root) + { + return deque.PeekBack(); + } + } + + public override T[] ToArray() + { + lock(root) + { + return deque.ToArray(); + } + } + + public override object Clone() + { + lock(root) + { + return deque.Clone(); + } + } + + public override void CopyTo(Array array, int index) + { + lock(root) + { + deque.CopyTo(array, index); + } + } + + public override IEnumerator GetEnumerator() + { + lock(root) + { + return deque.GetEnumerator(); + } + } + + /// + /// Returns an enumerator that can iterate through the Deque. + /// + /// + /// An IEnumerator for the Deque. + /// + IEnumerator IEnumerable.GetEnumerator() + { + lock(root) + { + return ((IEnumerable)deque).GetEnumerator(); + } + } + + #endregion + + #region Properties + + public override int Count + { + get + { + lock(root) + { + return deque.Count; + } + } + } + + public override bool IsSynchronized + { + get + { + return true; + } + } + + #endregion + + #endregion + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.cs new file mode 100644 index 00000000..a472f87e --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.cs @@ -0,0 +1,601 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Sanford.Collections.Generic +{ + /// + /// Represents a simple double-ended-queue collection of objects. + /// + [Serializable()] + public partial class Deque : ICollection, IEnumerable, ICloneable + { + #region Deque Members + + #region Fields + + // The node at the front of the deque. + private Node front = null; + + // The node at the back of the deque. + private Node back = null; + + // The number of elements in the deque. + private int count = 0; + + // The version of the deque. + private long version = 0; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the Deque class. + /// + public Deque() + { + } + + /// + /// Initializes a new instance of the Deque class that contains + /// elements copied from the specified collection. + /// + /// + /// The collection whose elements are copied to the new Deque. + /// + public Deque(IEnumerable collection) + { + #region Require + + if(collection == null) + { + throw new ArgumentNullException("col"); + } + + #endregion + + foreach(T item in collection) + { + PushBack(item); + } + } + + #endregion + + #region Methods + + /// + /// Removes all objects from the Deque. + /// + public virtual void Clear() + { + count = 0; + + front = back = null; + + version++; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Determines whether or not an element is in the Deque. + /// + /// + /// The Object to locate in the Deque. + /// + /// + /// true if obj if found in the Deque; otherwise, + /// false. + /// + public virtual bool Contains(T obj) + { + foreach(T o in this) + { + if(EqualityComparer.Default.Equals(o, obj)) + { + return true; + } + } + + return false; + } + + /// + /// Inserts an object at the front of the Deque. + /// + /// + /// The object to push onto the deque; + /// + public virtual void PushFront(T item) + { + // The new node to add to the front of the deque. + Node newNode = new Node(item); + + // Link the new node to the front node. The current front node at + // the front of the deque is now the second node in the deque. + newNode.Next = front; + + // If the deque isn't empty. + if(Count > 0) + { + // Link the current front to the new node. + front.Previous = newNode; + } + + // Make the new node the front of the deque. + front = newNode; + + // Keep track of the number of elements in the deque. + count++; + + // If this is the first element in the deque. + if(Count == 1) + { + // The front and back nodes are the same. + back = front; + } + + version++; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Inserts an object at the back of the Deque. + /// + /// + /// The object to push onto the deque; + /// + public virtual void PushBack(T item) + { + // The new node to add to the back of the deque. + Node newNode = new Node(item); + + // Link the new node to the back node. The current back node at + // the back of the deque is now the second to the last node in the + // deque. + newNode.Previous = back; + + // If the deque is not empty. + if(Count > 0) + { + // Link the current back node to the new node. + back.Next = newNode; + } + + // Make the new node the back of the deque. + back = newNode; + + // Keep track of the number of elements in the deque. + count++; + + // If this is the first element in the deque. + if(Count == 1) + { + // The front and back nodes are the same. + front = back; + } + + version++; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Removes and returns the object at the front of the Deque. + /// + /// + /// The object at the front of the Deque. + /// + /// + /// The Deque is empty. + /// + public virtual T PopFront() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException("Deque is empty."); + } + + #endregion + + // Get the object at the front of the deque. + T item = front.Value; + + // Move the front back one node. + front = front.Next; + + // Keep track of the number of nodes in the deque. + count--; + + // If the deque is not empty. + if(Count > 0) + { + // Tie off the previous link in the front node. + front.Previous = null; + } + // Else the deque is empty. + else + { + // Indicate that there is no back node. + back = null; + } + + version++; + + #region Invariant + + AssertValid(); + + #endregion + + return item; + } + + /// + /// Removes and returns the object at the back of the Deque. + /// + /// + /// The object at the back of the Deque. + /// + /// + /// The Deque is empty. + /// + public virtual T PopBack() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException("Deque is empty."); + } + + #endregion + + // Get the object at the back of the deque. + T item = back.Value; + + // Move back node forward one node. + back = back.Previous; + + // Keep track of the number of nodes in the deque. + count--; + + // If the deque is not empty. + if(Count > 0) + { + // Tie off the next link in the back node. + back.Next = null; + } + // Else the deque is empty. + else + { + // Indicate that there is no front node. + front = null; + } + + version++; + + #region Invariant + + AssertValid(); + + #endregion + + return item; + } + + /// + /// Returns the object at the front of the Deque without removing it. + /// + /// + /// The object at the front of the Deque. + /// + /// + /// The Deque is empty. + /// + public virtual T PeekFront() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException("Deque is empty."); + } + + #endregion + + return front.Value; + } + + /// + /// Returns the object at the back of the Deque without removing it. + /// + /// + /// The object at the back of the Deque. + /// + /// + /// The Deque is empty. + /// + public virtual T PeekBack() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException("Deque is empty."); + } + + #endregion + + return back.Value; + } + + /// + /// Copies the Deque to a new array. + /// + /// + /// A new array containing copies of the elements of the Deque. + /// + public virtual T[] ToArray() + { + T[] array = new T[Count]; + int index = 0; + + foreach(T item in this) + { + array[index] = item; + index++; + } + + return array; + } + + /// + /// Returns a synchronized (thread-safe) wrapper for the Deque. + /// + /// + /// The Deque to synchronize. + /// + /// + /// A synchronized wrapper around the Deque. + /// + public static Deque Synchronized(Deque deque) + { + #region Require + + if(deque == null) + { + throw new ArgumentNullException("deque"); + } + + #endregion + + return new SynchronizedDeque(deque); + } + + [Conditional("DEBUG")] + private void AssertValid() + { + int n = 0; + Node current = front; + + while(current != null) + { + n++; + current = current.Next; + } + + Debug.Assert(n == Count); + + if(Count > 0) + { + Debug.Assert(front != null && back != null, "Front/Back Null Test - Count > 0"); + + Node f = front; + Node b = back; + + while(f.Next != null && b.Previous != null) + { + f = f.Next; + b = b.Previous; + } + + Debug.Assert(f.Next == null && b.Previous == null, "Front/Back Termination Test"); + Debug.Assert(f == back && b == front, "Front/Back Equality Test"); + } + else + { + Debug.Assert(front == null && back == null, "Front/Back Null Test - Count == 0"); + } + } + + #endregion + + #endregion + + #region ICollection Members + + /// + /// Gets a value indicating whether access to the Deque is synchronized + /// (thread-safe). + /// + public virtual bool IsSynchronized + { + get + { + return false; + } + } + + /// + /// Gets the number of elements contained in the Deque. + /// + public virtual int Count + { + get + { + return count; + } + } + + /// + /// Copies the Deque elements to an existing one-dimensional Array, + /// starting at the specified array index. + /// + /// + /// The one-dimensional Array that is the destination of the elements + /// copied from Deque. The Array must have zero-based indexing. + /// + /// + /// The zero-based index in array at which copying begins. + /// + public virtual void CopyTo(Array array, int index) + { + #region Require + + if(array == null) + { + throw new ArgumentNullException("array"); + } + else if(index < 0) + { + throw new ArgumentOutOfRangeException("index", index, + "Index is less than zero."); + } + else if(array.Rank > 1) + { + throw new ArgumentException("Array is multidimensional."); + } + else if(index >= array.Length) + { + throw new ArgumentException("Index is equal to or greater " + + "than the length of array."); + } + else if(Count > array.Length - index) + { + throw new ArgumentException( + "The number of elements in the source Deque is greater " + + "than the available space from index to the end of the " + + "destination array."); + } + + #endregion + + int i = index; + + foreach(object obj in this) + { + array.SetValue(obj, i); + i++; + } + } + + /// + /// Gets an object that can be used to synchronize access to the Deque. + /// + public virtual object SyncRoot + { + get + { + return this; + } + } + + #endregion + + #region IEnumerable Members + + /// + /// Returns an enumerator that can iterate through the Deque. + /// + /// + /// An IEnumerator for the Deque. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this); + } + + #endregion + + #region ICloneable Members + + /// + /// Creates a shallow copy of the Deque. + /// + /// + /// A shallow copy of the Deque. + /// + public virtual object Clone() + { + Deque clone = new Deque(this); + + clone.version = this.version; + + return clone; + } + + #endregion + + #region IEnumerable Members + + public virtual IEnumerator GetEnumerator() + { + return new Enumerator(this); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/ICommand.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/ICommand.cs new file mode 100644 index 00000000..8a74f1e1 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/ICommand.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Collections.Generic +{ + internal interface ICommand + { + void Execute(); + void Undo(); + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoManager.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoManager.cs new file mode 100644 index 00000000..4910b72f --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoManager.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; + +namespace Sanford.Collections.Generic +{ + internal class UndoManager + { + private Stack undoStack = new Stack(); + + private Stack redoStack = new Stack(); + + #region Methods + + public void Execute(ICommand command) + { + command.Execute(); + + undoStack.Push(command); + redoStack.Clear(); + } + + /// + /// Undoes the last operation. + /// + /// + /// true if the last operation was undone, false if there + /// are no more operations left to undo. + /// + public bool Undo() + { + #region Guard + + if(undoStack.Count == 0) + { + return false; + } + + #endregion + + ICommand command = undoStack.Pop(); + + command.Undo(); + + redoStack.Push(command); + + return true; + } + + /// + /// Redoes the last operation. + /// + /// + /// true if the last operation was redone, false if there + /// are no more operations left to redo. + /// + public bool Redo() + { + #region Guard + + if(redoStack.Count == 0) + { + return false; + } + + #endregion + + ICommand command = redoStack.Pop(); + + command.Execute(); + + undoStack.Push(command); + + return true; + } + + /// + /// Clears the undo/redo history. + /// + public void ClearHistory() + { + undoStack.Clear(); + redoStack.Clear(); + } + + #endregion + + #region Properties + + /// + /// The number of operations left to undo. + /// + public int UndoCount + { + get + { + return undoStack.Count; + } + } + + /// + /// The number of operations left to redo. + /// + public int RedoCount + { + get + { + return redoStack.Count; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Commands.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Commands.cs new file mode 100644 index 00000000..e54f997b --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Commands.cs @@ -0,0 +1,511 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Sanford.Collections.Generic +{ + public partial class UndoableList : IList + { + + #region SetCommand + + private class SetCommand : ICommand + { + private IList theList; + + private int index; + + private T oldItem; + + private T newItem; + + private bool undone = true; + + public SetCommand(IList theList, int index, T item) + { + this.theList = theList; + this.index = index; + this.newItem = item; + } + + #region ICommand Members + + public void Execute() + { + #region Guard + + if(!undone) + { + return; + } + + #endregion + + Debug.Assert(index >= 0 && index < theList.Count); + + oldItem = theList[index]; + theList[index] = newItem; + undone = false; + } + + public void Undo() + { + #region Guard + + if(undone) + { + return; + } + + #endregion + + Debug.Assert(index >= 0 && index < theList.Count); + Debug.Assert(theList[index].Equals(newItem)); + + theList[index] = oldItem; + undone = true; + } + + #endregion + } + + #endregion + + #region InsertCommand + + private class InsertCommand : ICommand + { + private IList theList; + + private int index; + + private T item; + + private bool undone = true; + + private int count; + + public InsertCommand(IList theList, int index, T item) + { + this.theList = theList; + this.index = index; + this.item = item; + } + + #region ICommand Members + + public void Execute() + { + #region Guard + + if(!undone) + { + return; + } + + #endregion + + Debug.Assert(index >= 0 && index <= theList.Count); + + count = theList.Count; + theList.Insert(index, item); + undone = false; + } + + public void Undo() + { + #region Guard + + if(undone) + { + return; + } + + #endregion + + Debug.Assert(index >= 0 && index <= theList.Count); + Debug.Assert(theList[index].Equals(item)); + + theList.RemoveAt(index); + undone = true; + + Debug.Assert(theList.Count == count); + } + + #endregion + } + + #endregion + + #region InsertRangeCommand + + private class InsertRangeCommand : ICommand + { + private List theList; + + private int index; + + private List insertList; + + private bool undone = true; + + public InsertRangeCommand(List theList, int index, IEnumerable collection) + { + this.theList = theList; + this.index = index; + + insertList = new List(collection); + } + + #region ICommand Members + + public void Execute() + { + #region Guard + + if(!undone) + { + return; + } + + #endregion + + Debug.Assert(index >= 0 && index <= theList.Count); + + theList.InsertRange(index, insertList); + + undone = false; + } + + public void Undo() + { + #region Guard + + if(undone) + { + return; + } + + #endregion + + Debug.Assert(index >= 0 && index <= theList.Count); + + theList.RemoveRange(index, insertList.Count); + + undone = true; + } + + #endregion + } + + #endregion + + #region RemoveAtCommand + + private class RemoveAtCommand : ICommand + { + private IList theList; + + private int index; + + private T item; + + private bool undone = true; + + private int count; + + public RemoveAtCommand(IList theList, int index) + { + this.theList = theList; + this.index = index; + } + + #region ICommand Members + + public void Execute() + { + #region Guard + + if(!undone) + { + return; + } + + #endregion + + Debug.Assert(index >= 0 && index < theList.Count); + + item = theList[index]; + count = theList.Count; + theList.RemoveAt(index); + undone = false; + } + + public void Undo() + { + #region Guard + + if(undone) + { + return; + } + + #endregion + + Debug.Assert(index >= 0 && index < theList.Count); + + theList.Insert(index, item); + undone = true; + + Debug.Assert(theList.Count == count); + } + + #endregion + } + + #endregion + + #region RemoveRangeCommand + + private class RemoveRangeCommand : ICommand + { + private List theList; + + private int index; + + private int count; + + private List rangeList = new List(); + + private bool undone = true; + + public RemoveRangeCommand(List theList, int index, int count) + { + this.theList = theList; + this.index = index; + this.count = count; + } + + #region ICommand Members + + public void Execute() + { + #region Guard + + if(!undone) + { + return; + } + + #endregion + + Debug.Assert(index >= 0 && index < theList.Count); + Debug.Assert(index + count <= theList.Count); + + rangeList = new List(theList.GetRange(index, count)); + + theList.RemoveRange(index, count); + + undone = false; + } + + public void Undo() + { + #region Guard + + if(undone) + { + return; + } + + #endregion + + theList.InsertRange(index, rangeList); + + undone = true; + } + + #endregion + } + + #endregion + + #region ClearCommand + + private class ClearCommand : ICommand + { + private IList theList; + + private IList undoList; + + private bool undone = true; + + public ClearCommand(IList theList) + { + this.theList = theList; + } + + #region ICommand Members + + public void Execute() + { + #region Guard + + if(!undone) + { + return; + } + + #endregion + + undoList = new List(theList); + + theList.Clear(); + + undone = false; + } + + public void Undo() + { + #region Guard + + if(undone) + { + return; + } + + #endregion + + Debug.Assert(theList.Count == 0); + + foreach(T item in undoList) + { + theList.Add(item); + } + + undoList.Clear(); + + undone = true; + } + + #endregion + } + + #endregion + + #region ReverseCommand + + private class ReverseCommand : ICommand + { + private List theList; + + private int index; + + private int count; + + private bool reverseRange; + + private bool undone = true; + + public ReverseCommand(List theList) + { + this.theList = theList; + this.reverseRange = false; + } + + public ReverseCommand(List theList, int index, int count) + { + this.theList = theList; + this.index = index; + this.count = count; + this.reverseRange = true; + } + + #region ICommand Members + + public void Execute() + { + #region Guard + + if(!undone) + { + return; + } + + #endregion + + if(reverseRange) + { + theList.Reverse(index, count); + } + else + { + theList.Reverse(); + } + + undone = false; + } + + public void Undo() + { + #region Guard + + if(undone) + { + return; + } + + #endregion + + if(reverseRange) + { + theList.Reverse(index, count); + } + else + { + theList.Reverse(); + } + + undone = true; + } + + #endregion + } + + #endregion + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Test.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Test.cs new file mode 100644 index 00000000..48c79f12 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Test.cs @@ -0,0 +1,274 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Sanford.Collections.Generic +{ + public partial class UndoableList : IList + { + [Conditional("DEBUG")] + public static void Test() + { + int count = 10; + List comparisonList = new List(count); + UndoableList undoList = new UndoableList(count); + + PopulateLists(comparisonList, undoList, count); + TestAdd(comparisonList, undoList); + TestClear(comparisonList, undoList); + TestInsert(comparisonList, undoList); + TestInsertRange(comparisonList, undoList); + TestRemove(comparisonList, undoList); + TestRemoveAt(comparisonList, undoList); + TestRemoveRange(comparisonList, undoList); + TestReverse(comparisonList, undoList); + } + + [Conditional("DEBUG")] + private static void TestAdd(List comparisonList, UndoableList undoList) + { + TestEquals(comparisonList, undoList); + + Stack redoStack = new Stack(); + + while(comparisonList.Count > 0) + { + redoStack.Push(comparisonList[comparisonList.Count - 1]); + comparisonList.RemoveAt(comparisonList.Count - 1); + Debug.Assert(undoList.Undo()); + TestEquals(comparisonList, undoList); + } + + while(redoStack.Count > 0) + { + comparisonList.Add(redoStack.Pop()); + Debug.Assert(undoList.Redo()); + TestEquals(comparisonList, undoList); + } + } + + [Conditional("DEBUG")] + private static void TestClear(List comparisonList, UndoableList undoList) + { + TestEquals(comparisonList, undoList); + + undoList.Clear(); + + Debug.Assert(undoList.Undo()); + + TestEquals(comparisonList, undoList); + } + + [Conditional("DEBUG")] + private static void TestInsert(List comparisonList, UndoableList undoList) + { + TestEquals(comparisonList, undoList); + + int index = comparisonList.Count / 2; + + comparisonList.Insert(index, 999); + undoList.Insert(index, 999); + + comparisonList.RemoveAt(index); + Debug.Assert(undoList.Undo()); + + TestEquals(comparisonList, undoList); + + comparisonList.Insert(index, 999); + Debug.Assert(undoList.Redo()); + + TestEquals(comparisonList, undoList); + } + + [Conditional("DEBUG")] + private static void TestInsertRange(List comparisonList, UndoableList undoList) + { + TestEquals(comparisonList, undoList); + + int[] range = { 1, 2, 3, 4, 5 }; + int index = comparisonList.Count / 2; + + comparisonList.InsertRange(index, range); + undoList.InsertRange(index, range); + + TestEquals(comparisonList, undoList); + + comparisonList.RemoveRange(index, range.Length); + Debug.Assert(undoList.Undo()); + + TestEquals(comparisonList, undoList); + + comparisonList.InsertRange(index, range); + Debug.Assert(undoList.Redo()); + + TestEquals(comparisonList, undoList); + } + + [Conditional("DEBUG")] + private static void TestRemove(List comparisonList, UndoableList undoList) + { + TestEquals(comparisonList, undoList); + + int index = comparisonList.Count / 2; + + int item = comparisonList[index]; + + comparisonList.Remove(item); + undoList.Remove(item); + + TestEquals(comparisonList, undoList); + + comparisonList.Insert(index, item); + Debug.Assert(undoList.Undo()); + + TestEquals(comparisonList, undoList); + } + + [Conditional("DEBUG")] + private static void TestRemoveAt(List comparisonList, UndoableList undoList) + { + TestEquals(comparisonList, undoList); + + int index = comparisonList.Count / 2; + + int item = comparisonList[index]; + + comparisonList.RemoveAt(index); + undoList.RemoveAt(index); + + TestEquals(comparisonList, undoList); + + comparisonList.Insert(index, item); + Debug.Assert(undoList.Undo()); + + TestEquals(comparisonList, undoList); + } + + [Conditional("DEBUG")] + private static void TestRemoveRange(List comparisonList, UndoableList undoList) + { + TestEquals(comparisonList, undoList); + + int index = comparisonList.Count / 2; + int count = comparisonList.Count - index; + + List range = comparisonList.GetRange(index, count); + + comparisonList.RemoveRange(index, count); + undoList.RemoveRange(index, count); + + TestEquals(comparisonList, undoList); + + comparisonList.InsertRange(index, range); + Debug.Assert(undoList.Undo()); + + TestEquals(comparisonList, undoList); + } + + [Conditional("DEBUG")] + private static void TestReverse(List comparisonList, UndoableList undoList) + { + TestEquals(comparisonList, undoList); + + comparisonList.Reverse(); + undoList.Reverse(); + + TestEquals(comparisonList, undoList); + + comparisonList.Reverse(); + Debug.Assert(undoList.Undo()); + + TestEquals(comparisonList, undoList); + + comparisonList.Reverse(); + Debug.Assert(undoList.Redo()); + + TestEquals(comparisonList, undoList); + + int count = comparisonList.Count / 2; + + comparisonList.Reverse(0, count); + undoList.Reverse(0, count); + + TestEquals(comparisonList, undoList); + + comparisonList.Reverse(0, count); + Debug.Assert(undoList.Undo()); + + TestEquals(comparisonList, undoList); + + comparisonList.Reverse(0, count); + Debug.Assert(undoList.Redo()); + + TestEquals(comparisonList, undoList); + } + + [Conditional("DEBUG")] + private static void PopulateLists(IList a, IList b, int count) + { + Random r = new Random(); + int item; + + for(int i = 0; i < count; i++) + { + item = r.Next(); + a.Add(item); + b.Add(item); + } + } + + [Conditional("DEBUG")] + private static void TestEquals(ICollection a, ICollection b) + { + bool equals = true; + + if(a.Count != b.Count) + { + equals = false; + } + IEnumerator aEnumerator = a.GetEnumerator(); + IEnumerator bEnumerator = b.GetEnumerator(); + + while(equals && aEnumerator.MoveNext() && bEnumerator.MoveNext()) + { + equals = aEnumerator.Current.Equals(bEnumerator.Current); + } + + Debug.Assert(equals); + } + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.cs new file mode 100644 index 00000000..8cecb32f --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.cs @@ -0,0 +1,410 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Sanford.Collections.Generic +{ + /// + /// Represents a list with undo/redo functionality. + /// + /// + /// The type of elements in the list. + /// + public partial class UndoableList : IList + { + #region UndoableList Members + + #region Fields + + private List theList; + + private UndoManager undoManager = new UndoManager(); + + #endregion + + #region Construction + + public UndoableList() + { + theList = new List(); + } + + public UndoableList(IEnumerable collection) + { + theList = new List(collection); + } + + public UndoableList(int capacity) + { + theList = new List(capacity); + } + + #endregion + + #region Methods + + /// + /// Undoes the last operation. + /// + /// + /// true if the last operation was undone, false if there + /// are no more operations left to undo. + /// + public bool Undo() + { + return undoManager.Undo(); + } + + /// + /// Redoes the last operation. + /// + /// + /// true if the last operation was redone, false if there + /// are no more operations left to redo. + /// + public bool Redo() + { + return undoManager.Redo(); + } + + /// + /// Clears the undo/redo history. + /// + public void ClearHistory() + { + undoManager.ClearHistory(); + } + + #region List Wrappers + + public int BinarySearch(T item) + { + return theList.BinarySearch(item); + } + + public int BinarySearch(T item, IComparer comparer) + { + return theList.BinarySearch(item, comparer); + } + + public int BinarySearch(int index, int count, T item, IComparer comparer) + { + return theList.BinarySearch(index, count, item, comparer); + } + + public bool Contains(T item) + { + return theList.Contains(item); + } + + public List ConvertAll(Converter converter) + { + return theList.ConvertAll(converter); + } + + public bool Exists(Predicate match) + { + return theList.Exists(match); + } + + public T Find(Predicate match) + { + return theList.Find(match); + } + + public List FindAll(Predicate match) + { + return theList.FindAll(match); + } + + public int FindIndex(Predicate match) + { + return theList.FindIndex(match); + } + + public int FindIndex(int startIndex, Predicate match) + { + return theList.FindIndex(startIndex, match); + } + + public int FindIndex(int startIndex, int count, Predicate match) + { + return theList.FindIndex(startIndex, count, match); + } + + public int FindLastIndex(Predicate match) + { + return theList.FindLastIndex(match); + } + + public int FindLastIndex(int startIndex, Predicate match) + { + return theList.FindLastIndex(startIndex, match); + } + + public int FindLastIndex(int startIndex, int count, Predicate match) + { + return theList.FindLastIndex(startIndex, count, match); + } + + public T FindLast(Predicate match) + { + return theList.FindLast(match); + } + + public int LastIndexOf(T item) + { + return theList.LastIndexOf(item); + } + + public int LastIndexOf(T item, int index) + { + return theList.LastIndexOf(item, index); + } + + public int LastIndexOf(T item, int index, int count) + { + return theList.LastIndexOf(item, index, count); + } + + public bool TrueForAll(Predicate match) + { + return theList.TrueForAll(match); + } + + public T[] ToArray() + { + return theList.ToArray(); + } + + public void TrimExcess() + { + theList.TrimExcess(); + } + + public void AddRange(IEnumerable collection) + { + InsertRangeCommand command = new InsertRangeCommand(theList, theList.Count, collection); + + undoManager.Execute(command); + } + + public void InsertRange(int index, IEnumerable collection) + { + InsertRangeCommand command = new InsertRangeCommand(theList, index, collection); + + undoManager.Execute(command); + } + + public void RemoveRange(int index, int count) + { + RemoveRangeCommand command = new RemoveRangeCommand(theList, index, count); + + undoManager.Execute(command); + } + + public void Reverse() + { + ReverseCommand command = new ReverseCommand(theList); + + undoManager.Execute(command); + } + + public void Reverse(int index, int count) + { + ReverseCommand command = new ReverseCommand(theList, index, count); + + undoManager.Execute(command); + } + + #endregion + + #endregion + + #region Properties + + /// + /// The number of operations left to undo. + /// + public int UndoCount + { + get + { + return undoManager.UndoCount; + } + } + + /// + /// The number of operations left to redo. + /// + public int RedoCount + { + get + { + return undoManager.RedoCount; + } + } + + #endregion + + #endregion + + #region IList Members + + public int IndexOf(T item) + { + return theList.IndexOf(item); + } + + public void Insert(int index, T item) + { + InsertCommand command = new InsertCommand(theList, index, item); + + undoManager.Execute(command); + } + + public void RemoveAt(int index) + { + RemoveAtCommand command = new RemoveAtCommand(theList, index); + + undoManager.Execute(command); + } + + public T this[int index] + { + get + { + return theList[index]; + } + set + { + SetCommand command = new SetCommand(theList, index, value); + + undoManager.Execute(command); + } + } + + #endregion + + #region ICollection Members + + public void Add(T item) + { + InsertCommand command = new InsertCommand(theList, Count, item); + + undoManager.Execute(command); + } + + public void Clear() + { + #region Guard + + if(Count == 0) + { + return; + } + + #endregion + + ClearCommand command = new ClearCommand(theList); + + undoManager.Execute(command); + } + + public void CopyTo(T[] array, int arrayIndex) + { + theList.CopyTo(array, arrayIndex); + } + + public int Count + { + get + { + return theList.Count; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + public bool Remove(T item) + { + int index = IndexOf(item); + bool result; + + if(index >= 0) + { + RemoveAtCommand command = new RemoveAtCommand(theList, index); + + undoManager.Execute(command); + + result = true; + } + else + { + result = false; + } + + return result; + } + + #endregion + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + return theList.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return theList.GetEnumerator(); + } + + #endregion + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/AvlEnumerator.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/AvlEnumerator.cs new file mode 100644 index 00000000..03e84f0e --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/AvlEnumerator.cs @@ -0,0 +1,167 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.Collections; +using System.Diagnostics; + +namespace Sanford.Collections.Immutable +{ + /// + /// Provides functionality for iterating over an AVL tree. + /// + internal class AvlEnumerator : IEnumerator + { + #region AvlEnumerator Members + + #region Instance Fields + + // The root of the AVL tree. + private IAvlNode root; + + // The number of nodes in the tree. + private readonly int count; + + // The object at the current position. + private object current = null; + + // The current index. + private int index; + + // Used for traversing the tree inorder. + private System.Collections.Stack nodeStack = new System.Collections.Stack(); + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the AvlEnumerator class. + /// + /// + /// The root of the AVL tree to iterate over. + /// + public AvlEnumerator(IAvlNode root) + { + this.root = root; + this.count = root.Count; + + Reset(); + } + + /// + /// Initializes a new instance of the AvlEnumerator class. + /// + /// + /// The root of the AVL tree to iterate over. + /// + /// + /// The number of nodes in the tree. + /// + public AvlEnumerator(IAvlNode root, int count) + { + Debug.Assert(count <= root.Count); + + this.root = root; + this.count = count; + + Reset(); + } + + #endregion + + #endregion + + #region IEnumerator Members + + /// + /// Sets the enumerator to its initial position, which is before + /// the first element in the AVL tree. + /// + public void Reset() + { + index = 0; + + nodeStack.Clear(); + + IAvlNode currentNode = root; + + // Push nodes on to the stack to get to the first item. + while(currentNode != AvlNode.NullNode) + { + nodeStack.Push(currentNode); + currentNode = currentNode.LeftChild; + } + } + + /// + /// Gets the current element in the AVL tree. + /// + /// + /// The enumerator is positioned before the first element in the AVL + /// tree or after the last element. + /// + public object Current + { + get + { + if(index == 0) + { + throw new InvalidOperationException( + "The enumerator is positioned before the first " + + "element of the collection or after the last " + + "element."); + } + + return current; + } + } + + /// + /// Advances the enumerator to the next element of the AVL tree. + /// + /// + /// true if the enumerator was successfully advanced to the + /// next element; false if the enumerator has passed the end + /// of the collection. + /// + public bool MoveNext() + { + bool result; + + // If the end of the AVL tree has not yet been reached. + if(index < count) + { + // Get the next node. + IAvlNode currentNode = (IAvlNode)nodeStack.Pop(); + + current = currentNode.Data; + + currentNode = currentNode.RightChild; + + while(currentNode != AvlNode.NullNode) + { + nodeStack.Push(currentNode); + currentNode = currentNode.LeftChild; + } + + index++; + + result = true; + } + else + { + result = false; + } + + return result; + } + + #endregion + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/AvlNode.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/AvlNode.cs new file mode 100644 index 00000000..5c89cb0f --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/AvlNode.cs @@ -0,0 +1,440 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Collections.Immutable +{ + /// + /// Represents a node in an AVL tree. + /// + [ImmutableObject(true)] + internal class AvlNode : IAvlNode + { + #region AvlNode Members + + #region Class Fields + + // For use as a null node. + public static readonly NullAvlNode NullNode = new NullAvlNode(); + + #endregion + + #region Instance Fields + + // The data represented by this node. + private readonly object data; + + // The number of nodes in the subtree. + private readonly int count; + + // The height of this node. + private readonly int height; + + // Left and right children. + private readonly IAvlNode leftChild; + private readonly IAvlNode rightChild; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the AvlNode class with the specified + /// data and left and right children. + /// + /// + /// The data for the node. + /// + /// + /// The left child. + /// + /// + /// The right child. + /// + public AvlNode(object data, IAvlNode leftChild, IAvlNode rightChild) + { + // Preconditions. + Debug.Assert(leftChild != null && rightChild != null); + + this.data = data; + this.leftChild = leftChild; + this.rightChild = rightChild; + + count = 1 + leftChild.Count + rightChild.Count; + height = 1 + Math.Max(leftChild.Height, rightChild.Height); + } + + #endregion + + #region Methods + + #region Rotation Methods + + // Left - left single rotation. + private IAvlNode DoLLRotation(IAvlNode node) + { + /* + * An LL rotation looks like the following: + * + * A B + * / / \ + * B ---> C A + * / + * C + */ + + // Create right child of the new root. + IAvlNode a = new AvlNode( + node.Data, + node.LeftChild.RightChild, + node.RightChild); + + IAvlNode b = new AvlNode( + node.LeftChild.Data, + node.LeftChild.LeftChild, + a); + + // Postconditions. + Debug.Assert(b.Data == node.LeftChild.Data); + Debug.Assert(b.LeftChild == node.LeftChild.LeftChild); + Debug.Assert(b.RightChild.Data == node.Data); + + return b; + } + + // Left - right double rotation. + private IAvlNode DoLRRotation(IAvlNode node) + { + /* + * An LR rotation looks like the following: + * + * Perform an RR rotation at B: + * + * A A + * / / + * B ---> C + * \ / + * C B + * + * Perform an LL rotation at A: + * + * A C + * / / \ + * C ---> B A + * / + * B + */ + + IAvlNode a = new AvlNode( + node.Data, + DoRRRotation(node.LeftChild), + node.RightChild); + + IAvlNode c = DoLLRotation(a); + + // Postconditions. + Debug.Assert(c.Data == node.LeftChild.RightChild.Data); + Debug.Assert(c.LeftChild.Data == node.LeftChild.Data); + Debug.Assert(c.RightChild.Data == node.Data); + + return c; + } + + // Right - right single rotation. + private IAvlNode DoRRRotation(IAvlNode node) + { + /* + * An RR rotation looks like the following: + * + * A B + * \ / \ + * B ---> A C + * \ + * C + */ + + // Create left child of the new root. + IAvlNode a = new AvlNode( + node.Data, + node.LeftChild, + node.RightChild.LeftChild); + + IAvlNode b = new AvlNode( + node.RightChild.Data, + a, + node.RightChild.RightChild); + + // Postconditions. + Debug.Assert(b.Data == node.RightChild.Data); + Debug.Assert(b.RightChild == node.RightChild.RightChild); + Debug.Assert(b.LeftChild.Data == node.Data); + + return b; + } + + // Right - left double rotation. + private IAvlNode DoRLRotation(IAvlNode node) + { + /* + * An RL rotation looks like the following: + * + * Perform an LL rotation at B: + * + * A A + * \ \ + * B ---> C + * / \ + * C B + * + * Perform an RR rotation at A: + * + * A C + * \ / \ + * C ---> A B + * \ + * B + */ + + IAvlNode a = new AvlNode( + node.Data, + node.LeftChild, + DoLLRotation(node.RightChild)); + + IAvlNode c = DoRRRotation(a); + + // Postconditions. + Debug.Assert(c.Data == node.RightChild.LeftChild.Data); + Debug.Assert(c.LeftChild.Data == node.Data); + Debug.Assert(c.RightChild.Data == node.RightChild.Data); + + return c; + } + + #endregion + + #endregion + + #endregion + + #region IAvlNode Members + + /// + /// Removes the current node from the AVL tree. + /// + /// + /// The node to in the tree to replace the current node. + /// + public IAvlNode Remove() + { + IAvlNode result; + + /* + * Deal with the three cases for removing a node from a binary tree. + */ + + // If the node has no right children. + if(this.RightChild == AvlNode.NullNode) + { + // The replacement node is the node's left child. + result = this.LeftChild; + } + // Else if the node's right child has no left children. + else if(this.RightChild.LeftChild == AvlNode.NullNode) + { + // The replacement node is the node's right child. + result = new AvlNode( + this.RightChild.Data, + this.LeftChild, + this.RightChild.RightChild); + } + // Else the node's right child has left children. + else + { + /* + * Go to the node's right child and descend as far left as + * possible. The node found at this point will replace the + * node to be removed. + */ + + IAvlNode replacement = AvlNode.NullNode; + IAvlNode rightChild = RemoveReplacement(this.RightChild, ref replacement); + + // Create new node with the replacement node and the new + // right child. + result = new AvlNode( + replacement.Data, + this.LeftChild, + rightChild); + } + + return result; + } + + // Finds and removes replacement node for deletion (third case). + private IAvlNode RemoveReplacement(IAvlNode node, ref IAvlNode replacement) + { + IAvlNode newNode; + + // If the bottom of the left tree has been found. + if(node.LeftChild == AvlNode.NullNode) + { + // The replacement node is the node found at this point. + replacement = node; + + // Get the node's right child. This will be needed as we + // ascend back up the tree. + newNode = node.RightChild; + } + // Else the bottom of the left tree has not been found. + else + { + // Create new node and continue descending down the left tree. + newNode = new AvlNode(node.Data, + RemoveReplacement(node.LeftChild, ref replacement), + node.RightChild); + + // If the node is out of balance. + if(!newNode.IsBalanced()) + { + // Rebalance the node. + newNode = newNode.Balance(); + } + } + + // Postconditions. + Debug.Assert(newNode.IsBalanced()); + + return newNode; + } + + /// + /// Balances the subtree represented by the node. + /// + /// + /// The root node of the balanced subtree. + /// + public IAvlNode Balance() + { + IAvlNode result; + + if(BalanceFactor < -1) + { + if(leftChild.BalanceFactor < 0) + { + result = DoLLRotation(this); + } + else + { + result = DoLRRotation(this); + } + } + else if(BalanceFactor > 1) + { + if(rightChild.BalanceFactor > 0) + { + result = DoRRRotation(this); + } + else + { + result = DoRLRotation(this); + } + } + else + { + result = this; + } + + Debug.Assert(result.IsBalanced()); + + return result; + } + + /// + /// Indicates whether or not the subtree the node represents is in + /// balance. + /// + /// + /// true if the subtree is in balance; otherwise, false. + /// + public bool IsBalanced() + { + return BalanceFactor >= -1 && BalanceFactor <= 1; + } + + /// + /// Gets the balance factor of the subtree the node represents. + /// + public int BalanceFactor + { + get + { + return rightChild.Height - leftChild.Height; + } + } + + /// + /// Gets the number of nodes in the subtree. + /// + public int Count + { + get + { + return count; + } + } + + /// + /// Gets the node's data. + /// + public object Data + { + get + { + return data; + } + } + + /// + /// Gets the height of the subtree the node represents. + /// + public int Height + { + get + { + return height; + } + } + + /// + /// Gets the node's left child. + /// + public IAvlNode LeftChild + { + get + { + return leftChild; + } + } + + /// + /// Gets the node's right child. + /// + public IAvlNode RightChild + { + get + { + return rightChild; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/IAvlNode.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/IAvlNode.cs new file mode 100644 index 00000000..7715416f --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/IAvlNode.cs @@ -0,0 +1,91 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; + +namespace Sanford.Collections.Immutable +{ + /// + /// Represents the functionality and properties of AVL nodes. + /// + internal interface IAvlNode + { + /// + /// Removes the current node from the AVL tree. + /// + /// + /// The node to in the tree to replace the current node. + /// + IAvlNode Remove(); + + /// + /// Balances the subtree represented by the node. + /// + /// + /// The root node of the balanced subtree. + /// + IAvlNode Balance(); + + /// + /// Indicates whether or not the subtree the node represents is in + /// balance. + /// + /// + /// true if the subtree is in balance; otherwise, false. + /// + bool IsBalanced(); + + /// + /// Gets the balance factor of the subtree the node represents. + /// + int BalanceFactor + { + get; + } + + /// + /// Gets the number of nodes in the subtree. + /// + int Count + { + get; + } + + /// + /// Gets the node's data. + /// + object Data + { + get; + } + + /// + /// Gets the height of the subtree the node represents. + /// + int Height + { + get; + } + + /// + /// Gets the node's left child. + /// + IAvlNode LeftChild + { + get; + } + + /// + /// Gets the node's right child. + /// + IAvlNode RightChild + { + get; + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/NullAvlNode.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/NullAvlNode.cs new file mode 100644 index 00000000..dabf81f3 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/AVL Tree Classes/NullAvlNode.cs @@ -0,0 +1,124 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.ComponentModel; + +namespace Sanford.Collections.Immutable +{ + /// + /// Represents a null AVL node. + /// + [ImmutableObject(true)] + internal class NullAvlNode : IAvlNode + { + #region IAvlNode Members + + /// + /// Removes the current node from the AVL tree. + /// + /// + /// The node to in the tree to replace the current node. + /// + public IAvlNode Remove() + { + return this; + } + + /// + /// Balances the subtree represented by the node. + /// + /// + /// The root node of the balanced subtree. + /// + public IAvlNode Balance() + { + return this; + } + + /// + /// Indicates whether or not the subtree the node represents is in + /// balance. + /// + /// + /// true if the subtree is in balance; otherwise, false. + /// + public bool IsBalanced() + { + return true; + } + + /// + /// Gets the balance factor of the subtree the node represents. + /// + public int BalanceFactor + { + get + { + return 0; + } + } + + /// + /// Gets the number of nodes in the subtree. + /// + public int Count + { + get + { + return 0; + } + } + + /// + /// Gets the node's data. + /// + public object Data + { + get + { + return null; + } + } + + /// + /// Gets the height of the subtree the node represents. + /// + public int Height + { + get + { + return 0; + } + } + + /// + /// Gets the node's left child. + /// + public IAvlNode LeftChild + { + get + { + return this; + } + } + + /// + /// Gets the node's right child. + /// + public IAvlNode RightChild + { + get + { + return this; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/Array.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/Array.cs new file mode 100644 index 00000000..a97a411c --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/Array.cs @@ -0,0 +1,213 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.Collections; +using System.ComponentModel; + +namespace Sanford.Collections.Immutable +{ + /// + /// Represents an array data structure. + /// + [ImmutableObject(true)] + public class Array : IEnumerable + { + #region Array Members + + #region Instance Fields + + // The length of the array. + private int length; + + // The head node of the random access list. + private RalTopNode head; + + #endregion + + #region Construction + + /// + /// Initialize an instance of the Array class with the specified array + /// length. + /// + /// + /// The length of the array. + /// + public Array(int length) + { + // Precondition. + if(length < 0) + { + throw new ArgumentOutOfRangeException("length", length, + "Array length out of range."); + } + + this.length = length; + + int n = length; + int exponent; + int count; + + head = null; + + /* + * The following algorithm creates the trees for the array. The + * trees have the form of a random access list. + */ + + // While there are still nodes to create. + while(n > 0) + { + // Get the log based 2 of the number of nodes. + exponent = (int)Math.Log(n, 2); + + // Get the number of nodes for each subtree. + count = ((int)Math.Pow(2, exponent) - 1) / 2; + + // Create the top node representing the subtree. + head = new RalTopNode( + new RalTreeNode( + null, + CreateSubTree(count), + CreateSubTree(count)), + head); + + // Get the remaining number of nodes to create. + n -= head.Root.Count; + } + } + + /// + /// Initializes a new instance of the Array class with the specified + /// head of the random access list and the length of the array. + /// + /// + /// The head of the random access list. + /// + /// + /// The length of the array. + /// + private Array(RalTopNode head, int length) + { + this.head = head; + this.length = length; + } + + #endregion + + #region Methods + + /// + /// Gets the value of the specified element in the current Array. + /// + /// + /// An integer that represents the position of the Array element to + /// get. + /// + /// + /// The value at the specified position in the Array. + /// + /// + /// index is outside the range of valid indexes for the current Array. + /// + public object GetValue(int index) + { + // Preconditions. + if(index < 0 || index >= Length) + { + throw new ArgumentOutOfRangeException( + "Index out of range."); + } + + return head.GetValue(index); + } + + /// + /// Sets the specified element in the current Array to the specified + /// value. + /// + /// + /// The new value for the specified element. + /// + /// + /// An integer that represents the position of the Array element to set. + /// + /// + /// A new array with the element at the specified position set to the + /// specified value. + /// + /// + /// index is outside the range of valid indexes for the current Array. + /// + public Array SetValue(object value, int index) + { + // Preconditions. + if(index < 0 || index >= Length) + { + throw new ArgumentOutOfRangeException( + "Index out of range."); + } + + return new Array(head.SetValue(value, index), Length); + } + + // Creates subtrees within the random access list. + private RalTreeNode CreateSubTree(int count) + { + RalTreeNode result = null; + + if(count > 0) + { + int c = count / 2; + + result = new RalTreeNode( + null, + CreateSubTree(c), + CreateSubTree(c)); + } + + return result; + } + + #endregion + + #region Properties + + /// + /// Gets an integer that represents the total number of elements in all + /// the dimensions of the Array. + /// + public int Length + { + get + { + return length; + } + } + + #endregion + + #endregion + + #region IEnumerable Members + + /// + /// Returns an IEnumerator for the Array. + /// + /// + /// An IEnumerator for the Array. + /// + public IEnumerator GetEnumerator() + { + return new RalEnumerator(head, length); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/ArrayList.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/ArrayList.cs new file mode 100644 index 00000000..4c3ca12a --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/ArrayList.cs @@ -0,0 +1,694 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/28/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Collections.Immutable +{ + /// + /// Represents a collection of elements accessible by index and supports + /// insertion and deletion. + /// + [ImmutableObject(true)] + public class ArrayList + { + #region ArrayList Members + + #region Constants + + // The height of the tree pool. + private const int TreePoolHeight = 30; + + // The default height of the initial tree. + private const int DefaultCapacityHeight = 5; + + #endregion + + #region Readonly + + /* + * The tree pool is a tree made up of null nodes. It is completely + * balanced and is used to form a template of nodes for use in the + * ArrayList. Initially, a small subtree is taken from the tree pool + * when an ArrayList is created. New nodes replace the null nodes as + * new versions of the ArrayList are created. Once the tree has been + * filled, another subtree of equal height is taken from the tree pool + * to enlarge the tree for the next version of the ArrayList. + * + * The reasoning behind this approach is that the Add method of the + * ArrayList will probably be the most widely used operation. By having + * a prefabricated balanced tree, no rebalancing has to take place as + * new nodes are added to the tree. Their position in the tree has + * already been determined by the existing null tree. This improves + * performance. + */ + + private static readonly IAvlNode TreePool; + + #endregion + + #region Fields + + // The number of items in the ArrayList. + private int count = 0; + + // The root of the tree. + private IAvlNode root; + + #endregion + + #region Contstruction + + /// + /// Initializes the ArrayList class. + /// + static ArrayList() + { + IAvlNode parent = AvlNode.NullNode; + IAvlNode child = AvlNode.NullNode; + + // Create the tree pool. + for(int i = 0; i < TreePoolHeight; i++) + { + parent = new AvlNode(null, child, child); + child = parent; + } + + TreePool = parent; + + // Postconditions. + Debug.Assert(TreePool.Height == TreePoolHeight); + } + + /// + /// Initializes a new instance of the ArrayList class. + /// + public ArrayList() + { + root = GetSubTree(DefaultCapacityHeight); + } + + /// + /// Initializes a new instance of the ArrayList class that contains + /// elements copied from the specified collection. + /// + /// + /// The ICollection whose elements are copied to the new list. + /// + public ArrayList(ICollection collection) + { + if(collection.Count > 0) + { + int height = (int)Math.Log(collection.Count, 2) + 1; + + root = CollectionToTree(collection.GetEnumerator(), height); + } + else + { + root = GetSubTree(DefaultCapacityHeight); + } + + count = collection.Count; + } + + /// + /// Initializes a new instance of the ArrayList class with the + /// specified root and count. + /// + /// + /// The root of the tree. + /// + /// + /// The number of items in the ArrayList. + /// + private ArrayList(IAvlNode root, int count) + { + this.root = root; + this.count = count; + } + + #endregion + + #region Methods + + /// + /// Adds an object to the end of the ArrayList. + /// + /// + /// The Object to be added to the end of the ArrayList. + /// + /// + /// A new ArrayList object with the specified value added at the end. + /// + public ArrayList Add(object value) + { + ArrayList result; + + // If the tree has been filled. + if(count == root.Count) + { + // Create a new ArrayList while enlarging the tree. The + // current count serves as an index for setting the specified + // value. + result = new ArrayList( + SetValue(count, value, EnlargeTree()), + count + 1); + } + // Else the tree has not been filled. + else + { + // Create a new ArrayList. The current count serves as an index + // for setting the specified value. + result = new ArrayList( + SetValue(count, value, root), + count + 1); + } + + // Postconditions. + Debug.Assert(result.Count == Count + 1); + Debug.Assert(result.GetValue(result.Count - 1) == value); + + return result; + } + + /// + /// Determines whether an element is in the ArrayList. + /// + /// + /// The Object to locate in the ArrayList. + /// + /// + /// true if item is found in the ArrayList; otherwise, + /// false. + /// + public bool Contains(object value) + { + return IndexOf(value) > -1; + } + + /// + /// Returns the zero-based index of the first occurrence of a value in + /// the ArrayList. + /// + /// + /// The Object to locate in the ArrayList. + /// + /// + /// The zero-based index of the first occurrence of value within the + /// ArrayList, if found; otherwise, -1. + /// + public int IndexOf(object value) + { + int index = 0; + + // Iterate through the ArrayList and compare each value with the + // specified value. If they match, return the index of the value. + foreach(object v in this) + { + if(value.Equals(v)) + { + return index; + } + + index++; + } + + // The specified value is not in the ArrayList. + return -1; + } + + /// + /// Inserts an element into the ArrayList at the specified index. + /// + /// + /// The zero-based index at which value should be inserted. + /// + /// + /// The Object to insert. + /// + /// + /// A new ArrayList with the specified object inserted at the specified + /// index. + /// + /// + /// index is less than zero or index is greater than Count. + /// + public ArrayList Insert(int index, object value) + { + // Preconditions. + if(index < 0 || index > Count) + { + throw new ArgumentOutOfRangeException( + "ArrayList index out of range."); + } + + // Create new ArrayList with the value inserted at the specified index. + ArrayList result = new ArrayList(Insert(index, value, root), count + 1); + + // Post conditions. + Debug.Assert(result.GetValue(index) == value); + + return result; + } + + /// + /// Removes the first occurrence of a specified object from the + /// ArrayList. + /// + /// + /// The Object to remove from the ArrayList. + /// + /// + /// A new ArrayList with the first occurrent of the specified object + /// removed. + /// + public ArrayList Remove(object value) + { + ArrayList result; + int index = IndexOf(value); + + // If the object is in the ArrayList. + if(index > -1) + { + // Remove the object. + result = RemoveAt(index); + + // Postcondition. + Debug.Assert(result.Count == Count - 1); + } + // Else the object is not in the ArrayList. + else + { + result = this; + } + + return result; + } + + /// + /// Removes the element at the specified index of the ArrayList. + /// + /// + /// The zero-based index of the element to remove. + /// + /// + /// A new ArrayList with the element at the specified index removed. + /// + /// + /// index is less than zero or index is equal to or greater than Count. + /// + public ArrayList RemoveAt(int index) + { + // Preconditions. + if(index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException("index", index, + "ArrayList index out of range."); + } + + // Create a new ArrayList with the element at the specified index + // removed. + ArrayList result = new ArrayList(RemoveAt(index, root), count - 1); + + // Postconditions. + Debug.Assert(result.Count == Count - 1); + + return result; + } + + /// + /// Gets the value at the specified index. + /// + /// + /// The zero-based index of the element to get. + /// + /// + /// The value at the specified index. + /// + /// + /// index is less than zero or index is equal to or greater than Count. + /// + public object GetValue(int index) + { + // Preconditions. + if(index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException("index", index, + "Index out of range."); + } + + return GetValue(index, root); + } + + /// + /// Sets the value at the specified index. + /// + /// + /// The zero-based index of the element to set. + /// + /// + /// The value to set at the specified index. + /// + /// + /// A new ArrayList with the specified value set at the specified index. + /// + /// + /// index is less than zero or index is equal to or greater than Count. + /// + public ArrayList SetValue(int index, object value) + { + // Preconditions. + if(index < 0 || index >= count) + { + throw new ArgumentOutOfRangeException( + "ArrayList index out of range."); + } + + // Create a new ArrayList with the specified value set at the + // specified index. + ArrayList result = new ArrayList(SetValue(index, value, root), count); + + // Postconditions. + Debug.Assert(result.GetValue(index) == value); + + return result; + } + + private IAvlNode CollectionToTree(IEnumerator enumerator, int height) + { + IAvlNode result; + + if(height == 0) + { + object data = null; + + if(enumerator.MoveNext()) + { + data = enumerator.Current; + } + + result = new AvlNode( + data, + AvlNode.NullNode, + AvlNode.NullNode); + } + else + { + IAvlNode leftChild, rightChild; + object data = null; + + leftChild = CollectionToTree(enumerator, height - 1); + + if(enumerator.MoveNext()) + { + data = enumerator.Current; + + rightChild = CollectionToTree(enumerator, height - 1); + } + else + { + rightChild = GetSubTree(height - 1); + } + + result = new AvlNode( + data, + leftChild, + rightChild); + } + + Debug.Assert(result.IsBalanced()); + + return result; + } + + // Enlarges the tree used by the ArrayList. + private IAvlNode EnlargeTree() + { + // Preconditions. + Debug.Assert(root.Height <= TreePool.Height); + + // Create new root for the enlarged tree. + IAvlNode result = new AvlNode(null, root, GetSubTree(root.Height)); + + // Postconditions. + Debug.Assert(result.BalanceFactor == 0); + Debug.Assert(result.Height == root.Height + 1); + + return result; + } + + // Recursive GetValue helper method. + private object GetValue(int index, IAvlNode node) + { + // Preconditions. + Debug.Assert(index >= 0 && index < Count); + Debug.Assert(node != AvlNode.NullNode); + + object result; + int leftCount = node.LeftChild.Count; + + // If the node has been found. + if(index == leftCount) + { + // Get value. + result = node.Data; + } + // Else if the node is in the left tree. + else if(index < leftCount) + { + // Move search to left child. + result = GetValue(index, node.LeftChild); + } + // Else if the node is in the right tree. + else + { + // Move search to the right child. + result = GetValue(index - (leftCount + 1), node.RightChild); + } + + return result; + } + + // Recursive SetValue helper method. + private IAvlNode SetValue(int index, object value, IAvlNode node) + { + // Preconditions. + Debug.Assert(index >= 0 && index < node.Count); + Debug.Assert(node != AvlNode.NullNode); + + IAvlNode result; + int leftCount = node.LeftChild.Count; + + // If the node has been found. + if(index == leftCount) + { + // Create new node with the new value. + result = new AvlNode(value, node.LeftChild, node.RightChild); + } + // Else if the node is in the left tree. + else if(index < leftCount) + { + // Create new node and move search to the left child. The new + // node will reuse the right child subtree. + result = new AvlNode( + node.Data, + SetValue(index, value, node.LeftChild), + node.RightChild); + } + // Else if the node is in the right tree. + else + { + // Create new node and move search to the right child. The new + // node will reuse the left child subtree. + result = new AvlNode( + node.Data, + node.LeftChild, + SetValue(index - (leftCount + 1), value, node.RightChild)); + } + + return result; + } + + // Gets a subtree from the tree pool at the specified height. + private static IAvlNode GetSubTree(int height) + { + // Preconditions. + Debug.Assert(height >= 0 && height <= TreePool.Height); + + IAvlNode result = TreePool; + + // How far to descend into the tree pool to get the subtree. + int d = TreePool.Height - height; + + // Descend down the tree pool until arriving at the root of the + // subtree. + for(int i = 0; i < d; i++) + { + result = result.LeftChild; + } + + // Postconditions. + Debug.Assert(result.Height == height); + + return result; + } + + // Recursive Insert helper method. + private IAvlNode Insert(int index, object value, IAvlNode node) + { + // Preconditions. + Debug.Assert(index >= 0 && index <= Count); + Debug.Assert(node != null); + + /* + * The insertion algorithm searches for the correct place to add a + * new node at the bottom of the tree using the specified index. + */ + + IAvlNode result; + + // If the bottom of the tree has not yet been reached. + if(node != AvlNode.NullNode) + { + int leftCount = node.LeftChild.Count; + + // If we need to descend to the left. + if(index <= leftCount) + { + // Create new node and move search to the left child. The + // new node will reuse the right child subtree. + result = new AvlNode( + node.Data, + Insert(index, value, node.LeftChild), + node.RightChild); + } + // Else we need to descend to the right. + else + { + // Create new node and move search to the right child. The + // new node will reuse the left child subtree. + result = new AvlNode( + node.Data, + node.LeftChild, + Insert(index - (leftCount + 1), + value, + node.RightChild)); + } + } + // Else the bottom of the tree has been reached. + else + { + // Create new node at the specified index. + result = new AvlNode(value, AvlNode.NullNode, AvlNode.NullNode); + } + + /* + * This check isn't necessary if a node has already been rebalanced + * after an insertion. AVL tree insertions never require more than + * one rebalancing. However, it's easier to go ahead and check at + * this point since we're using recursion. This may need optimizing + * in the future. + */ + + // If the node is not balanced. + if(!result.IsBalanced()) + { + // Rebalance node. + result = result.Balance(); + } + + // Postconditions. + Debug.Assert(result.IsBalanced()); + + return result; + } + + // Recursive RemoveAt helper method. + private IAvlNode RemoveAt(int index, IAvlNode node) + { + // Preconditions. + Debug.Assert(index >= 0 && index < Count); + Debug.Assert(node != AvlNode.NullNode); + + IAvlNode newNode = AvlNode.NullNode; + + int leftCount = node.LeftChild.Count; + + // If the node has been found. + if(index == leftCount) + { + newNode = node.Remove(); + } + // Else if the node is in the left tree. + else if(index < leftCount) + { + // Create new node and move search to the left child. The new + // node will reuse the right child subtree. + newNode = new AvlNode( + node.Data, + RemoveAt(index, node.LeftChild), + node.RightChild); + } + // Else if the node is in the right tree. + else + { + // Create new node and move search to the right child. The new + // node will reuse the left child subtree. + newNode = new AvlNode( + node.Data, + node.LeftChild, + RemoveAt(index - (leftCount + 1), node.RightChild)); + } + + // If the node is out of balance. + if(!newNode.IsBalanced()) + { + // Rebalance node. + newNode = newNode.Balance(); + } + + // Postconditions. + Debug.Assert(newNode.IsBalanced()); + + return newNode; + } + + #endregion + + /// + /// Gets the number of elements contained in the ArrayList. + /// + public int Count + { + get + { + return count; + } + } + + #endregion + + #region IEnumerable Members + + /// + /// Returns an enumerator that can iterate through the ArrayList. + /// + /// + /// An IEnumerator that can be used to iterate through the ArrayList. + /// + public IEnumerator GetEnumerator() + { + return new AvlEnumerator(root, Count); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalEnumerator.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalEnumerator.cs new file mode 100644 index 00000000..9612cbbd --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalEnumerator.cs @@ -0,0 +1,211 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.Collections; +using System.Diagnostics; + +namespace Sanford.Collections.Immutable +{ + /// + /// Provides functionality for enumerating a RandomAccessList. + /// + internal class RalEnumerator : IEnumerator + { + #region Enumerator Members + + #region Instance Fields + + // The object at the current position. + private object current = null; + + // The current index position. + private int index; + + // For storing and traversing the nodes in the tree. + private System.Collections.Stack treeStack = new System.Collections.Stack(); + + // The first top node in the list. + private RalTopNode head; + + // The current top node in the list. + private RalTopNode currentTopNode; + + // The number of nodes in the list. + private int count; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the Enumerator with the specified + /// head of the list and the number of nodes in the list. + /// + /// + /// The head of the list. + /// + /// + /// The number of nodes in the list. + /// + public RalEnumerator(RalTopNode head, int count) + { + this.head = head; + this.count = count; + + if(count > 0) + { + Debug.Assert(head != null); + } + + Reset(); + } + + #endregion + + #endregion + + #region IEnumerator Members + + /// + /// Sets the enumerator to its initial position, which is before + /// the first element in the random access list. + /// + public void Reset() + { + index = -1; + currentTopNode = head; + treeStack.Clear(); + + // If the list is not empty. + if(count > 0) + { + // Push the first node in the list onto the stack. + treeStack.Push(head.Root); + } + } + + /// + /// Gets the current element in the random access list. + /// + /// + /// The enumerator is positioned before the first element in the + /// random access list or after the last element. + /// + public object Current + { + get + { + // Preconditions. + if(index < 0 || index >= count) + { + throw new InvalidOperationException( + "The enumerator is positioned before the first " + + "element of the collection or after the last element."); + } + + return current; + } + } + + /// + /// Advances the enumerator to the next element in the random access + /// list. + /// + /// + /// true if the enumerator was successfully advanced to the + /// next element; false if the enumerator has passed the end + /// of the collection. + /// + public bool MoveNext() + { + // Move index to the next position. + index++; + + // If the index has moved beyond the end of the list, return false. + if(index >= count) + return false; + + RalTreeNode currentNode; + + // Get the node at the top of the stack. + currentNode = (RalTreeNode)treeStack.Peek(); + + // Get the value at the top of the stack. + current = currentNode.Value; + + // If there are still left children to traverse. + if(currentNode.LeftChild != null) + { + // If the left child is not null, the right child should not be + // null either. + Debug.Assert(currentNode.RightChild != null); + + // Push left child onto stack. + treeStack.Push(currentNode.LeftChild); + } + // Else the bottom of the tree has been reached. + else + { + // If the left child is null, the right child should be null, + // too. + Debug.Assert(currentNode.RightChild == null); + + // Move back up in the tree to the parent node. + treeStack.Pop(); + + RalTreeNode previousNode; + + // Whild the stack is not empty. + while(treeStack.Count > 0) + { + // Get the previous node. + previousNode = (RalTreeNode)treeStack.Peek(); + + // If the bottom of the left tree has been reached. + if(currentNode == previousNode.LeftChild) + { + // Push the right child onto the stack so that the + // right tree will now be traversed. + treeStack.Push(previousNode.RightChild); + + // Finished. + break; + } + // Else the bottom of the right tree has been reached. + else + { + // Keep track of the current node. + currentNode = previousNode; + + // Pop the stack to move back up the tree. + treeStack.Pop(); + } + } + + // If the stack is empty. + if(treeStack.Count == 0) + { + // Move to the next tree in the list. + currentTopNode = currentTopNode.NextNode; + + // If the end of the list has not yet been reached. + if(currentTopNode != null) + { + // Begin with the next tree. + treeStack.Push(currentTopNode.Root); + } + } + } + + return true; + } + + #endregion + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalTopNode.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalTopNode.cs new file mode 100644 index 00000000..4e834f73 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalTopNode.cs @@ -0,0 +1,155 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Collections.Immutable +{ + /// + /// Represents the top nodes in a RandomAccessList. + /// + [ImmutableObject(true)] + internal class RalTopNode + { + #region RalTopNode Members + + #region Instance Fields + + // The root of the tree the top node represents. + private readonly RalTreeNode root; + + // The next top node in the list. + private readonly RalTopNode nextNode; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the RalTopNode with the specified + /// root of the tree this node represents and the next top node in the + /// list. + /// + /// + /// The root node of the tree this top node represents. + /// + /// + /// The next top node in the list. + /// + public RalTopNode(RalTreeNode root, RalTopNode nextNode) + { + Debug.Assert(root != null); + + this.root = root; + this.nextNode = nextNode; + } + + #endregion + + #region Methods + + /// + /// Gets the value at the specified element in the random access list. + /// + /// + /// An integer that represents the position of the random access list + /// element to get. + /// + /// + /// The value at the specified position in the random access list. + /// + public object GetValue(int index) + { + int i = index; + RalTopNode currentNode = this; + + // Find the top node containing the specified element. + while(i >= currentNode.Root.Count) + { + i -= currentNode.Root.Count; + currentNode = currentNode.NextNode; + + Debug.Assert(currentNode != null); + } + + return currentNode.Root.GetValue(i); + } + + /// + /// Sets the specified element in the current random access list to the + /// specified value. + /// + /// + /// The new value for the specified element. + /// + /// + /// An integer that represents the position of the random access list + /// element to set. + /// + /// + /// A new random access list top node with the element at the specified + /// position set to the specified value. + /// + public RalTopNode SetValue(object value, int index) + { + RalTopNode result; + + // If the element is in the tree represented by the current top + // node. + if(index < Root.Count) + { + // Descend into the tree. + result = new RalTopNode( + root.SetValue(value, index), + NextNode); + } + // Else the element is further along in the list. + else + { + // Move to the next top node. + result = new RalTopNode( + root, + NextNode.SetValue(value, index - Root.Count)); + } + + return result; + } + + #endregion + + #region Properties + + /// + /// Gets the root node represented by the top node. + /// + public RalTreeNode Root + { + get + { + return root; + } + } + + /// + /// Gets the next top node in the random access list. + /// + public RalTopNode NextNode + { + get + { + return nextNode; + } + } + + #endregion + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalTreeNode.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalTreeNode.cs new file mode 100644 index 00000000..bf1051b3 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RAL Helper Classes/RalTreeNode.cs @@ -0,0 +1,250 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Collections.Immutable +{ + /// + /// Represents subtree nodes within random access lists. + /// + [ImmutableObject(true)] + internal class RalTreeNode + { + #region RalTreeNode Members + + #region Instance Fields + + // The value represented by this node. + private readonly object value; + + // The number of nodes in the tree. + private readonly int count; + + // Left and right children. + private readonly RalTreeNode leftChild = null; + private readonly RalTreeNode rightChild = null; + + #endregion + + #region Construction + + /// + /// Initializes an instance of the RandomAccessListNode with the + /// specified value, left child, and right child. + /// + /// + /// The value to store in the node. + /// + /// + /// The left child. + /// + /// + /// The right child. + /// + public RalTreeNode( + object value, + RalTreeNode leftChild, + RalTreeNode rightChild) + { + this.value = value; + this.leftChild = leftChild; + this.rightChild = rightChild; + + count = 1; + + if(leftChild != null) + { + count += leftChild.Count * 2; + + Debug.Assert(rightChild != null); + Debug.Assert(count == 1 + leftChild.Count + rightChild.Count); + } + } + + #endregion + + #region Methods + + /// + /// Gets the value at the specified element in the random access list + /// subtree. + /// + /// + /// An integer that represents the position of the random access list + /// subtree element to get. + /// + /// + /// The value at the specified position in the random access list + /// subtree. + /// + public object GetValue(int index) + { + Debug.Assert(index < Count); + + return GetValue(index, this); + } + + // Recursive method for getting the value at the specified position. + private object GetValue(int index, RalTreeNode node) + { + object result; + + // If the position of the value to get has been reached. + if(index == 0) + { + // Get the value. + result = node.Value; + } + // Else the position of the value to get has not been reached. + else + { + int n = node.Count / 2; + + // If the value is in the left subtree. + if(index <= n) + { + Debug.Assert(node.LeftChild != null); + + // Descend into the left subtree. + result = GetValue(index - 1, node.LeftChild); + } + // Else the value is in the right subtree. + else + { + Debug.Assert(node.RightChild != null); + + // Descend into the right subtree. + result = GetValue(index - 1 - n, node.RightChild); + } + } + + return result; + } + + /// + /// Sets the specified element in the current random access list + /// subtree to the specified value. + /// + /// + /// The new value for the specified element. + /// + /// + /// An integer that represents the position of the random access list + /// subtree element to set. + /// + /// + /// A new random access list tree node with the element at the specified + /// position set to the specified value. + /// + public RalTreeNode SetValue(object value, int index) + { + return SetValue(value, index, this); + } + + // Recursive method for setting the value at the specified position. + private RalTreeNode SetValue(object value, int index, RalTreeNode node) + { + RalTreeNode result; + + // If the position of the value to set has been reached. + if(index == 0) + { + // Set the value. + result = new RalTreeNode( + value, + node.LeftChild, + node.RightChild); + } + // Else if the position of the value to set has not been reached. + else + { + Debug.Assert(node.LeftChild != null); + + int n = Count / 2; + + // If the value is in the left subtree. + if(index <= n) + { + // Descend into the left subtree. + result = new RalTreeNode( + node.Value, + node.LeftChild.SetValue(value, index - 1), + node.RightChild); + } + // Else if the value is in the right subtree. + else + { + Debug.Assert(node.RightChild != null); + + // Descend into the right subtree. + result = new RalTreeNode( + node.Value, + node.LeftChild, + node.RightChild.SetValue(value, index - 1 - n)); + } + } + + return result; + } + + #endregion + + #region Properties + + /// + /// Gets the number of nodes in the tree. + /// + public int Count + { + get + { + return count; + } + } + + /// + /// Gets the left child. + /// + public RalTreeNode LeftChild + { + get + { + return leftChild; + } + } + + /// + /// Gets the right child. + /// + public RalTreeNode RightChild + { + get + { + return rightChild; + } + } + + /// + /// Gets the value represented by this node. + /// + public object Value + { + get + { + return value; + } + } + + #endregion + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RandomAccessList.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RandomAccessList.cs new file mode 100644 index 00000000..76ce0b3f --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/RandomAccessList.cs @@ -0,0 +1,302 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Collections.Immutable +{ + /// + /// Implements Chris Okasaki's random access list. + /// + [ImmutableObject(true)] + public class RandomAccessList : IEnumerable + { + #region RandomAccessList Members + + #region Class Fields + + /// + /// Represents an empty random access list. + /// + public static readonly RandomAccessList Empty = new RandomAccessList(); + + #endregion + + #region Instance Fields + + // The number of elements in the random access list. + private readonly int count; + + // The first top node in the list. + private readonly RalTopNode first; + + // A random access list representing the head of the current list. + private RandomAccessList head = null; + + // A random access list representing the tail of the current list. + private RandomAccessList tail = null; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the RandomAccessList class. + /// + public RandomAccessList() + { + count = 0; + first = null; + } + + /// + /// Initializes a new instance of the RandomAccessList class with the + /// specified first top node and the number of elements in the list. + /// + /// + /// The first top node in the list. + /// + /// + /// The number of nodes in the list. + /// + private RandomAccessList(RalTopNode first, int count) + { + this.first = first; + this.count = count; + } + + #endregion + + #region Methods + + /// + /// Prepends a value to the random access list. + /// + /// + /// The value to prepend to the list. + /// + /// + /// A new random access list with the specified value prepended to the + /// list. + /// + public RandomAccessList Cons(object value) + { + RandomAccessList result; + + // If the list is empty, or there is only one tree in the list, or + // the first tree is smaller than the second tree. + if(Count == 0 || + first.NextNode == null || + first.Root.Count < first.NextNode.Root.Count) + { + // Create a new first node with the specified value. + RalTreeNode newRoot = new RalTreeNode(value, null, null); + + // Create a new random access list. + result = new RandomAccessList( + new RalTopNode(newRoot, first), + Count + 1); + } + // Else the first and second trees in the list are the same size. + else + { + Debug.Assert(first.Root.Count == first.NextNode.Root.Count); + + // Create a new first node with the old first and second node + // as the left and right children respectively. + RalTreeNode newRoot = new RalTreeNode( + value, + first.Root, + first.NextNode.Root); + + // Create a new random access list. + result = new RandomAccessList( + new RalTopNode(newRoot, first.NextNode.NextNode), + Count + 1); + } + + return result; + } + + /// + /// Gets the value at the specified position in the current + /// RandomAccessList. + /// + /// + /// An integer that represents the position of the RandomAccessList + /// element to get. + /// + /// + /// The value at the specified position in the RandomAccessList. + /// + /// + /// index is outside the range of valid indexes for the current + /// RandomAccessList. + /// + public object GetValue(int index) + { + // Precondition. + if(index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException("index", index, + "Index out of range."); + } + + return first.GetValue(index); + } + + /// + /// Sets the specified element in the current RandomAccessList to the + /// specified value. + /// + /// + /// The new value for the specified element. + /// + /// + /// An integer that represents the position of the RandomAccessList + /// element to set. + /// + /// + /// A new RandomAccessList with the element at the specified position + /// set to the specified value. + /// + /// + /// index is outside the range of valid indexes for the current + /// RandomAccessList. + /// + public RandomAccessList SetValue(object value, int index) + { + // Precondition. + if(index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException("index", index, + "Index out of range."); + } + + return new RandomAccessList(first.SetValue(value, index), Count); + } + + #endregion + + #region Properties + + /// + /// Gets the number of elements in the RandomAccessList. + /// + public int Count + { + get + { + return count; + } + } + + /// + /// Gets a RandomAccessList with first element of the current + /// RandomAccessList. + /// + /// + /// If the RandomAccessList is empty. + /// + public RandomAccessList Head + { + get + { + // Preconditions. + if(Count == 0) + { + throw new InvalidOperationException( + "Cannot get the head of an empty random access list."); + } + + if(head == null) + { + RalTreeNode newRoot = new RalTreeNode( + first.Root.Value, null, null); + + RalTopNode newFirst = new RalTopNode(newRoot, null); + + head = new RandomAccessList(newFirst, 1); + } + + return head; + } + } + + /// + /// Gets a RandomAccessList with all but the first element of the + /// current RandomAccessList. + /// + /// + /// If the RandomAccessList is empty. + /// + public RandomAccessList Tail + { + get + { + // Precondition. + if(Count == 0) + { + throw new InvalidOperationException( + "Cannot get the tail of an empty random access list."); + } + + if(tail == null) + { + if(Count == 1) + { + tail = Empty; + } + else + { + if(first.Root.Count > 1) + { + RalTreeNode left = first.Root.LeftChild; + RalTreeNode right = first.Root.RightChild; + + RalTopNode newSecond = new RalTopNode( + right, first.NextNode); + RalTopNode newFirst = new RalTopNode( + left, newSecond); + + tail = new RandomAccessList(newFirst, Count - 1); + } + else + { + tail = new RandomAccessList(first.NextNode, Count - 1); + } + } + } + + return tail; + } + } + + #endregion + + #endregion + + #region IEnumerable Members + + /// + /// Returns an IEnumerator for the RandomAccessList. + /// + /// + /// An IEnumerator for the RandomAccessList. + /// + public IEnumerator GetEnumerator() + { + return new RalEnumerator(first, Count); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/SortedList.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/SortedList.cs new file mode 100644 index 00000000..074fdddc --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/SortedList.cs @@ -0,0 +1,509 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Collections.Immutable +{ + /// + /// Represents a collection of key-and-value pairs that are sorted by the + /// keys and are accessible by key. + /// + [ImmutableObject(true)] + public class SortedList : IEnumerable + { + #region SortedList Members + + #region Class Fields + + /// + /// An empty SortedList. + /// + public static readonly SortedList Empty = new SortedList(); + + #endregion + + #region Instance Fields + + // The compare object used for making comparisions. + private IComparer comparer = null; + + // The root of the AVL tree. + private IAvlNode root = AvlNode.NullNode; + + // Represents the method responsible for comparing keys. + private delegate int CompareHandler(object x, object y); + + // The actual delegate to use for comparing keys. + private CompareHandler compareHandler; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the SortedList class that is empty + /// and is sorted according to the IComparable interface implemented by + /// each key added to the SortedList. + /// + public SortedList() + { + InitializeCompareHandler(); + } + + /// + /// Initializes a new instance of the SortedList class that is empty + /// and is sorted according to the specified IComparer interface. + /// + /// + /// The IComparer implementation to use when comparing keys, or a null + /// reference to use the IComparable implementation of each key. + /// + public SortedList(IComparer comparer) + { + this.comparer = comparer; + + InitializeCompareHandler(); + } + + /// + /// Initializes a new instance of the SortedList class with the + /// specified root node and the IComparer interface to use for sorting + /// keys. + /// + /// + /// The root of the AVL tree. + /// + /// + /// The IComparer implementation to use when comparing keys, or a null + /// reference to use the IComparable implementation of each key. + /// + private SortedList(IAvlNode root, IComparer comparer) + { + this.root = root; + this.comparer = comparer; + + InitializeCompareHandler(); + } + + #endregion + + #region Methods + + /// + /// Adds an element with the specified key and value to the SortedList. + /// + /// + /// The key of the element to add. + /// + /// + /// The value of the element to add. The value can be a null reference. + /// + /// + /// A new SortedList with the specified key and value added to the + /// previous SortedList. + /// + /// + /// key is a null reference. + /// + /// + /// An element with the specified key already exists in the SortedList, + /// or The SortedList is set to use the IComparable interface, and key + /// does not implement the IComparable interface. + /// + public SortedList Add(object key, object value) + { + // Preconditions. + if(key == null) + { + throw new ArgumentNullException("key", + "Key cannot be null."); + } + else if(comparer == null && !(key is IComparable)) + { + throw new ArgumentException( + "Key does not implement IComparable interface."); + } + + return new SortedList( + Add(key, value, root), + comparer); + } + + /// + /// Determines whether the SortedList contains a specific key. + /// + /// + /// The key to locate in the SortedList. + /// + /// + /// true if the SortedList contains an element with the + /// specified key; otherwise, false. + /// + public bool Contains(object key) + { + return this[key] != null; + } + + /// + /// Returns an IDictionaryEnumerator that can iterate through the + /// SortedList. + /// + /// + /// An IDictionaryEnumerator for the SortedList. + /// + public IDictionaryEnumerator GetEnumerator() + { + return new SortedListEnumerator(root); + } + + /// + /// Removes the element with the specified key from SortedList. + /// + /// + /// + /// + /// The key of the element to remove. + /// + /// + /// key is a null reference. + /// + /// + /// The SortedList is set to use the IComparable interface, and key + /// does not implement the IComparable interface. + /// + public SortedList Remove(object key) + { + // Preconditions. + if(key == null) + { + throw new ArgumentNullException("key", + "Key cannot be null."); + } + else if(comparer == null && !(key is IComparable)) + { + throw new ArgumentException( + "Key does not implement IComparable interface."); + } + + return new SortedList(Remove(key, root), comparer); + } + + // Initializes the delegate to use for making key comparisons. + private void InitializeCompareHandler() + { + if(comparer == null) + { + compareHandler = new CompareHandler(CompareWithoutComparer); + } + else + { + compareHandler = new CompareHandler(CompareWithComparer); + } + } + + // Method for comparing keys using the IComparable interface. + private int CompareWithoutComparer(object x, object y) + { + return ((IComparable)x).CompareTo(y); + } + + // Method for comparing keys using the provided comparer. + private int CompareWithComparer(object x, object y) + { + return comparer.Compare(x, y); + } + + // Adds key/value pair to the internal AVL tree. + private IAvlNode Add(object key, object value, IAvlNode node) + { + IAvlNode result; + + // If the bottom of the tree has been reached. + if(node == AvlNode.NullNode) + { + // Create new node representing the new key/value pair. + result = new AvlNode( + new DictionaryEntry(key, value), + AvlNode.NullNode, + AvlNode.NullNode); + } + // Else the bottom of the tree has not been reached. + else + { + DictionaryEntry entry = (DictionaryEntry)node.Data; + int compareResult = compareHandler(key, entry.Key); + + // If the specified key is less than the current key. + if(compareResult < 0) + { + // Create new node and continue searching to the left. + result = new AvlNode( + node.Data, + Add(key, value, node.LeftChild), + node.RightChild); + } + // Else the specified key is greater than the current key. + else if(compareResult > 0) + { + // Create new node and continue searching to the right. + result = new AvlNode( + node.Data, + node.LeftChild, + Add(key, value, node.RightChild)); + } + // Else the specified key is equal to the current key. + else + { + // Throw exception. Duplicate keys are not allowed. + throw new ArgumentException( + "Item is already in the collection."); + } + } + + // If the current node is not balanced. + if(!result.IsBalanced()) + { + // Balance node. + result = result.Balance(); + } + + return result; + } + + // Search for the node with the specified key. + private object Search(object key, IAvlNode node) + { + object result; + + // If the key is not in the SortedList. + if(node == AvlNode.NullNode) + { + // Result is null. + result = null; + } + // Else the key has not yet been found. + else + { + DictionaryEntry entry = (DictionaryEntry)node.Data; + int compareResult = compareHandler(key, entry.Key); + + // If the specified key is less than the current key. + if(compareResult < 0) + { + // Search to the left. + result = Search(key, node.LeftChild); + } + // Else if the specified key is greater than the current key. + else if(compareResult > 0) + { + // Search to the right. + result = Search(key, node.RightChild); + } + // Else the key has been found. + else + { + // Get value. + result = entry.Value; + } + } + + return result; + } + + // Remove the node with the specified key. + private IAvlNode Remove(object key, IAvlNode node) + { + IAvlNode result; + + // The the key does not exist in the SortedList. + if(node == AvlNode.NullNode) + { + // Result is null. + result = node; + } + // Else the key has not yet been found. + else + { + DictionaryEntry entry = (DictionaryEntry)node.Data; + int compareResult = compareHandler(key, entry.Key); + + // If the specified key is less than the current key. + if(compareResult < 0) + { + // Create node and continue searching to the left. + result = new AvlNode( + node.Data, + Remove(key, node.LeftChild), + node.RightChild); + } + // Else if the specified key is greater than the current key. + else if(compareResult > 0) + { + // Create node and continue searching to the right. + result = new AvlNode( + node.Data, + node.LeftChild, + Remove(key, node.RightChild)); + } + // Else the node to remove has been found. + else + { + // Remove node. + result = node.Remove(); + } + } + + // If the node is out of balance. + if(!result.IsBalanced()) + { + // Rebalance node. + result = result.Balance(); + } + + // Postconditions. + Debug.Assert(result.IsBalanced()); + + return result; + } + + #endregion + + #region Properties + + /// + /// Gets the value associated with the specified key. + /// + public object this[object key] + { + get + { + return Search(key, root); + } + } + + /// + /// Gets the number of elements contained in the SortedList. + /// + public int Count + { + get + { + return root.Count; + } + } + + #endregion + + #region SortedListEnumerator Class + + /// + /// Provides functionality for iterating through a SortedList. + /// + private class SortedListEnumerator : IDictionaryEnumerator + { + #region SortedListEnumerator Members + + #region Instance Fields + + private AvlEnumerator enumerator; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the SortedListEnumerator class + /// with the specified root of the AVL tree to iterate over. + /// + /// + /// The root of the AVL tree the SortedList uses internally. + /// + public SortedListEnumerator(IAvlNode root) + { + enumerator = new AvlEnumerator(root); + } + + #endregion + + #endregion + + #region IDictionaryEnumerator Members + + public object Key + { + get + { + DictionaryEntry entry = (DictionaryEntry)enumerator.Current; + + return entry.Key; + } + } + + public object Value + { + get + { + DictionaryEntry entry = (DictionaryEntry)enumerator.Current; + + return entry.Value; + } + } + + public DictionaryEntry Entry + { + get + { + DictionaryEntry entry = (DictionaryEntry)enumerator.Current; + + return entry; + } + } + + #endregion + + #region IEnumerator Members + + public void Reset() + { + enumerator.Reset(); + } + + public object Current + { + get + { + return enumerator.Current; + } + } + + public bool MoveNext() + { + return enumerator.MoveNext(); + } + + #endregion + } + + #endregion + + #endregion + + #region IEnumerable Members + + IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return new AvlEnumerator(root); + } + + #endregion + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/Stack.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/Stack.cs new file mode 100644 index 00000000..f429ded1 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Immutable/Stack.cs @@ -0,0 +1,315 @@ +/* + * Created by: Leslie Sanford + * + * Last modified: 02/23/2005 + * + * Contact: jabberdabber@hotmail.com + */ + +using System; +using System.Collections; +using System.ComponentModel; + +namespace Sanford.Collections.Immutable +{ + /// + /// Represents a simple last-in-first-out collection of objects. + /// + [ImmutableObject(true)] + public class Stack : IEnumerable + { + #region Stack Members + + #region Class Fields + + /// + /// An empty Stack. + /// + public static readonly Stack Empty = new Stack(); + + #endregion + + #region Instance Fields + + // The number of elements in the stack. + private readonly int count; + + // The top node in the stack. + private Node top = null; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the Stack class. + /// + public Stack() + { + count = 0; + } + + /// + /// Initializes a new instance of the Stack class with the + /// specified top node and the number of elements in the stack. + /// + /// + /// The top node in the stack. + /// + /// + /// The number of elements in the stack. + /// + private Stack(Node top, int count) + { + this.top = top; + this.count = count; + } + + #endregion + + #region Methods + + /// + /// Inserts an object at the top of the Stack. + /// + /// + /// The Object to push onto the Stack. + /// + /// + /// A new stack with the specified object on the top of the stack. + /// + public Stack Push(object obj) + { + Node newTop = new Node(obj, top); + + return new Stack(newTop, Count + 1); + } + + /// + /// Removes the object at the top of the Stack. + /// + /// + /// A new stack with top of the previous stack removed. + /// + /// + /// The Stack is empty. + /// + public Stack Pop() + { + // Preconditions. + if(Count == 0) + { + throw new InvalidOperationException( + "Cannot pop an empty stack."); + } + + Stack result; + + if(Count - 1 == 0) + { + result = Empty; + } + else + { + result = new Stack(top.Next, Count - 1); + } + + return result; + } + + #endregion + + #region Properties + + /// + /// Gets the number of elements in the Stack. + /// + public int Count + { + get + { + return count; + } + } + + /// + /// Gets the top of the stack. + /// + /// + /// The Stack is empty. + /// + public object Top + { + get + { + if(Count == 0) + { + throw new InvalidOperationException( + "Cannot access the top when the stack is empty."); + } + + return top.Value; + } + } + + #endregion + + #region Node Class + + /// + /// Represents a node in the stack. + /// + private class Node + { + private Node next = null; + + private object value; + + public Node(object value, Node next) + { + this.value = value; + this.next = next; + } + + public Node Next + { + get + { + return next; + } + } + + public object Value + { + get + { + return value; + } + } + } + + #endregion + + #region StackEnumerator Class + + /// + /// Provides functionality for iterating over the Stack class. + /// + private class StackEnumerator : IEnumerator + { + #region StackEnumerator Members + + #region Instance Fields + + // The stack to iterate over. + private Stack owner; + + // The current index into the stack. + private int index; + + // The current node. + private Node current; + + // The next node in the stack. + private Node next; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the StackEnumerator class with + /// the specified stack to iterate over. + /// + /// + /// The Stack to iterate over. + /// + public StackEnumerator(Stack owner) + { + this.owner = owner; + + Reset(); + } + + #endregion + + #region IEnumerator Members + + /// + /// Sets the enumerator to its initial position, which is before + /// the first element in the Stack. + /// + public void Reset() + { + index = -1; + + next = owner.top; + } + + /// + /// Gets the current element in the Stack. + /// + /// + /// The enumerator is positioned before the first element of the + /// Stack or after the last element. + /// + public object Current + { + get + { + // Preconditions. + if(index < 0 || index >= owner.Count) + { + throw new InvalidOperationException( + "The enumerator is positioned before the first " + + "element of the collection or after the last element."); + } + + return current.Value; + } + } + + /// + /// Advances the enumerator to the next element of the Stack. + /// + /// + public bool MoveNext() + { + index++; + + if(index >= owner.Count) + { + return false; + } + + current = next; + next = next.Next; + + return true; + } + + #endregion + } + + #endregion + + #endregion + + #endregion + + #region IEnumerable Members + + /// + /// Returns an IEnumerator for the Stack. + /// + /// + /// An IEnumerator for the Stack. + /// + public IEnumerator GetEnumerator() + { + return new StackEnumerator(this); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/PriorityQueue.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/PriorityQueue.cs new file mode 100644 index 00000000..65411587 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/PriorityQueue.cs @@ -0,0 +1,887 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; +using System.Diagnostics; + +namespace Sanford.Collections +{ + /// + /// Represents the priority queue data structure. + /// + public class PriorityQueue : ICollection + { + #region PriorityQueue Members + + #region Fields + + // The maximum level of the skip list. + private const int LevelMaxValue = 16; + + // The probability value used to randomly select the next level value. + private const double Probability = 0.5; + + // The current level of the skip list. + private int currentLevel = 1; + + // The header node of the skip list. + private Node header = new Node(null, LevelMaxValue); + + // Used to generate node levels. + private Random rand = new Random(); + + // The number of elements in the PriorityQueue. + private int count = 0; + + // The version of this PriorityQueue. + private long version = 0; + + // Used for comparing and sorting elements. + private IComparer comparer; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the PriorityQueue class. + /// + /// + /// The PriorityQueue will cast its elements to the IComparable + /// interface when making comparisons. + /// + public PriorityQueue() + { + comparer = new DefaultComparer(); + } + + /// + /// Initializes a new instance of the PriorityQueue class with the + /// specified IComparer. + /// + /// + /// The IComparer to use for comparing and ordering elements. + /// + /// + /// If the specified IComparer is null, the PriorityQueue will cast its + /// elements to the IComparable interface when making comparisons. + /// + public PriorityQueue(IComparer comparer) + { + // If no comparer was provided. + if(comparer == null) + { + // Use the DefaultComparer. + this.comparer = new DefaultComparer(); + } + // Else a comparer was provided. + else + { + // Use the provided comparer. + this.comparer = comparer; + } + } + + #endregion + + #region Methods + + /// + /// Enqueues the specified element into the PriorityQueue. + /// + /// + /// The element to enqueue into the PriorityQueue. + /// + /// + /// If element is null. + /// + public virtual void Enqueue(object element) + { + #region Require + + if(element == null) + { + throw new ArgumentNullException("element"); + } + + #endregion + + Node x = header; + Node[] update = new Node[LevelMaxValue]; + int nextLevel = NextLevel(); + + // Find the place in the queue to insert the new element. + for(int i = currentLevel - 1; i >= 0; i--) + { + while(x[i] != null && comparer.Compare(x[i].Element, element) > 0) + { + x = x[i]; + } + + update[i] = x; + } + + // If the new node's level is greater than the current level. + if(nextLevel > currentLevel) + { + for(int i = currentLevel; i < nextLevel; i++) + { + update[i] = header; + } + + // Update level. + currentLevel = nextLevel; + } + + // Create new node. + Node newNode = new Node(element, nextLevel); + + // Insert the new node into the list. + for(int i = 0; i < nextLevel; i++) + { + newNode[i] = update[i][i]; + update[i][i] = newNode; + } + + // Keep track of the number of elements in the PriorityQueue. + count++; + + version++; + + #region Ensure + + Debug.Assert(Contains(element), "Contains Test", "Contains test for element " + element.ToString() + " failed."); + + #endregion + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Removes the element at the head of the PriorityQueue. + /// + /// + /// The element at the head of the PriorityQueue. + /// + /// + /// If Count is zero. + /// + public virtual object Dequeue() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException( + "Cannot dequeue into an empty PriorityQueue."); + } + + #endregion + + // Get the first item in the queue. + object element = header[0].Element; + + // Keep track of the node that is about to be removed. + Node oldNode = header[0]; + + // Update the header so that its pointers that pointed to the + // node to be removed now point to the node that comes after it. + for(int i = 0; i < currentLevel && header[i] == oldNode; i++) + { + header[i] = oldNode[i]; + } + + // Update the current level of the list in case the node that + // was removed had the highest level. + while(currentLevel > 1 && header[currentLevel - 1] == null) + { + currentLevel--; + } + + // Keep track of how many items are in the queue. + count--; + + version++; + + #region Ensure + + Debug.Assert(count >= 0); + + #endregion + + #region Invariant + + AssertValid(); + + #endregion + + return element; + } + + /// + /// Removes the specified element from the PriorityQueue. + /// + /// + /// The element to remove. + /// + /// + /// If element is null + /// + public virtual void Remove(object element) + { + #region Require + + if(element == null) + { + throw new ArgumentNullException("element"); + } + + #endregion + + Node x = header; + Node[] update = new Node[LevelMaxValue]; + int nextLevel = NextLevel(); + + // Find the specified element. + for(int i = currentLevel - 1; i >= 0; i--) + { + while(x[i] != null && comparer.Compare(x[i].Element, element) > 0) + { + x = x[i]; + } + + update[i] = x; + } + + x = x[0]; + + // If the specified element was found. + if(x != null && comparer.Compare(x.Element, element) == 0) + { + // Remove element. + for(int i = 0; i < currentLevel && update[i][i] == x; i++) + { + update[i][i] = x[i]; + } + + // Update list level. + while(currentLevel > 1 && header[currentLevel - 1] == null) + { + currentLevel--; + } + + // Keep track of the number of elements in the PriorityQueue. + count--; + + version++; + } + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Returns a value indicating whether the specified element is in the + /// PriorityQueue. + /// + /// + /// The element to test. + /// + /// + /// true if the element is in the PriorityQueue; otherwise + /// false. + /// + public virtual bool Contains(object element) + { + #region Guard + + if(element == null) + { + return false; + } + + #endregion + + bool found; + Node x = header; + + // Find the specified element. + for(int i = currentLevel - 1; i >= 0; i--) + { + while(x[i] != null && comparer.Compare(x[i].Element, element) > 0) + { + x = x[i]; + } + } + + x = x[0]; + + // If the element is in the PriorityQueue. + if(x != null && comparer.Compare(x.Element, element) == 0) + { + found = true; + } + // Else the element is not in the PriorityQueue. + else + { + found = false; + } + + return found; + } + + /// + /// Returns the element at the head of the PriorityQueue without + /// removing it. + /// + /// + /// The element at the head of the PriorityQueue. + /// + public virtual object Peek() + { + #region Require + + if(Count == 0) + { + throw new InvalidOperationException( + "Cannot peek into an empty PriorityQueue."); + } + + #endregion + + return header[0].Element; + } + + /// + /// Removes all elements from the PriorityQueue. + /// + public virtual void Clear() + { + header = new Node(null, LevelMaxValue); + + currentLevel = 1; + + count = 0; + + version++; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Returns a synchronized wrapper of the specified PriorityQueue. + /// + /// + /// The PriorityQueue to synchronize. + /// + /// + /// A synchronized PriorityQueue. + /// + /// + /// If queue is null. + /// + public static PriorityQueue Synchronized(PriorityQueue queue) + { + #region Require + + if(queue == null) + { + throw new ArgumentNullException("queue"); + } + + #endregion + + return new SynchronizedPriorityQueue(queue); + } + + // Generates a random level for the next node. + private int NextLevel() + { + int nextLevel = 1; + + while(rand.NextDouble() < Probability && + nextLevel < LevelMaxValue && + nextLevel <= currentLevel) + { + nextLevel++; + } + + return nextLevel; + } + + // Makes sure none of the PriorityQueue's invariants have been violated. + [Conditional("DEBUG")] + private void AssertValid() + { + int n = 0; + Node x = header[0]; + + while(x != null) + { + if(x[0] != null) + { + Debug.Assert(comparer.Compare(x.Element, x[0].Element) >= 0, "Order test"); + } + + x = x[0]; + n++; + } + + Debug.Assert(n == Count, "Count test."); + + for(int i = 1; i < currentLevel; i++) + { + Debug.Assert(header[i] != null, "Level non-null test."); + } + + for(int i = currentLevel; i < LevelMaxValue; i++) + { + Debug.Assert(header[i] == null, "Level null test."); + } + } + + [Conditional("DEBUG")] + public static void Test() + { + Random r = new Random(); + PriorityQueue queue = new PriorityQueue(); + int count = 1000; + int element; + + for(int i = 0; i < count; i++) + { + element = r.Next(); + queue.Enqueue(element); + + Debug.Assert(queue.Contains(element), "Contains Test"); + } + + Debug.Assert(queue.Count == count, "Count Test"); + + int previousElement = (int)queue.Peek(); + int peekElement; + + for(int i = 0; i < count; i++) + { + peekElement = (int)queue.Peek(); + element = (int)queue.Dequeue(); + + Debug.Assert(element == peekElement, "Peek Test"); + Debug.Assert(element <= previousElement, "Order Test"); + + previousElement = element; + } + + Debug.Assert(queue.Count == 0); + } + + #endregion + + #region Private Classes + + #region SynchronizedPriorityQueue Class + + // A synchronized wrapper for the PriorityQueue class. + private class SynchronizedPriorityQueue : PriorityQueue + { + private PriorityQueue queue; + + private object root; + + public SynchronizedPriorityQueue(PriorityQueue queue) + { + #region Require + + if(queue == null) + { + throw new ArgumentNullException("queue"); + } + + #endregion + + this.queue = queue; + + root = queue.SyncRoot; + } + + public override void Enqueue(object element) + { + lock(root) + { + queue.Enqueue(element); + } + } + + public override object Dequeue() + { + lock(root) + { + return queue.Dequeue(); + } + } + + public override void Remove(object element) + { + lock(root) + { + queue.Remove(element); + } + } + + public override void Clear() + { + lock(root) + { + queue.Clear(); + } + } + + public override bool Contains(object element) + { + lock(root) + { + return queue.Contains(element); + } + } + + public override object Peek() + { + lock(root) + { + return queue.Peek(); + } + } + + public override void CopyTo(Array array, int index) + { + lock(root) + { + queue.CopyTo(array, index); + } + } + + public override int Count + { + get + { + lock(root) + { + return queue.Count; + } + } + } + + public override bool IsSynchronized + { + get + { + return true; + } + } + + public override object SyncRoot + { + get + { + return root; + } + } + + public override IEnumerator GetEnumerator() + { + lock(root) + { + return queue.GetEnumerator(); + } + } + } + + #endregion + + #region DefaultComparer Class + + // The IComparer to use of no comparer was provided. + private class DefaultComparer : IComparer + { + #region IComparer Members + + public int Compare(object x, object y) + { + #region Require + + if(!(y is IComparable)) + { + throw new ArgumentException( + "Item does not implement IComparable."); + } + + #endregion + + IComparable a = x as IComparable; + + Debug.Assert(a != null); + + return a.CompareTo(y); + } + + #endregion + } + + #endregion + + #region Node Class + + // Represents a node in the list of nodes. + private class Node + { + private Node[] forward; + + private object element; + + public Node(object element, int level) + { + this.forward = new Node[level]; + this.element = element; + } + + public Node this[int index] + { + get + { + return forward[index]; + } + set + { + forward[index] = value; + } + } + + public object Element + { + get + { + return element; + } + } + } + + #endregion + + #region PriorityQueueEnumerator Class + + // Implements the IEnumerator interface for the PriorityQueue class. + private class PriorityQueueEnumerator : IEnumerator + { + private PriorityQueue owner; + + private Node head; + + private Node currentNode; + + private bool moveResult; + + private long version; + + public PriorityQueueEnumerator(PriorityQueue owner) + { + this.owner = owner; + this.version = owner.version; + head = owner.header; + + Reset(); + } + + #region IEnumerator Members + + public void Reset() + { + #region Require + + if(version != owner.version) + { + throw new InvalidOperationException( + "The PriorityQueue was modified after the enumerator was created."); + } + + #endregion + + currentNode = head; + moveResult = true; + } + + public object Current + { + get + { + #region Require + + if(currentNode == head || currentNode == null) + { + throw new InvalidOperationException( + "The enumerator is positioned before the first " + + "element of the collection or after the last element."); + } + + #endregion + + return currentNode.Element; + } + } + + public bool MoveNext() + { + #region Require + + if(version != owner.version) + { + throw new InvalidOperationException( + "The PriorityQueue was modified after the enumerator was created."); + } + + #endregion + + if(moveResult) + { + currentNode = currentNode[0]; + } + + if(currentNode == null) + { + moveResult = false; + } + + return moveResult; + } + + #endregion + } + + #endregion + + #endregion + + #endregion + + #region ICollection Members + + public virtual bool IsSynchronized + { + get + { + return false; + } + } + + public virtual int Count + { + get + { + return count; + } + } + + public virtual void CopyTo(Array array, int index) + { + #region Require + + if(array == null) + { + throw new ArgumentNullException("array"); + } + else if(index < 0) + { + throw new ArgumentOutOfRangeException("index", index, + "Array index out of range."); + } + else if(array.Rank > 1) + { + throw new ArgumentException( + "Array has more than one dimension.", "array"); + } + else if(index >= array.Length) + { + throw new ArgumentException( + "index is equal to or greater than the length of array.", "index"); + } + else if(Count > array.Length - index) + { + throw new ArgumentException( + "The number of elements in the PriorityQueue is greater " + + "than the available space from index to the end of the " + + "destination array.", "index"); + } + + #endregion + + int i = index; + + foreach(object element in this) + { + array.SetValue(element, i); + i++; + } + } + + public virtual object SyncRoot + { + get + { + return this; + } + } + + #endregion + + #region IEnumerable Members + + public virtual IEnumerator GetEnumerator() + { + return new PriorityQueueEnumerator(this); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/SkipList.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/SkipList.cs new file mode 100644 index 00000000..01949434 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/SkipList.cs @@ -0,0 +1,1110 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; + + +namespace Sanford.Collections +{ + /// + /// Represents a collection of key-and-value pairs. + /// + /// + /// The SkipList class is an implementation of the IDictionary interface. It + /// is based on the data structure created by William Pugh. + /// + public class SkipList : IDictionary + { + #region SkipList Members + + #region Constants + + // Maximum level any node in a skip list can have + private const int MaxLevel = 32; + + // Probability factor used to determine the node level + private const double Probability = 0.5; + + #endregion + + #region Fields + + // The skip list header. It also serves as the NIL node. + private Node header = new Node(MaxLevel); + + // Comparer for comparing keys. + private IComparer comparer; + + // Random number generator for generating random node levels. + private Random random = new Random(); + + // Current maximum list level. + private int listLevel; + + // Current number of elements in the skip list. + private int count; + + // Version of the skip list. Used for validation checks with + // enumerators. + private long version = 0; + + #endregion + + /// + /// Initializes a new instance of the SkipList class that is empty and + /// is sorted according to the IComparable interface implemented by + /// each key added to the SkipList. + /// + /// + /// Each key must implement the IComparable interface to be capable of + /// comparisons with every other key in the SortedList. The elements + /// are sorted according to the IComparable implementation of each key + /// added to the SkipList. + /// + public SkipList() + { + // Initialize the skip list. + Initialize(); + } + + /// + /// Initializes a new instance of the SkipList class that is empty and + /// is sorted according to the specified IComparer interface. + /// + /// + /// The IComparer implementation to use when comparing keys. + /// + /// + /// The elements are sorted according to the specified IComparer + /// implementation. If comparer is a null reference, the IComparable + /// implementation of each key is used; therefore, each key must + /// implement the IComparable interface to be capable of comparisons + /// with every other key in the SkipList. + /// + public SkipList(IComparer comparer) + { + // Initialize comparer with the client provided comparer. + this.comparer = comparer; + + // Initialize the skip list. + Initialize(); + } + + /// + /// Destructor. + /// + ~SkipList() + { + Clear(); + } + + #region Private Helper Methods + + /// + /// Initializes the SkipList. + /// + private void Initialize() + { + listLevel = 1; + count = 0; + + // When the list is empty, make sure all forward references in the + // header point back to the header. This is important because the + // header is used as the sentinel to mark the end of the skip list. + for(int i = 0; i < MaxLevel; i++) + { + header.forward[i] = header; + } + } + + /// + /// Returns a level value for a new SkipList node. + /// + /// + /// The level value for a new SkipList node. + /// + private int GetNewLevel() + { + int level = 1; + + // Determines the next node level. + while(random.NextDouble() < Probability && level < MaxLevel && + level <= listLevel) + { + level++; + } + + return level; + } + + /// + /// Searches for the specified key. + /// + /// + /// The key to search for. + /// + /// + /// Returns true if the specified key is in the SkipList. + /// + private bool Search(object key) + { + Node curr; + Node[] dummy = new Node[MaxLevel]; + + return Search(key, out curr, dummy); + } + + /// + /// Searches for the specified key. + /// + /// + /// The key to search for. + /// + /// + /// A SkipList node to hold the results of the search. + /// + /// + /// Returns true if the specified key is in the SkipList. + /// + private bool Search(object key, out Node curr) + { + Node[] dummy = new Node[MaxLevel]; + + return Search(key, out curr, dummy); + } + + /// + /// Searches for the specified key. + /// + /// + /// The key to search for. + /// + /// + /// An array of nodes holding references to the places in the SkipList + /// search in which the search dropped down one level. + /// + /// + /// Returns true if the specified key is in the SkipList. + /// + private bool Search(object key, Node[] update) + { + Node curr; + + return Search(key, out curr, update); + } + + /// + /// Searches for the specified key. + /// + /// + /// The key to search for. + /// + /// + /// A SkipList node to hold the results of the search. + /// + /// + /// An array of nodes holding references to the places in the SkipList + /// search in which the search dropped down one level. + /// + /// + /// Returns true if the specified key is in the SkipList. + /// + private bool Search(object key, out Node curr, Node[] update) + { + // Make sure key isn't null. + if(key == null) + { + throw new ArgumentNullException("An attempt was made to pass a null key to a SkipList."); + } + + bool result; + + // Check to see if we will search with a comparer. + if(comparer != null) + { + result = SearchWithComparer(key, out curr, update); + } + // Else we're using the IComparable interface. + else + { + result = SearchWithComparable(key, out curr, update); + } + + return result; + } + + /// + /// Search for the specified key using a comparer. + /// + /// + /// The key to search for. + /// + /// + /// A SkipList node to hold the results of the search. + /// + /// + /// An array of nodes holding references to the places in the SkipList + /// search in which the search dropped down one level. + /// + /// + /// Returns true if the specified key is in the SkipList. + /// + private bool SearchWithComparer(object key, out Node curr, + Node[] update) + { + bool found = false; + + // Start from the beginning of the skip list. + curr = header; + + // Work our way down from the top of the skip list to the bottom. + for(int i = listLevel - 1; i >= 0; i--) + { + // While we haven't reached the end of the skip list and the + // current key is less than the search key. + while(curr.forward[i] != header && + comparer.Compare(curr.forward[i].Entry.Key, key) < 0) + { + // Move forward in the skip list. + curr = curr.forward[i]; + } + + // Keep track of each node where we move down a level. This + // will be used later to rearrange node references when + // inserting or deleting a new element. + update[i] = curr; + } + + // Move ahead in the skip list. If the new key doesn't already + // exist in the skip list, this should put us at either the end of + // the skip list or at a node with a key greater than the search key. + // If the new key already exists in the skip list, this should put + // us at a node with a key equal to the search key. + curr = curr.forward[0]; + + // If we haven't reached the end of the skip list and the + // current key is equal to the search key. + if(curr != header && comparer.Compare(key, curr.Entry.Key) == 0) + { + // Indicate that we've found the search key. + found = true; + } + + return found; + } + + /// + /// Search for the specified key using the IComparable interface + /// implemented by each key. + /// + /// + /// The key to search for. + /// + /// + /// A SkipList node to hold the results of the search. + /// + /// + /// An array of nodes holding references to the places in the SkipList + /// search in which the search dropped down one level. + /// + /// + /// Returns true if the specified key is in the SkipList. + /// + /// + /// Assumes each key inserted into the SkipList implements the + /// IComparable interface. + /// + /// If the specified key is in the SkipList, the curr parameter will + /// reference the node with the key. If the specified key is not in the + /// SkipList, the curr paramater will either hold the node with the + /// first key value greater than the specified key or it will have the + /// same value as the header indicating that the search reached the end + /// of the SkipList. + /// + private bool SearchWithComparable(object key, out Node curr, + Node[] update) + { + // Make sure key is comparable. + if(!(key is IComparable)) + { + throw new ArgumentException("The SkipList was set to use the IComparable interface and an attempt was made to add a key that does not support this interface."); + } + + bool found = false; + IComparable comp; + + // Begin at the start of the skip list. + curr = header; + + // Work our way down from the top of the skip list to the bottom. + for(int i = listLevel - 1; i >= 0; i--) + { + // Get the comparable interface for the current key. + comp = (IComparable)curr.forward[i].Key; + + // While we haven't reached the end of the skip list and the + // current key is less than the search key. + while(curr.forward[i] != header && comp.CompareTo(key) < 0) + { + // Move forward in the skip list. + curr = curr.forward[i]; + // Get the comparable interface for the current key. + comp = (IComparable)curr.forward[i].Key; + } + + // Keep track of each node where we move down a level. This + // will be used later to rearrange node references when + // inserting a new element. + update[i] = curr; + } + + // Move ahead in the skip list. If the new key doesn't already + // exist in the skip list, this should put us at either the end of + // the skip list or at a node with a key greater than the search key. + // If the new key already exists in the skip list, this should put + // us at a node with a key equal to the search key. + curr = curr.forward[0]; + + // Get the comparable interface for the current key. + comp = (IComparable)curr.Key; + + // If we haven't reached the end of the skip list and the + // current key is equal to the search key. + if(curr != header && comp.CompareTo(key) == 0) + { + // Indicate that we've found the search key. + found = true; + } + + return found; + } + + /// + /// Inserts a key/value pair into the SkipList. + /// + /// + /// The key to insert into the SkipList. + /// + /// + /// The value to insert into the SkipList. + /// + /// + /// An array of nodes holding references to places in the SkipList in + /// which the search for the place to insert the new key/value pair + /// dropped down one level. + /// + private void Insert(object key, object val, Node[] update) + { + // Get the level for the new node. + int newLevel = GetNewLevel(); + + // If the level for the new node is greater than the skip list + // level. + if(newLevel > listLevel) + { + // Make sure our update references above the current skip list + // level point to the header. + for(int i = listLevel; i < newLevel; i++) + { + update[i] = header; + } + + // The current skip list level is now the new node level. + listLevel = newLevel; + } + + // Create the new node. + Node newNode = new Node(newLevel, key, val); + + // Insert the new node into the skip list. + for(int i = 0; i < newLevel; i++) + { + // The new node forward references are initialized to point to + // our update forward references which point to nodes further + // along in the skip list. + newNode.forward[i] = update[i].forward[i]; + + // Take our update forward references and point them towards + // the new node. + update[i].forward[i] = newNode; + } + + // Keep track of the number of nodes in the skip list. + count++; + + // Indicate that the skip list has changed. + version++; + } + + #endregion + + #endregion + + #region Node Class + + /// + /// Represents a node in the SkipList. + /// + private class Node : IDisposable + { + #region Fields + + // References to nodes further along in the skip list. + public Node[] forward; + + // Node key. + private Object key; + + // Node value. + private Object val; + + #endregion + + /// + /// Initializes an instant of a Node with its node level. + /// + /// + /// The node level. + /// + public Node(int level) + { + forward = new Node[level]; + } + + /// + /// Initializes an instant of a Node with its node level and + /// key/value pair. + /// + /// + /// The node level. + /// + /// + /// The key for the node. + /// + /// + /// The value for the node. + /// + public Node(int level, object key, object val) + { + forward = new Node[level]; + + Key = key; + Value = val; + } + + /// + /// Key property. + /// + public Object Key + { + get + { + return key; + } + set + { + key = value; + } + } + + /// + /// Value property. + /// + public Object Value + { + get + { + return val; + } + set + { + val = value; + } + } + + /// + /// Node dictionary Entry property - contains key/value pair. + /// + public DictionaryEntry Entry + { + get + { + return new DictionaryEntry(Key, Value); + } + } + + #region IDisposable Members + + /// + /// Disposes the Node. + /// + public void Dispose() + { + for(int i = 0; i < forward.Length; i++) + { + forward[i] = null; + } + } + + #endregion + } + + #endregion + + #region SkipListEnumerator Class + + /// + /// Enumerates the elements of a skip list. + /// + private class SkipListEnumerator : IDictionaryEnumerator + { + #region SkipListEnumerator Members + + #region Fields + + // The skip list to enumerate. + private SkipList list; + + // The current node. + private Node current; + + // The version of the skip list we are enumerating. + private long version; + + // Keeps track of previous move result so that we can know + // whether or not we are at the end of the skip list. + private bool moveResult = true; + + #endregion + + /// + /// Initializes an instance of a SkipListEnumerator. + /// + /// + public SkipListEnumerator(SkipList list) + { + this.list = list; + version = list.version; + current = list.header; + } + + #endregion + + #region IDictionaryEnumerator Members + + /// + /// Gets both the key and the value of the current dictionary + /// entry. + /// + public DictionaryEntry Entry + { + get + { + DictionaryEntry entry; + + // Make sure the skip list hasn't been modified since the + // enumerator was created. + if(version != list.version) + { + throw new InvalidOperationException("SkipListEnumerator is no longer valid. The SkipList has been modified since the creation of this enumerator."); + } + // Make sure we are not before the beginning or beyond the + // end of the skip list. + else if(current == list.header) + { + throw new InvalidOperationException("SkipListEnumerator is no longer valid. The SkipList has been modified since the creation of this enumerator."); + } + // Finally, all checks have passed. Get the current entry. + else + { + entry = current.Entry; + } + + return entry; + } + } + + /// + /// Gets the key of the current dictionary entry. + /// + public object Key + { + get + { + object key = Entry.Key; + + return key; + } + } + + /// + /// Gets the value of the current dictionary entry. + /// + public object Value + { + get + { + object val = Entry.Value; + + return val; + } + } + + #endregion + + #region IEnumerator Members + + /// + /// Advances the enumerator to the next element of the skip list. + /// + /// + /// true if the enumerator was successfully advanced to the next + /// element; false if the enumerator has passed the end of the + /// skip list. + /// + public bool MoveNext() + { + // Make sure the skip list hasn't been modified since the + // enumerator was created. + if(version == list.version) + { + // If the result of the previous move operation was true + // we can still move forward in the skip list. + if(moveResult) + { + // Move forward in the skip list. + current = current.forward[0]; + + // If we are at the end of the skip list. + if(current == list.header) + { + // Indicate that we've reached the end of the skip + // list. + moveResult = false; + } + } + } + // Else this version of the enumerator doesn't match that of + // the skip list. The skip list has been modified since the + // creation of the enumerator. + else + { + throw new InvalidOperationException("SkipListEnumerator is no longer valid. The SkipList has been modified since the creation of this enumerator."); + } + + return moveResult; + } + + /// + /// Sets the enumerator to its initial position, which is before + /// the first element in the skip list. + /// + public void Reset() + { + // Make sure the skip list hasn't been modified since the + // enumerator was created. + if(version == list.version) + { + current = list.header; + moveResult = true; + } + // Else this version of the enumerator doesn't match that of + // the skip list. The skip list has been modified since the + // creation of the enumerator. + else + { + throw new InvalidOperationException("SkipListEnumerator is no longer valid. The SkipList has been modified since the creation of this enumerator."); + } + } + + /// + /// Gets the current element in the skip list. + /// + public object Current + { + get + { + return Entry; + } + } + + #endregion + } + + #endregion + + #region IDictionary Members + + /// + /// Adds an element with the provided key and value to the SkipList. + /// + /// + /// The Object to use as the key of the element to add. + /// + /// + /// The Object to use as the value of the element to add. + /// + public void Add(object key, object value) + { + Node[] update = new Node[MaxLevel]; + + // If key does not already exist in the skip list. + if(!Search(key, update)) + { + // Inseart key/value pair into the skip list. + Insert(key, value, update); + } + // Else throw an exception. The IDictionary Add method throws an + // exception if an attempt is made to add a key that already + // exists in the skip list. + else + { + throw new ArgumentException("An attempt was made to add an element in which the key of the element already exists in the SkipList."); + } + } + + /// + /// Removes all elements from the SkipList. + /// + public void Clear() + { + // Start at the beginning of the skip list. + Node curr = header.forward[0]; + Node prev; + + // While we haven't reached the end of the skip list. + while(curr != header) + { + // Keep track of the previous node. + prev = curr; + // Move forward in the skip list. + curr = curr.forward[0]; + // Dispose of the previous node. + prev.Dispose(); + } + + // Initialize skip list and indicate that it has been changed. + Initialize(); + version++; + } + + /// + /// Determines whether the SkipList contains an element with the + /// specified key. + /// + /// + /// The key to locate in the SkipList. + /// + /// + /// true if the SkipList contains an element with the key; otherwise, + /// false. + /// + public bool Contains(object key) + { + return Search(key); + } + + /// + /// Returns an IDictionaryEnumerator for the SkipList. + /// + /// + /// An IDictionaryEnumerator for the SkipList. + /// + public IDictionaryEnumerator GetEnumerator() + { + return new SkipListEnumerator(this); + } + + /// + /// Removes the element with the specified key from the SkipList. + /// + /// + /// The key of the element to remove. + /// + public void Remove(object key) + { + Node[] update = new Node[MaxLevel]; + Node curr; + + if(Search(key, out curr, update)) + { + // Take the forward references that point to the node to be + // removed and reassign them to the nodes that come after it. + for(int i = 0; i < listLevel && + update[i].forward[i] == curr; i++) + { + update[i].forward[i] = curr.forward[i]; + } + + curr.Dispose(); + + // After removing the node, we may need to lower the current + // skip list level if the node had the highest level of all of + // the nodes. + while(listLevel > 1 && header.forward[listLevel - 1] == header) + { + listLevel--; + } + + // Keep track of the number of nodes. + count--; + // Indicate that the skip list has changed. + version++; + } + } + + /// + /// Gets a value indicating whether the SkipList has a fixed size. + /// + public bool IsFixedSize + { + get + { + return false; + } + } + + /// + /// Gets a value indicating whether the IDictionary is read-only. + /// + public bool IsReadOnly + { + get + { + return false; + } + } + + /// + /// Gets or sets the element with the specified key. This is the + /// indexer for the SkipList. + /// + public object this[object key] + { + get + { + object val = null; + Node curr; + + if(Search(key, out curr)) + { + val = curr.Entry.Value; + } + + return val; + } + set + { + Node[] update = new Node[MaxLevel]; + Node curr; + + // If the search key already exists in the skip list. + if(Search(key, out curr, update)) + { + // Replace the current value with the new value. + curr.Value = value; + // Indicate that the skip list has changed. + version++; + } + // Else the key doesn't exist in the skip list. + else + { + // Insert the key and value into the skip list. + Insert(key, value, update); + } + } + } + + /// + /// Gets an ICollection containing the keys of the SkipList. + /// + public ICollection Keys + { + get + { + // Start at the beginning of the skip list. + Node curr = header.forward[0]; + // Create a collection to hold the keys. + ArrayList collection = new ArrayList(); + + // While we haven't reached the end of the skip list. + while(curr != header) + { + // Add the key to the collection. + collection.Add(curr.Entry.Key); + // Move forward in the skip list. + curr = curr.forward[0]; + } + + return collection; + } + } + + /// + /// Gets an ICollection containing the values of the SkipList. + /// + public ICollection Values + { + get + { + // Start at the beginning of the skip list. + Node curr = header.forward[0]; + // Create a collection to hold the values. + ArrayList collection = new ArrayList(); + + // While we haven't reached the end of the skip list. + while(curr != header) + { + // Add the value to the collection. + collection.Add(curr.Entry.Value); + // Move forward in the skip list. + curr = curr.forward[0]; + } + + return collection; + } + } + + #endregion + + #region ICollection Members + + /// + /// Copies the elements of the SkipList to an Array, starting at a + /// particular Array index. + /// + /// + /// The one-dimensional Array that is the destination of the elements + /// copied from SkipList. + /// + /// + /// The zero-based index in array at which copying begins. + /// + public void CopyTo(Array array, int index) + { + // Make sure array isn't null. + if(array == null) + { + throw new ArgumentNullException("An attempt was made to pass a null array to the CopyTo method of a SkipList."); + } + // Make sure index is not negative. + else if(index < 0) + { + throw new ArgumentOutOfRangeException("An attempt was made to pass an out of range index to the CopyTo method of a SkipList."); + } + // Array bounds checking. + else if(index >= array.Length) + { + throw new ArgumentException("An attempt was made to pass an out of range index to the CopyTo method of a SkipList."); + } + // Make sure that the number of elements in the skip list is not + // greater than the available space from index to the end of the + // array. + else if((array.Length - index) < Count) + { + throw new ArgumentException("An attempt was made to pass an out of range index to the CopyTo method of a SkipList."); + } + // Else copy elements from skip list into array. + else + { + // Start at the beginning of the skip list. + Node curr = header.forward[0]; + + // While we haven't reached the end of the skip list. + while(curr != header) + { + // Copy current value into array. + array.SetValue(curr.Entry.Value, index); + + // Move forward in the skip list and array. + curr = curr.forward[0]; + index++; + } + } + } + + /// + /// Gets the number of elements contained in the SkipList. + /// + public int Count + { + get + { + return count; + } + } + + /// + /// Gets a value indicating whether access to the SkipList is + /// synchronized (thread-safe). + /// + public bool IsSynchronized + { + get + { + return false; + } + } + + /// + /// Gets an object that can be used to synchronize access to the + /// SkipList. + /// + public object SyncRoot + { + get + { + return this; + } + } + + #endregion + + #region IEnumerable Members + + /// + /// Returns an enumerator that can iterate through the SkipList. + /// + /// + /// An IEnumerator that can be used to iterate through the collection. + /// + IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return new SkipListEnumerator(this); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi.Core.csproj b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi.Core.csproj new file mode 100644 index 00000000..e5fdebe9 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi.Core.csproj @@ -0,0 +1,12 @@ + + + net5.0 + Local + + + 7.0.0 + + + docs\Sanford.Multimedia.Midi.XML + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/IClock.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/IClock.cs new file mode 100644 index 00000000..049fc53d --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/IClock.cs @@ -0,0 +1,89 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Represents functionality for generating events for driving Sequence playback. + /// + public interface IClock + { + #region IClock Members + + /// + /// Occurs when an IClock generates a tick. + /// + event EventHandler Tick; + + /// + /// Occurs when an IClock starts generating Ticks. + /// + /// + /// When an IClock is started, it resets itself and generates ticks to + /// drive playback from the beginning of the Sequence. + /// + event EventHandler Started; + + /// + /// Occurs when an IClock continues generating Ticks. + /// + /// + /// When an IClock is continued, it generates ticks to drive playback + /// from the current position within the Sequence. + /// + event EventHandler Continued; + + /// + /// Occurs when an IClock is stopped. + /// + event EventHandler Stopped; + + /// + /// Gets a value indicating whether the IClock is running. + /// + bool IsRunning + { + get; + } + + int Ticks + { + get; + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/MidiInternalClock.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/MidiInternalClock.cs new file mode 100644 index 00000000..a39c6373 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/MidiInternalClock.cs @@ -0,0 +1,381 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Sanford.Multimedia.Timers; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Generates clock events internally. + /// + public class MidiInternalClock : PpqnClock, IComponent + { + #region MidiInternalClock Members + + #region Fields + + // Used for generating tick events. + private ITimer timer; + + // Parses meta message tempo change messages. + private TempoChangeBuilder builder = new TempoChangeBuilder(); + + // Tick accumulator. + private int ticks = 0; + + // Indicates whether the clock has been disposed. + private bool disposed = false; + + private ISite site = null; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the MidiInternalClock class. + /// + public MidiInternalClock() + : this(TimerCaps.Default.periodMin) + { + } + + public MidiInternalClock(int timerPeriod) : base(timerPeriod) + { + timer = TimerFactory.Create(); + timer.Period = timerPeriod; + timer.Tick += new EventHandler(HandleTick); + } + + /// + /// Initializes a new instance of the MidiInternalClock class with the + /// specified IContainer. + /// + /// + /// The IContainer to which the MidiInternalClock will add itself. + /// + public MidiInternalClock(IContainer container) : + this() + { + /// + /// Required for Windows.Forms Class Composition Designer support + /// + container.Add(this); + } + + #endregion + + #region Methods + + /// + /// Starts the MidiInternalClock. + /// + public void Start() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("MidiInternalClock"); + } + + #endregion + + #region Guard + + if(running) + { + return; + } + + #endregion + + ticks = 0; + + Reset(); + + OnStarted(EventArgs.Empty); + + // Start the multimedia timer in order to start generating ticks. + timer.Start(); + + // Indicate that the clock is now running. + running = true; + + } + + /// + /// Resumes tick generation from the current position. + /// + public void Continue() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("MidiInternalClock"); + } + + #endregion + + #region Guard + + if(running) + { + return; + } + + #endregion + + // Raise Continued event. + OnContinued(EventArgs.Empty); + + // Start multimedia timer in order to start generating ticks. + timer.Start(); + + // Indicate that the clock is now running. + running = true; + } + + /// + /// Stops the MidiInternalClock. + /// + public void Stop() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("MidiInternalClock"); + } + + #endregion + + #region Guard + + if(!running) + { + return; + } + + #endregion + + // Stop the multimedia timer. + timer.Stop(); + + // Indicate that the clock is not running. + running = false; + + OnStopped(EventArgs.Empty); + } + + public void SetTicks(int ticks) + { + #region Require + + if(ticks < 0) + { + throw new ArgumentOutOfRangeException(); + } + + #endregion + + if(IsRunning) + { + Stop(); + } + + this.ticks = ticks; + + Reset(); + } + + public void Process(MetaMessage message) + { + #region Require + + if(message == null) + { + throw new ArgumentNullException("message"); + } + + #endregion + + #region Guard + + if(message.MetaType != MetaType.Tempo) + { + return; + } + + #endregion + + TempoChangeBuilder builder = new TempoChangeBuilder(message); + + // Set the new tempo. + Tempo = builder.Tempo; + } + + #region Event Raiser Methods + + protected virtual void OnDisposed(EventArgs e) + { + EventHandler handler = Disposed; + + if(handler != null) + { + handler(this, e); + } + } + + #endregion + + #region Event Handler Methods + + // Handles Tick events generated by the multimedia timer. + private void HandleTick(object sender, EventArgs e) + { + int t = GenerateTicks(); + + for(int i = 0; i < t; i++) + { + OnTick(EventArgs.Empty); + + ticks++; + } + } + + #endregion + + #endregion + + #region Properties + + /// + /// Gets or sets the tempo in microseconds per beat. + /// + public int Tempo + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("MidiInternalClock"); + } + + #endregion + + return GetTempo(); + } + set + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("MidiInternalClock"); + } + + #endregion + + SetTempo(value); + } + } + + public override int Ticks + { + get + { + return ticks; + } + } + + #endregion + + #endregion + + #region IComponent Members + + public event EventHandler Disposed; + + public ISite Site + { + get + { + return site; + } + set + { + site = value; + } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + #region Guard + + if(disposed) + { + return; + } + + #endregion + + if(running) + { + // Stop the multimedia timer. + timer.Stop(); + } + + disposed = true; + + timer.Dispose(); + + GC.SuppressFinalize(this); + + OnDisposed(EventArgs.Empty); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/PpqnClock.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/PpqnClock.cs new file mode 100644 index 00000000..5a809cea --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/PpqnClock.cs @@ -0,0 +1,259 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Provides basic functionality for generating tick events with pulses per + /// quarter note resolution. + /// + public abstract class PpqnClock : IClock + { + #region PpqnClock Members + + #region Fields + + /// + /// The default tempo in microseconds: 120bpm. + /// + public const int DefaultTempo = 500000; + + /// + /// The minimum pulses per quarter note value. + /// + public const int PpqnMinValue = 24; + + // The number of microseconds per millisecond. + private const int MicrosecondsPerMillisecond = 1000; + + // The pulses per quarter note value. + private int ppqn = PpqnMinValue; + + // The tempo in microseconds. + private int tempo = DefaultTempo; + + // The product of the timer period, the pulses per quarter note, and + // the number of microseconds per millisecond. + private int periodResolution; + + // The number of ticks per MIDI clock. + private int ticksPerClock; + + // The running fractional tick count. + private int fractionalTicks = 0; + + // The timer period. + private readonly int timerPeriod; + + // Indicates whether the clock is running. + protected bool running = false; + + #endregion + + #region Construction + + protected PpqnClock(int timerPeriod) + { + #region Require + + if(timerPeriod < 1) + { + throw new ArgumentOutOfRangeException("timerPeriod", timerPeriod, + "Timer period cannot be less than one."); + } + + #endregion + + this.timerPeriod = timerPeriod; + + CalculatePeriodResolution(); + CalculateTicksPerClock(); + } + + #endregion + + #region Methods + + protected int GetTempo() + { + return tempo; + } + + protected void SetTempo(int tempo) + { + #region Require + + if(tempo < 1) + { + throw new ArgumentOutOfRangeException( + "Tempo out of range."); + } + + #endregion + + this.tempo = tempo; + } + + protected void Reset() + { + fractionalTicks = 0; + } + + protected int GenerateTicks() + { + int ticks = (fractionalTicks + periodResolution) / tempo; + fractionalTicks += periodResolution - ticks * tempo; + + return ticks; + } + + private void CalculatePeriodResolution() + { + periodResolution = ppqn * timerPeriod * MicrosecondsPerMillisecond; + } + + private void CalculateTicksPerClock() + { + ticksPerClock = ppqn / PpqnMinValue; + } + + protected virtual void OnTick(EventArgs e) + { + EventHandler handler = Tick; + + if(handler != null) + { + handler(this, EventArgs.Empty); + } + } + + protected virtual void OnStarted(EventArgs e) + { + EventHandler handler = Started; + + if(handler != null) + { + handler(this, e); + } + } + + protected virtual void OnStopped(EventArgs e) + { + EventHandler handler = Stopped; + + if(handler != null) + { + handler(this, e); + } + } + + protected virtual void OnContinued(EventArgs e) + { + EventHandler handler = Continued; + + if(handler != null) + { + handler(this, e); + } + } + + #endregion + + #region Properties + + public int Ppqn + { + get + { + return ppqn; + } + set + { + #region Require + + if(value < PpqnMinValue) + { + throw new ArgumentOutOfRangeException("Ppqn", value, + "Pulses per quarter note is smaller than 24."); + } + + #endregion + + ppqn = value; + + CalculatePeriodResolution(); + CalculateTicksPerClock(); + } + } + + public abstract int Ticks + { + get; + } + + public int TicksPerClock + { + get + { + return ticksPerClock; + } + } + + #endregion + + #endregion + + #region IClock Members + + public event System.EventHandler Tick; + + public event System.EventHandler Started; + + public event System.EventHandler Continued; + + public event System.EventHandler Stopped; + + public bool IsRunning + { + get + { + return running; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Construction.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Construction.cs new file mode 100644 index 00000000..7f1daa38 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Construction.cs @@ -0,0 +1,79 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Threading; +using Sanford.Threading; + +namespace Sanford.Multimedia.Midi +{ + public partial class InputDevice : MidiDevice + { + #region Construction + + /// + /// Initializes a new instance of the InputDevice class with the + /// specified device ID. + /// + public InputDevice(int deviceID, bool postEventsOnCreationContext = true, bool postDriverCallbackToDelegateQueue = true) + : base(deviceID) + { + midiInProc = HandleMessage; + + delegateQueue = new DelegateQueue(); + int result = midiInOpen(out handle, deviceID, midiInProc, IntPtr.Zero, CALLBACK_FUNCTION); + + System.Diagnostics.Debug.WriteLine("MidiIn handle:" + handle.ToInt64()); + + if (result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new InputDeviceException(result); + } + + PostEventsOnCreationContext = postEventsOnCreationContext; + PostDriverCallbackToDelegateQueue = postDriverCallbackToDelegateQueue; + } + + ~InputDevice() + { + if (!IsDisposed) + { + midiInReset(handle); + midiInClose(handle); + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Events.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Events.cs new file mode 100644 index 00000000..8fb1294c --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Events.cs @@ -0,0 +1,202 @@ +using System; + +namespace Sanford.Multimedia.Midi +{ + public delegate void MidiMessageEventHandler(IMidiMessage message); + + public partial class InputDevice + { + /// + /// Gets or sets a value indicating whether the midi events should be posted on the same synchronization context as the device constructor was called. + /// Default is true. If set to false the events are fired on the driver callback or the thread of the driver callback delegate queue, depending on the PostDriverCallbackToDelegateQueue property. + /// + /// + /// true if midi events should be posted on the same synchronization context as the device constructor was called; otherwise, false. + /// + public bool PostEventsOnCreationContext + { + get; + set; + } + + /// + /// Occurs when any message was received. The underlying type of the message is as specific as possible. + /// Channel, Common, Realtime or SysEx. + /// + public event MidiMessageEventHandler MessageReceived; + + public event EventHandler ShortMessageReceived; + + public event EventHandler ChannelMessageReceived; + + public event EventHandler SysExMessageReceived; + + public event EventHandler SysCommonMessageReceived; + + public event EventHandler SysRealtimeMessageReceived; + + public event EventHandler InvalidShortMessageReceived; + + public event EventHandler InvalidSysExMessageReceived; + + protected virtual void OnShortMessage(ShortMessageEventArgs e) + { + EventHandler handler = ShortMessageReceived; + + if (handler != null) + { + if (PostEventsOnCreationContext) + { + context.Post(delegate (object dummy) + { + handler(this, e); + }, null); + } + else + { + handler(this, e); + } + } + } + + protected void OnMessageReceived(IMidiMessage message) + { + MidiMessageEventHandler handler = MessageReceived; + + if (handler != null) + { + if (PostEventsOnCreationContext) + { + context.Post(delegate (object dummy) + { + handler(message); + }, null); + } + else + { + handler(message); + } + } + } + + protected virtual void OnChannelMessageReceived(ChannelMessageEventArgs e) + { + EventHandler handler = ChannelMessageReceived; + + if(handler != null) + { + if (PostEventsOnCreationContext) + { + context.Post(delegate (object dummy) + { + handler(this, e); + }, null); + } + else + { + handler(this, e); + } + } + } + + protected virtual void OnSysExMessageReceived(SysExMessageEventArgs e) + { + EventHandler handler = SysExMessageReceived; + + if(handler != null) + { + if (PostEventsOnCreationContext) + { + context.Post(delegate (object dummy) + { + handler(this, e); + }, null); + } + else + { + handler(this, e); + } + } + } + + protected virtual void OnSysCommonMessageReceived(SysCommonMessageEventArgs e) + { + EventHandler handler = SysCommonMessageReceived; + + if(handler != null) + { + if (PostEventsOnCreationContext) + { + context.Post(delegate (object dummy) + { + handler(this, e); + }, null); + } + else + { + handler(this, e); + } + } + } + + protected virtual void OnSysRealtimeMessageReceived(SysRealtimeMessageEventArgs e) + { + EventHandler handler = SysRealtimeMessageReceived; + + if(handler != null) + { + if (PostEventsOnCreationContext) + { + context.Post(delegate (object dummy) + { + handler(this, e); + }, null); + } + else + { + handler(this, e); + } + } + } + + protected virtual void OnInvalidShortMessageReceived(InvalidShortMessageEventArgs e) + { + EventHandler handler = InvalidShortMessageReceived; + + if(handler != null) + { + if (PostEventsOnCreationContext) + { + context.Post(delegate (object dummy) + { + handler(this, e); + }, null); + } + else + { + handler(this, e); + } + } + } + + protected virtual void OnInvalidSysExMessageReceived(InvalidSysExMessageEventArgs e) + { + EventHandler handler = InvalidSysExMessageReceived; + + if(handler != null) + { + if (PostEventsOnCreationContext) + { + context.Post(delegate (object dummy) + { + handler(this, e); + }, null); + } + else + { + handler(this, e); + } + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Fields.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Fields.cs new file mode 100644 index 00000000..ff11255c --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Fields.cs @@ -0,0 +1,71 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using Sanford.Threading; + +namespace Sanford.Multimedia.Midi +{ + public partial class InputDevice + { + private delegate void GenericDelegate(T args); + + private DelegateQueue delegateQueue = null; + + private volatile int bufferCount = 0; + + private readonly object lockObject = new object(); + + private MidiInProc midiInProc; + + private bool recording = false; + + private MidiHeaderBuilder headerBuilder = new MidiHeaderBuilder(); + + private ChannelMessageBuilder cmBuilder = new ChannelMessageBuilder(); + + private SysCommonMessageBuilder scBuilder = new SysCommonMessageBuilder(); + + private IntPtr handle; + + private volatile bool resetting = false; + + private int sysExBufferSize = 4096; + + private List sysExData = new List(); + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Messaging.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Messaging.cs new file mode 100644 index 00000000..f4a48dfb --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Messaging.cs @@ -0,0 +1,338 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using Sanford.Multimedia; + +namespace Sanford.Multimedia.Midi +{ + internal struct MidiInParams + { + public readonly IntPtr Param1; + public readonly IntPtr Param2; + + public MidiInParams(IntPtr param1, IntPtr param2) + { + Param1 = param1; + Param2 = param2; + } + } + + public partial class InputDevice : MidiDevice + { + /// + /// Gets or sets a value indicating whether the midi input driver callback should be posted on a delegate queue with its own thread. + /// Default is true. If set to false the driver callback directly calls the events for lowest possible latency. + /// + /// + /// true if the midi input driver callback should be posted on a delegate queue with its own thread; otherwise, false. + /// + public bool PostDriverCallbackToDelegateQueue + { + get; + set; + } + + int FLastParam2; + + private void HandleMessage(IntPtr hnd, int msg, IntPtr instance, IntPtr param1, IntPtr param2) + { + var param = new MidiInParams(param1, param2); + + if (msg == MIM_OPEN) + { + } + else if (msg == MIM_CLOSE) + { + } + else if (msg == MIM_DATA) + { + if (PostDriverCallbackToDelegateQueue) + delegateQueue.Post(HandleShortMessage, param); + else + HandleShortMessage(param); + } + else if (msg == MIM_MOREDATA) + { + if (PostDriverCallbackToDelegateQueue) + delegateQueue.Post(HandleShortMessage, param); + else + HandleShortMessage(param); + } + else if (msg == MIM_LONGDATA) + { + if (PostDriverCallbackToDelegateQueue) + delegateQueue.Post(HandleSysExMessage, param); + else + HandleSysExMessage(param); + } + else if (msg == MIM_ERROR) + { + if (PostDriverCallbackToDelegateQueue) + delegateQueue.Post(HandleInvalidShortMessage, param); + else + HandleInvalidShortMessage(param); + } + else if (msg == MIM_LONGERROR) + { + if (PostDriverCallbackToDelegateQueue) + delegateQueue.Post(HandleInvalidSysExMessage, param); + else + HandleInvalidSysExMessage(param); + } + } + + private void HandleShortMessage(object state) + { + + var param = (MidiInParams)state; + int message = param.Param1.ToInt32(); + int timestamp = param.Param2.ToInt32(); + + //first send RawMessage + OnShortMessage(new ShortMessageEventArgs(message, timestamp)); + + int status = ShortMessage.UnpackStatus(message); + + if (status >= (int)ChannelCommand.NoteOff && + status <= (int)ChannelCommand.PitchWheel + + ChannelMessage.MidiChannelMaxValue) + { + cmBuilder.Message = message; + cmBuilder.Build(); + + cmBuilder.Result.Timestamp = timestamp; + OnMessageReceived(cmBuilder.Result); + OnChannelMessageReceived(new ChannelMessageEventArgs(cmBuilder.Result)); + } + else if (status == (int)SysCommonType.MidiTimeCode || + status == (int)SysCommonType.SongPositionPointer || + status == (int)SysCommonType.SongSelect || + status == (int)SysCommonType.TuneRequest) + { + scBuilder.Message = message; + scBuilder.Build(); + + scBuilder.Result.Timestamp = timestamp; + OnMessageReceived(scBuilder.Result); + OnSysCommonMessageReceived(new SysCommonMessageEventArgs(scBuilder.Result)); + } + else + { + SysRealtimeMessageEventArgs e = null; + + switch ((SysRealtimeType)status) + { + case SysRealtimeType.ActiveSense: + e = SysRealtimeMessageEventArgs.ActiveSense; + break; + + case SysRealtimeType.Clock: + e = SysRealtimeMessageEventArgs.Clock; + break; + + case SysRealtimeType.Continue: + e = SysRealtimeMessageEventArgs.Continue; + break; + + case SysRealtimeType.Reset: + e = SysRealtimeMessageEventArgs.Reset; + break; + + case SysRealtimeType.Start: + e = SysRealtimeMessageEventArgs.Start; + break; + + case SysRealtimeType.Stop: + e = SysRealtimeMessageEventArgs.Stop; + break; + + case SysRealtimeType.Tick: + e = SysRealtimeMessageEventArgs.Tick; + break; + } + + e.Message.Timestamp = timestamp; + OnMessageReceived(e.Message); + OnSysRealtimeMessageReceived(e); + } + } + + private void HandleSysExMessage(object state) + { + lock (lockObject) + { + var param = (MidiInParams)state; + IntPtr headerPtr = param.Param1; + + MidiHeader header = (MidiHeader)Marshal.PtrToStructure(headerPtr, typeof(MidiHeader)); + + if (!resetting) + { + for (int i = 0; i < header.bytesRecorded; i++) + { + sysExData.Add(Marshal.ReadByte(header.data, i)); + } + + if (sysExData.Count > 1 && sysExData[0] == 0xF0 && sysExData[sysExData.Count - 1] == 0xF7) + { + SysExMessage message = new SysExMessage(sysExData.ToArray()); + message.Timestamp = param.Param2.ToInt32(); + + sysExData.Clear(); + + OnMessageReceived(message); + OnSysExMessageReceived(new SysExMessageEventArgs(message)); + } + + int result = AddSysExBuffer(); + + if (result != DeviceException.MMSYSERR_NOERROR) + { + Exception ex = new InputDeviceException(result); + + OnError(new ErrorEventArgs(ex)); + } + } + + ReleaseBuffer(headerPtr); + } + } + + private void HandleInvalidShortMessage(object state) + { + var param = (MidiInParams)state; + OnInvalidShortMessageReceived(new InvalidShortMessageEventArgs(param.Param1.ToInt32())); + } + + private void HandleInvalidSysExMessage(object state) + { + lock (lockObject) + { + var param = (MidiInParams)state; + IntPtr headerPtr = param.Param1; + + MidiHeader header = (MidiHeader)Marshal.PtrToStructure(headerPtr, typeof(MidiHeader)); + + if (!resetting) + { + byte[] data = new byte[header.bytesRecorded]; + + Marshal.Copy(header.data, data, 0, data.Length); + + OnInvalidSysExMessageReceived(new InvalidSysExMessageEventArgs(data)); + + int result = AddSysExBuffer(); + + if (result != DeviceException.MMSYSERR_NOERROR) + { + Exception ex = new InputDeviceException(result); + + OnError(new ErrorEventArgs(ex)); + } + } + + ReleaseBuffer(headerPtr); + } + } + + private void ReleaseBuffer(IntPtr headerPtr) + { + int result = midiInUnprepareHeader(Handle, headerPtr, SizeOfMidiHeader); + + if (result != DeviceException.MMSYSERR_NOERROR) + { + Exception ex = new InputDeviceException(result); + + OnError(new ErrorEventArgs(ex)); + } + + headerBuilder.Destroy(headerPtr); + + bufferCount--; + + Debug.Assert(bufferCount >= 0); + + Monitor.Pulse(lockObject); + } + + public int AddSysExBuffer() + { + int result; + + // Initialize the MidiHeader builder. + headerBuilder.BufferLength = sysExBufferSize; + headerBuilder.Build(); + + // Get the pointer to the built MidiHeader. + IntPtr headerPtr = headerBuilder.Result; + + // Prepare the header to be used. + result = midiInPrepareHeader(Handle, headerPtr, SizeOfMidiHeader); + + // If the header was perpared successfully. + if (result == DeviceException.MMSYSERR_NOERROR) + { + bufferCount++; + + // Add the buffer to the InputDevice. + result = midiInAddBuffer(Handle, headerPtr, SizeOfMidiHeader); + + // If the buffer could not be added. + if (result != MidiDeviceException.MMSYSERR_NOERROR) + { + // Unprepare header - there's a chance that this will fail + // for whatever reason, but there's not a lot that can be + // done at this point. + midiInUnprepareHeader(Handle, headerPtr, SizeOfMidiHeader); + + bufferCount--; + + // Destroy header. + headerBuilder.Destroy(); + } + } + // Else the header could not be prepared. + else + { + // Destroy header. + headerBuilder.Destroy(); + } + + return result; + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Properties.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Properties.cs new file mode 100644 index 00000000..e1b38e7e --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Properties.cs @@ -0,0 +1,79 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; + +namespace Sanford.Multimedia.Midi +{ + public partial class InputDevice + { + public override IntPtr Handle + { + get + { + return handle; + } + } + + public int SysExBufferSize + { + get + { + return sysExBufferSize; + } + set + { + #region Require + + if(value < 1) + { + throw new ArgumentOutOfRangeException(); + } + + #endregion + + sysExBufferSize = value; + } + } + + public static int DeviceCount + { + get + { + return midiInGetNumDevs(); + } + } + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.PublicMethods.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.PublicMethods.cs new file mode 100644 index 00000000..f89683ef --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.PublicMethods.cs @@ -0,0 +1,214 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Diagnostics; +using System.Threading; + +namespace Sanford.Multimedia.Midi +{ + public partial class InputDevice + { + public override void Close() + { + #region Guard + + if(IsDisposed) + { + return; + } + + #endregion + + Dispose(true); + } + + public void StartRecording() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("InputDevice"); + } + + #endregion + + #region Guard + + if(recording) + { + return; + } + + #endregion + + lock(lockObject) + { + int result = AddSysExBuffer(); + + if(result == DeviceException.MMSYSERR_NOERROR) + { + result = AddSysExBuffer(); + } + + if(result == DeviceException.MMSYSERR_NOERROR) + { + result = AddSysExBuffer(); + } + + if(result == DeviceException.MMSYSERR_NOERROR) + { + result = AddSysExBuffer(); + } + + if(result == DeviceException.MMSYSERR_NOERROR) + { + result = midiInStart(Handle); + } + + if(result == MidiDeviceException.MMSYSERR_NOERROR) + { + recording = true; + } + else + { + throw new InputDeviceException(result); + } + } + } + + public void StopRecording() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("InputDevice"); + } + + #endregion + + #region Guard + + if(!recording) + { + return; + } + + #endregion + + lock(lockObject) + { + int result = midiInStop(Handle); + + if(result == MidiDeviceException.MMSYSERR_NOERROR) + { + recording = false; + } + else + { + throw new InputDeviceException(result); + } + } + } + + public override void Reset() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("InputDevice"); + } + + #endregion + + lock(lockObject) + { + resetting = true; + + int result = midiInReset(Handle); + + if(result == MidiDeviceException.MMSYSERR_NOERROR) + { + recording = false; + + while(bufferCount > 0) + { + Monitor.Wait(lockObject); + } + + resetting = false; + } + else + { + resetting = false; + + throw new InputDeviceException(result); + } + } + } + + public static MidiInCaps GetDeviceCapabilities(int deviceID) + { + int result; + MidiInCaps caps = new MidiInCaps(); + + IntPtr devID = (IntPtr)deviceID; + result = midiInGetDevCaps(devID, ref caps, SizeOfMidiHeader); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new InputDeviceException(result); + } + + return caps; + } + + public override void Dispose() + { + #region Guard + + if(IsDisposed) + { + return; + } + + #endregion + + Dispose(true); + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Win32.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Win32.cs new file mode 100644 index 00000000..988e9b30 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Win32.cs @@ -0,0 +1,95 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Runtime.InteropServices; + +namespace Sanford.Multimedia.Midi + { + public partial class InputDevice + { + // Represents the method that handles messages from Windows. + private delegate void MidiInProc(IntPtr handle, int msg, IntPtr instance, IntPtr param1, IntPtr param2); + + #region Win32 MIDI Input Functions and Constants + + [DllImport("winmm.dll")] + private static extern int midiInOpen(out IntPtr handle, int deviceID, + MidiInProc proc, IntPtr instance, int flags); + + [DllImport("winmm.dll")] + private static extern int midiInClose(IntPtr handle); + + [DllImport("winmm.dll")] + private static extern int midiInStart(IntPtr handle); + + [DllImport("winmm.dll")] + private static extern int midiInStop(IntPtr handle); + + [DllImport("winmm.dll")] + private static extern int midiInReset(IntPtr handle); + + [DllImport("winmm.dll")] + private static extern int midiInPrepareHeader(IntPtr handle, + IntPtr headerPtr, int sizeOfMidiHeader); + + [DllImport("winmm.dll")] + private static extern int midiInUnprepareHeader(IntPtr handle, + IntPtr headerPtr, int sizeOfMidiHeader); + + [DllImport("winmm.dll")] + private static extern int midiInAddBuffer(IntPtr handle, + IntPtr headerPtr, int sizeOfMidiHeader); + + [DllImport("winmm.dll")] + private static extern int midiInGetDevCaps(IntPtr deviceID, + ref MidiInCaps caps, int sizeOfMidiInCaps); + + [DllImport("winmm.dll")] + private static extern int midiInGetNumDevs(); + + private const int MIDI_IO_STATUS = 0x00000020; + + private const int MIM_OPEN = 0x3C1; + private const int MIM_CLOSE = 0x3C2; + private const int MIM_DATA = 0x3C3; + private const int MIM_LONGDATA = 0x3C4; + private const int MIM_ERROR = 0x3C5; + private const int MIM_LONGERROR = 0x3C6; + private const int MIM_MOREDATA = 0x3CC; + private const int MHDR_DONE = 0x00000001; + + #endregion + } + } diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.cs new file mode 100644 index 00000000..1170395b --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.cs @@ -0,0 +1,134 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Represents a MIDI device capable of receiving MIDI events. + /// + public partial class InputDevice : MidiDevice + { + protected override void Dispose(bool disposing) + { + if(disposing) + { + lock(lockObject) + { + Reset(); + + int result = midiInClose(handle); + + if(result == MidiDeviceException.MMSYSERR_NOERROR) + { + delegateQueue.Dispose(); + } + else + { + throw new InputDeviceException(result); + } + } + } + else + { + midiInReset(handle); + midiInClose(handle); + } + + base.Dispose(disposing); + } + } + + /// + /// The exception that is thrown when a error occurs with the InputDevice + /// class. + /// + public class InputDeviceException : MidiDeviceException + { + #region InputDeviceException Members + + #region Win32 Midi Input Error Function + + [DllImport("winmm.dll", CharSet = CharSet.Unicode)] + private static extern int midiInGetErrorText(int errCode, + StringBuilder errMsg, int sizeOfErrMsg); + + #endregion + + #region Fields + + // Error message. + private StringBuilder errMsg = new StringBuilder(128); + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the InputDeviceException class with + /// the specified error code. + /// + /// + /// The error code. + /// + public InputDeviceException(int errCode) : base(errCode) + { + // Get error message. + midiInGetErrorText(errCode, errMsg, errMsg.Capacity); + } + + #endregion + + #region Properties + + /// + /// Gets a message that describes the current exception. + /// + public override string Message + { + get + { + return errMsg.ToString(); + } + } + + #endregion + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/MidiInCaps.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/MidiInCaps.cs new file mode 100644 index 00000000..d36da936 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/MidiInCaps.cs @@ -0,0 +1,79 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Runtime.InteropServices; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Represents MIDI input device capabilities. + /// + [StructLayout(LayoutKind.Sequential)] + public struct MidiInCaps + { + #region MidiInCaps Members + + /// + /// Manufacturer identifier of the device driver for the Midi output + /// device. + /// + public short mid; + + /// + /// Product identifier of the Midi output device. + /// + public short pid; + + /// + /// Version number of the device driver for the Midi output device. The + /// high-order byte is the major version number, and the low-order byte + /// is the minor version number. + /// + public int driverVersion; + + /// + /// Product name. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string name; + + /// + /// Optional functionality supported by the device. + /// + public int support; + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDevice.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDevice.cs new file mode 100644 index 00000000..03d27445 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDevice.cs @@ -0,0 +1,120 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Sanford.Multimedia; + +namespace Sanford.Multimedia.Midi +{ + /// + /// The base class for all MIDI devices. + /// + public abstract class MidiDevice : Device + { + #region MidiDevice Members + + #region Win32 Midi Device Functions + + [DllImport("winmm.dll")] + private static extern int midiConnect(IntPtr handleA, IntPtr handleB, IntPtr reserved); + + [DllImport("winmm.dll")] + private static extern int midiDisconnect(IntPtr handleA, IntPtr handleB, IntPtr reserved); + + #endregion + + // Size of the MidiHeader structure. + protected static readonly int SizeOfMidiHeader; + + static MidiDevice() + { + SizeOfMidiHeader = Marshal.SizeOf(typeof(MidiHeader)); + } + + public MidiDevice(int deviceID) : base(deviceID) + { + } + + /// + /// Connects a MIDI InputDevice to a MIDI thru or OutputDevice, or + /// connects a MIDI thru device to a MIDI OutputDevice. + /// + /// + /// Handle to a MIDI InputDevice or a MIDI thru device (for thru + /// devices, this handle must belong to a MIDI OutputDevice). + /// + /// + /// Handle to the MIDI OutputDevice or thru device. + /// + /// + /// If an error occurred while connecting the two devices. + /// + public static void Connect(IntPtr handleA, IntPtr handleB) + { + int result = midiConnect(handleA, handleB, IntPtr.Zero); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new MidiDeviceException(result); + } + } + + /// + /// Disconnects a MIDI InputDevice from a MIDI thru or OutputDevice, or + /// disconnects a MIDI thru device from a MIDI OutputDevice. + /// + /// + /// Handle to a MIDI InputDevice or a MIDI thru device. + /// + /// + /// Handle to the MIDI OutputDevice to be disconnected. + /// + /// + /// If an error occurred while disconnecting the two devices. + /// + public static void Disconnect(IntPtr handleA, IntPtr handleB) + { + int result = midiDisconnect(handleA, handleB, IntPtr.Zero); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new MidiDeviceException(result); + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDeviceException.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDeviceException.cs new file mode 100644 index 00000000..3b80ce2f --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDeviceException.cs @@ -0,0 +1,73 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// The base class for all MIDI device exception classes. + /// + public class MidiDeviceException : DeviceException + { + #region Error Codes + + public const int MIDIERR_UNPREPARED = 64; /* header not prepared */ + public const int MIDIERR_STILLPLAYING = 65; /* still something playing */ + public const int MIDIERR_NOMAP = 66; /* no configured instruments */ + public const int MIDIERR_NOTREADY = 67; /* hardware is still busy */ + public const int MIDIERR_NODEVICE = 68; /* port no longer connected */ + public const int MIDIERR_INVALIDSETUP = 69; /* invalid MIF */ + public const int MIDIERR_BADOPENMODE = 70; /* operation unsupported w/ open mode */ + public const int MIDIERR_DONT_CONTINUE = 71; /* thru device 'eating' a message */ + public const int MIDIERR_LASTERROR = 71; /* last error in range */ + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the DeviceException class with the + /// specified error code. + /// + /// + /// The error code. + /// + public MidiDeviceException(int errCode) : base(errCode) + { + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiHeader.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiHeader.cs new file mode 100644 index 00000000..7bc4ed6d --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiHeader.cs @@ -0,0 +1,101 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Runtime.InteropServices; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Represents the Windows Multimedia MIDIHDR structure. + /// + [StructLayout(LayoutKind.Sequential)] + internal struct MidiHeader + { + #region MidiHeader Members + + /// + /// Pointer to MIDI data. + /// + public IntPtr data; + + /// + /// Size of the buffer. + /// + public int bufferLength; + + /// + /// Actual amount of data in the buffer. This value should be less than + /// or equal to the value given in the dwBufferLength member. + /// + public int bytesRecorded; + + /// + /// Custom user data. + /// + public int user; + + /// + /// Flags giving information about the buffer. + /// + public int flags; + + /// + /// Reserved; do not use. + /// + public IntPtr next; + + /// + /// Reserved; do not use. + /// + public int reserved; + + /// + /// Offset into the buffer when a callback is performed. (This + /// callback is generated because the MEVT_F_CALLBACK flag is + /// set in the dwEvent member of the MidiEventArgs structure.) + /// This offset enables an application to determine which + /// event caused the callback. + /// + public int offset; + + /// + /// Reserved; do not use. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst=4)] + public int[] reservedArray; + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiHeaderBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiHeaderBuilder.cs new file mode 100644 index 00000000..5664ccaf --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiHeaderBuilder.cs @@ -0,0 +1,248 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; +using System.Runtime.InteropServices; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Builds a pointer to a MidiHeader structure. + /// + internal class MidiHeaderBuilder + { + // The length of the system exclusive buffer. + private int bufferLength; + + // The system exclusive data. + private byte[] data; + + // Indicates whether the pointer to the MidiHeader has been built. + private bool built = false; + + // The built pointer to the MidiHeader. + private IntPtr result; + + /// + /// Initializes a new instance of the MidiHeaderBuilder. + /// + public MidiHeaderBuilder() + { + BufferLength = 1; + } + + #region Methods + + /// + /// Builds the pointer to the MidiHeader structure. + /// + public void Build() + { + MidiHeader header = new MidiHeader(); + + // Initialize the MidiHeader. + header.bufferLength = BufferLength; + header.bytesRecorded = BufferLength; + header.data = Marshal.AllocHGlobal(BufferLength); + header.flags = 0; + + // Write data to the MidiHeader. + for(int i = 0; i < BufferLength; i++) + { + Marshal.WriteByte(header.data, i, data[i]); + } + + try + { + result = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(MidiHeader))); + } + catch(Exception) + { + Marshal.FreeHGlobal(header.data); + + throw; + } + + try + { + Marshal.StructureToPtr(header, result, false); + } + catch(Exception) + { + Marshal.FreeHGlobal(header.data); + Marshal.FreeHGlobal(result); + + throw; + } + + built = true; + } + + /// + /// Initializes the MidiHeaderBuilder with the specified SysExMessage. + /// + /// + /// The SysExMessage to use for initializing the MidiHeaderBuilder. + /// + public void InitializeBuffer(SysExMessage message) + { + // If this is a start system exclusive message. + if(message.SysExType == SysExType.Start) + { + BufferLength = message.Length; + + // Copy entire message. + for(int i = 0; i < BufferLength; i++) + { + data[i] = message[i]; + } + } + // Else this is a continuation message. + else + { + BufferLength = message.Length - 1; + + // Copy all but the first byte of message. + for(int i = 0; i < BufferLength; i++) + { + data[i] = message[i + 1]; + } + } + } + + public void InitializeBuffer(ICollection events) + { + #region Require + + if(events == null) + { + throw new ArgumentNullException("events"); + } + else if(events.Count % 4 != 0) + { + throw new ArgumentException("Stream events not word aligned."); + } + + #endregion + + #region Guard + + if(events.Count == 0) + { + return; + } + + #endregion + + BufferLength = events.Count; + + events.CopyTo(data, 0); + } + + /// + /// Releases the resources associated with the built MidiHeader pointer. + /// + public void Destroy() + { + #region Require + + if(!built) + { + throw new InvalidOperationException("Cannot destroy MidiHeader"); + } + + #endregion + + Destroy(result); + } + + /// + /// Releases the resources associated with the specified MidiHeader pointer. + /// + /// + /// The MidiHeader pointer. + /// + public void Destroy(IntPtr headerPtr) + { + MidiHeader header = (MidiHeader)Marshal.PtrToStructure(headerPtr, typeof(MidiHeader)); + + Marshal.FreeHGlobal(header.data); + Marshal.FreeHGlobal(headerPtr); + } + + #endregion + + #region Properties + + /// + /// The length of the system exclusive buffer. + /// + public int BufferLength + { + get + { + return bufferLength; + } + set + { + #region Require + + if(value <= 0) + { + throw new ArgumentOutOfRangeException("BufferLength", value, + "MIDI header buffer length out of range."); + } + + #endregion + + bufferLength = value; + data = new byte[value]; + } + } + + /// + /// Gets the pointer to the MidiHeader. + /// + public IntPtr Result + { + get + { + return result; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/MidiOutCaps.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/MidiOutCaps.cs new file mode 100644 index 00000000..bfab9603 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/MidiOutCaps.cs @@ -0,0 +1,107 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Runtime.InteropServices; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Represents MIDI output device capabilities. + /// + [StructLayout(LayoutKind.Sequential)] + public struct MidiOutCaps + { + #region MidiOutCaps Members + + /// + /// Manufacturer identifier of the device driver for the Midi output + /// device. + /// + public short mid; + + /// + /// Product identifier of the Midi output device. + /// + public short pid; + + /// + /// Version number of the device driver for the Midi output device. The + /// high-order byte is the major version number, and the low-order byte + /// is the minor version number. + /// + public int driverVersion; + + /// + /// Product name. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string name; + + /// + /// Flags describing the type of the Midi output device. + /// + public short technology; + + /// + /// Number of voices supported by an internal synthesizer device. If + /// the device is a port, this member is not meaningful and is set + /// to 0. + /// + public short voices; + + /// + /// Maximum number of simultaneous notes that can be played by an + /// internal synthesizer device. If the device is a port, this member + /// is not meaningful and is set to 0. + /// + public short notes; + + /// + /// Channels that an internal synthesizer device responds to, where the + /// least significant bit refers to channel 0 and the most significant + /// bit to channel 15. Port devices that transmit on all channels set + /// this member to 0xFFFF. + /// + public short channelMask; + + /// + /// Optional functionality supported by the device. + /// + public int support; + + #endregion + } + +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/NoOpEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/NoOpEventArgs.cs new file mode 100644 index 00000000..ceed7af8 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/NoOpEventArgs.cs @@ -0,0 +1,59 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// + /// + public class NoOpEventArgs : EventArgs + { + private int data; + + public NoOpEventArgs(int data) + { + this.data = data; + } + + public int Data + { + get + { + return data; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDevice.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDevice.cs new file mode 100644 index 00000000..38e43d75 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDevice.cs @@ -0,0 +1,297 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Represents a device capable of sending MIDI messages. + /// + public sealed class OutputDevice : OutputDeviceBase + { + #region Win32 Midi Output Functions and Constants + + [DllImport("winmm.dll")] + private static extern int midiOutOpen(out IntPtr DeviceHandle, int deviceID, + MidiOutProc proc, IntPtr instance, int flags); + + [DllImport("winmm.dll")] + private static extern int midiOutClose(IntPtr DeviceHandle); + + #endregion + + private MidiOutProc midiOutProc; + + private bool runningStatusEnabled = false; + + private int runningStatus = 0; + + #region Construction + + /// + /// Initializes a new instance of the OutputDevice class. + /// + public OutputDevice(int deviceID) : base(deviceID) + { + midiOutProc = HandleMessage; + + int result = midiOutOpen(out DeviceHandle, deviceID, midiOutProc, IntPtr.Zero, CALLBACK_FUNCTION); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + + #endregion + + protected override void Dispose(bool disposing) + { + if(disposing) + { + lock(lockObject) + { + Reset(); + + // Close the OutputDevice. + int result = midiOutClose(Handle); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + // Throw an exception. + throw new OutputDeviceException(result); + } + } + } + else + { + midiOutReset(Handle); + midiOutClose(Handle); + } + + base.Dispose(disposing); + } + + /// + /// Closes the OutputDevice. + /// + /// + /// If an error occurred while closing the OutputDevice. + /// + public override void Close() + { + #region Guard + + if(IsDisposed) + { + return; + } + + #endregion + + Dispose(true); + } + + /// + /// Resets the OutputDevice. + /// + public override void Reset() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + runningStatus = 0; + + base.Reset(); + } + + public override void Send(ChannelMessage message) + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + lock(lockObject) + { + // If running status is enabled. + if(runningStatusEnabled) + { + // If the message's status value matches the running status. + if(message.Status == runningStatus) + { + // Send only the two data bytes without the status byte. + Send(message.Message >> 8); + } + // Else the message's status value does not match the running + // status. + else + { + // Send complete message with status byte. + Send(message.Message); + + // Update running status. + runningStatus = message.Status; + } + } + // Else running status has not been enabled. + else + { + Send(message.Message); + } + } + } + + public override void Send(SysExMessage message) + { + // System exclusive cancels running status. + runningStatus = 0; + + base.Send(message); + } + + public override void Send(SysCommonMessage message) + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + // System common cancels running status. + runningStatus = 0; + + base.Send(message); + } + + #region Properties + + /// + /// Gets or sets a value indicating whether the OutputDevice uses + /// a running status. + /// + public bool RunningStatusEnabled + { + get + { + return runningStatusEnabled; + } + set + { + runningStatusEnabled = value; + + // Reset running status. + runningStatus = 0; + } + } + + #endregion + } + + /// + /// The exception that is thrown when a error occurs with the OutputDevice + /// class. + /// + public class OutputDeviceException : MidiDeviceException + { + #region OutputDeviceException Members + + #region Win32 Midi Output Error Function + + [DllImport("winmm.dll", CharSet = CharSet.Unicode)] + private static extern int midiOutGetErrorText(int errCode, + StringBuilder message, int sizeOfMessage); + + #endregion + + #region Fields + + // The error message. + private StringBuilder message = new StringBuilder(128); + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the OutputDeviceException class with + /// the specified error code. + /// + /// + /// The error code. + /// + public OutputDeviceException(int errCode) : base(errCode) + { + // Get error message. + midiOutGetErrorText(errCode, message, message.Capacity); + } + + #endregion + + #region Properties + + /// + /// Gets a message that describes the current exception. + /// + public override string Message + { + get + { + return message.ToString(); + } + } + + #endregion + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDeviceBase.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDeviceBase.cs new file mode 100644 index 00000000..a76e997c --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDeviceBase.cs @@ -0,0 +1,357 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using Sanford.Threading; + +namespace Sanford.Multimedia.Midi +{ + public abstract class OutputDeviceBase : MidiDevice + { + [DllImport("winmm.dll")] + protected static extern int midiOutReset(IntPtr DeviceHandle); + + [DllImport("winmm.dll")] + protected static extern int midiOutShortMsg(IntPtr DeviceHandle, int message); + + [DllImport("winmm.dll")] + protected static extern int midiOutPrepareHeader(IntPtr DeviceHandle, + IntPtr headerPtr, int sizeOfMidiHeader); + + [DllImport("winmm.dll")] + protected static extern int midiOutUnprepareHeader(IntPtr DeviceHandle, + IntPtr headerPtr, int sizeOfMidiHeader); + + [DllImport("winmm.dll")] + protected static extern int midiOutLongMsg(IntPtr DeviceHandle, + IntPtr headerPtr, int sizeOfMidiHeader); + + [DllImport("winmm.dll")] + protected static extern int midiOutGetDevCaps(IntPtr deviceID, + ref MidiOutCaps caps, int sizeOfMidiOutCaps); + + [DllImport("winmm.dll")] + protected static extern int midiOutGetNumDevs(); + + protected const int MOM_OPEN = 0x3C7; + protected const int MOM_CLOSE = 0x3C8; + protected const int MOM_DONE = 0x3C9; + + protected delegate void GenericDelegate(T args); + + // Represents the method that handles messages from Windows. + protected delegate void MidiOutProc(IntPtr hnd, int msg, IntPtr instance, IntPtr param1, IntPtr param2); + + // For releasing buffers. + protected DelegateQueue delegateQueue = new DelegateQueue(); + + protected readonly object lockObject = new object(); + + // The number of buffers still in the queue. + protected int bufferCount = 0; + + // Builds MidiHeader structures for sending system exclusive messages. + private MidiHeaderBuilder headerBuilder = new MidiHeaderBuilder(); + + // The device handle. + protected IntPtr DeviceHandle = IntPtr.Zero; + + public OutputDeviceBase(int deviceID) : base(deviceID) + { + } + + ~OutputDeviceBase() + { + Dispose(false); + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + delegateQueue.Dispose(); + } + + base.Dispose(disposing); + } + + public virtual void Send(ChannelMessage message) + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + Send(message.Message); + } + + public virtual void SendShort(int message) + { + #region Require + + if (IsDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + Send(message); + } + + public virtual void Send(SysExMessage message) + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + lock(lockObject) + { + headerBuilder.InitializeBuffer(message); + headerBuilder.Build(); + + // Prepare system exclusive buffer. + int result = midiOutPrepareHeader(Handle, headerBuilder.Result, SizeOfMidiHeader); + + // If the system exclusive buffer was prepared successfully. + if(result == MidiDeviceException.MMSYSERR_NOERROR) + { + bufferCount++; + + // Send system exclusive message. + result = midiOutLongMsg(Handle, headerBuilder.Result, SizeOfMidiHeader); + + // If the system exclusive message could not be sent. + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + midiOutUnprepareHeader(Handle, headerBuilder.Result, SizeOfMidiHeader); + bufferCount--; + headerBuilder.Destroy(); + + // Throw an exception. + throw new OutputDeviceException(result); + } + } + // Else the system exclusive buffer could not be prepared. + else + { + // Destroy system exclusive buffer. + headerBuilder.Destroy(); + + // Throw an exception. + throw new OutputDeviceException(result); + } + } + } + + public virtual void Send(SysCommonMessage message) + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + Send(message.Message); + } + + public virtual void Send(SysRealtimeMessage message) + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + Send(message.Message); + } + + public override void Reset() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + lock(lockObject) + { + // Reset the OutputDevice. + int result = midiOutReset(Handle); + + if(result == MidiDeviceException.MMSYSERR_NOERROR) + { + while(bufferCount > 0) + { + Monitor.Wait(lockObject); + } + } + else + { + // Throw an exception. + throw new OutputDeviceException(result); + } + } + } + + protected void Send(int message) + { + lock(lockObject) + { + int result = midiOutShortMsg(Handle, message); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + } + + public static MidiOutCaps GetDeviceCapabilities(int deviceID) + { + MidiOutCaps caps = new MidiOutCaps(); + + // Get the device's capabilities. + IntPtr devId = (IntPtr)deviceID; + int result = midiOutGetDevCaps(devId, ref caps, Marshal.SizeOf(caps)); + + // If the capabilities could not be retrieved. + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + // Throw an exception. + throw new OutputDeviceException(result); + } + + return caps; + } + + // Handles Windows messages. + protected virtual void HandleMessage(IntPtr hnd, int msg, IntPtr instance, IntPtr param1, IntPtr param2) + { + if(msg == MOM_OPEN) + { + } + else if(msg == MOM_CLOSE) + { + } + else if(msg == MOM_DONE) + { + delegateQueue.Post(ReleaseBuffer, param1); + } + } + + // Releases buffers. + private void ReleaseBuffer(object state) + { + lock(lockObject) + { + IntPtr headerPtr = (IntPtr)state; + + // Unprepare the buffer. + int result = midiOutUnprepareHeader(Handle, headerPtr, SizeOfMidiHeader); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + Exception ex = new OutputDeviceException(result); + + OnError(new ErrorEventArgs(ex)); + } + + // Release the buffer resources. + headerBuilder.Destroy(headerPtr); + + bufferCount--; + + Monitor.Pulse(lockObject); + + Debug.Assert(bufferCount >= 0); + } + } + + public override void Dispose() + { + #region Guard + + if(IsDisposed) + { + return; + } + + #endregion + + lock(lockObject) + { + Close(); + } + } + + public override IntPtr Handle + { + get + { + return DeviceHandle; + } + } + + public static int DeviceCount + { + get + { + return midiOutGetNumDevs(); + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputStream.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputStream.cs new file mode 100644 index 00000000..b1c5947b --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputStream.cs @@ -0,0 +1,617 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Threading; +using Sanford.Multimedia.Timers; + +namespace Sanford.Multimedia.Midi +{ + public sealed class OutputStream : OutputDeviceBase + { + [DllImport("winmm.dll")] + private static extern int midiStreamOpen(ref IntPtr DeviceHandle, ref int deviceID, int reserved, + OutputDevice.MidiOutProc proc, IntPtr instance, uint flag); + + [DllImport("winmm.dll")] + private static extern int midiStreamClose(IntPtr DeviceHandle); + + [DllImport("winmm.dll")] + private static extern int midiStreamOut(IntPtr DeviceHandle, IntPtr headerPtr, int sizeOfMidiHeader); + + [DllImport("winmm.dll")] + private static extern int midiStreamPause(IntPtr DeviceHandle); + + [DllImport("winmm.dll")] + private static extern int midiStreamPosition(IntPtr DeviceHandle, ref Time t, int sizeOfTime); + + [DllImport("winmm.dll")] + private static extern int midiStreamProperty(IntPtr DeviceHandle, ref Property p, uint flags); + + [DllImport("winmm.dll")] + private static extern int midiStreamRestart(IntPtr DeviceHandle); + + [DllImport("winmm.dll")] + private static extern int midiStreamStop(IntPtr DeviceHandle); + + [StructLayout(LayoutKind.Sequential)] + private struct Property + { + public int sizeOfProperty; + public int property; + } + + private const uint MIDIPROP_SET = 0x80000000; + private const uint MIDIPROP_GET = 0x40000000; + private const uint MIDIPROP_TIMEDIV = 0x00000001; + private const uint MIDIPROP_TEMPO = 0x00000002; + + private const byte MEVT_CALLBACK = 0x40; + + private const byte MEVT_SHORTMSG = 0x00; + private const byte MEVT_TEMPO = 0x01; + private const byte MEVT_NOP = 0x02; + private const byte MEVT_LONGMSG = 0x80; + private const byte MEVT_COMMENT = 0x82; + private const byte MEVT_VERSION = 0x84; + + private const int MOM_POSITIONCB = 0x3CA; + + private const int SizeOfMidiEvent = 12; + + private const int EventTypeIndex = 11; + + private const int EventCodeOffset = 8; + + private MidiOutProc midiOutProc; + + private int offsetTicks = 0; + + private byte[] streamID = new byte[4]; + + private List events = new List(); + + private MidiHeaderBuilder headerBuilder = new MidiHeaderBuilder(); + + public event EventHandler NoOpOccurred; + + public OutputStream(int deviceID) : base(deviceID) + { + midiOutProc = HandleMessage; + + int result = midiStreamOpen(ref DeviceHandle, ref deviceID, 1, midiOutProc, IntPtr.Zero, CALLBACK_FUNCTION); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + lock(lockObject) + { + Reset(); + + int result = midiStreamClose(Handle); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + } + else + { + midiOutReset(Handle); + midiStreamClose(Handle); + } + + base.Dispose(disposing); + } + + public override void Close() + { + #region Guard + + if(IsDisposed) + { + return; + } + + #endregion + + Dispose(true); + } + + public void StartPlaying() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + lock(lockObject) + { + int result = midiStreamRestart(Handle); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + } + + public void PausePlaying() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + lock(lockObject) + { + int result = midiStreamPause(Handle); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + } + + public void StopPlaying() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + lock(lockObject) + { + int result = midiStreamStop(Handle); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + } + + public override void Reset() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + offsetTicks = 0; + events.Clear(); + + base.Reset(); + } + + public void Write(MidiEvent e) + { + switch(e.MidiMessage.MessageType) + { + case MessageType.Channel: + case MessageType.SystemCommon: + case MessageType.SystemRealtime: + Write(e.DeltaTicks, (ShortMessage)e.MidiMessage); + break; + + case MessageType.SystemExclusive: + Write(e.DeltaTicks, (SysExMessage)e.MidiMessage); + break; + + case MessageType.Meta: + Write(e.DeltaTicks, (MetaMessage)e.MidiMessage); + break; + } + } + + private void Write(int deltaTicks, ShortMessage message) + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + // Delta time. + events.AddRange(BitConverter.GetBytes(deltaTicks + offsetTicks)); + + // Stream ID. + events.AddRange(streamID); + + // Event code. + byte[] eventCode = message.GetBytes(); + eventCode[eventCode.Length - 1] = MEVT_SHORTMSG; + events.AddRange(eventCode); + + offsetTicks = 0; + } + + private void Write(int deltaTicks, SysExMessage message) + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + // Delta time. + events.AddRange(BitConverter.GetBytes(deltaTicks + offsetTicks)); + + // Stream ID. + events.AddRange(streamID); + + // Event code. + byte[] eventCode = BitConverter.GetBytes(message.Length); + eventCode[eventCode.Length - 1] = MEVT_LONGMSG; + events.AddRange(eventCode); + + byte[] sysExData; + + if(message.Length % 4 != 0) + { + sysExData = new byte[message.Length + (message.Length % 4)]; + message.GetBytes().CopyTo(sysExData, 0); + } + else + { + sysExData = message.GetBytes(); + } + + // SysEx data. + events.AddRange(sysExData); + + offsetTicks = 0; + } + + private void Write(int deltaTicks, MetaMessage message) + { + if(message.MetaType == MetaType.Tempo) + { + // Delta time. + events.AddRange(BitConverter.GetBytes(deltaTicks + offsetTicks)); + + // Stream ID. + events.AddRange(streamID); + + TempoChangeBuilder builder = new TempoChangeBuilder(message); + + byte[] t = BitConverter.GetBytes(builder.Tempo); + + t[t.Length - 1] = MEVT_SHORTMSG | MEVT_TEMPO; + + // Event code. + events.AddRange(t); + + offsetTicks = 0; + } + else + { + offsetTicks += deltaTicks; + } + } + + public void WriteNoOp(int deltaTicks, int data) + { + // Delta time. + events.AddRange(BitConverter.GetBytes(deltaTicks + offsetTicks)); + + // Stream ID. + events.AddRange(streamID); + + // Event code. + byte[] eventCode = BitConverter.GetBytes(data); + eventCode[eventCode.Length - 1] = (byte)(MEVT_NOP | MEVT_CALLBACK); + events.AddRange(eventCode); + + offsetTicks = 0; + } + + public void Flush() + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + lock(lockObject) + { + headerBuilder.InitializeBuffer(events); + headerBuilder.Build(); + + events.Clear(); + + int result = midiOutPrepareHeader(Handle, headerBuilder.Result, SizeOfMidiHeader); + + if(result == MidiDeviceException.MMSYSERR_NOERROR) + { + bufferCount++; + } + else + { + headerBuilder.Destroy(); + + throw new OutputDeviceException(result); + } + + result = midiStreamOut(Handle, headerBuilder.Result, SizeOfMidiHeader); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + midiOutUnprepareHeader(Handle, headerBuilder.Result, SizeOfMidiHeader); + + headerBuilder.Destroy(); + + throw new OutputDeviceException(result); + } + } + } + + public Time GetTime(TimeType type) + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + Time t = new Time(); + + t.type = (int)type; + + lock(lockObject) + { + int result = midiStreamPosition(Handle, ref t, Marshal.SizeOf(typeof(Time))); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + + return t; + } + + private void OnNoOpOccurred(NoOpEventArgs e) + { + EventHandler handler = NoOpOccurred; + + if(handler != null) + { + handler(this, e); + } + } + + protected override void HandleMessage(IntPtr hnd, int msg, IntPtr instance, IntPtr param1, IntPtr param2) + { + if(msg == MOM_POSITIONCB) + { + delegateQueue.Post(HandleNoOp, param1); + } + else + { + base.HandleMessage(DeviceHandle, msg, instance, param1, param2); + } + } + + private void HandleNoOp(object state) + { + IntPtr headerPtr = (IntPtr)state; + MidiHeader header = (MidiHeader)Marshal.PtrToStructure(headerPtr, typeof(MidiHeader)); + + byte[] midiEvent = new byte[SizeOfMidiEvent]; + + for(int i = 0; i < midiEvent.Length; i++) + { + midiEvent[i] = Marshal.ReadByte(header.data, header.offset + i); + } + + // If this is a NoOp event. + if((midiEvent[EventTypeIndex] & MEVT_NOP) == MEVT_NOP) + { + // Clear the event type byte. + midiEvent[EventTypeIndex] = 0; + + NoOpEventArgs e = new NoOpEventArgs(BitConverter.ToInt32(midiEvent, EventCodeOffset)); + + context.Post(new SendOrPostCallback(delegate(object s) + { + OnNoOpOccurred(e); + }), null); + } + } + + public int Division + { + get + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + Property d = new Property(); + + d.sizeOfProperty = Marshal.SizeOf(typeof(Property)); + + lock(lockObject) + { + int result = midiStreamProperty(Handle, ref d, MIDIPROP_GET | MIDIPROP_TIMEDIV); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + + return d.property; + } + set + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + else if(value < PpqnClock.PpqnMinValue) + { + throw new ArgumentOutOfRangeException("Ppqn", value, + "Pulses per quarter note is smaller than 24."); + } + + #endregion + + Property d = new Property(); + + d.sizeOfProperty = Marshal.SizeOf(typeof(Property)); + d.property = value; + + lock(lockObject) + { + int result = midiStreamProperty(Handle, ref d, MIDIPROP_SET | MIDIPROP_TIMEDIV); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + } + } + + public int Tempo + { + get + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + + #endregion + + Property t = new Property(); + t.sizeOfProperty = Marshal.SizeOf(typeof(Property)); + + lock(lockObject) + { + int result = midiStreamProperty(Handle, ref t, MIDIPROP_GET | MIDIPROP_TEMPO); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + + return t.property; + } + set + { + #region Require + + if(IsDisposed) + { + throw new ObjectDisposedException("OutputStream"); + } + else if(value < 0) + { + throw new ArgumentOutOfRangeException("Tempo", value, + "Tempo out of range."); + } + + #endregion + + Property t = new Property(); + t.sizeOfProperty = Marshal.SizeOf(typeof(Property)); + t.property = value; + + lock(lockObject) + { + int result = midiStreamProperty(Handle, ref t, MIDIPROP_SET | MIDIPROP_TEMPO); + + if(result != MidiDeviceException.MMSYSERR_NOERROR) + { + throw new OutputDeviceException(result); + } + } + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/GeneralMidi.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/GeneralMidi.cs new file mode 100644 index 00000000..b5da3374 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/GeneralMidi.cs @@ -0,0 +1,171 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +namespace Sanford.Multimedia.Midi +{ + /// + /// Defines constants representing the General MIDI instrument set. + /// + public enum GeneralMidiInstrument + { + AcousticGrandPiano, + BrightAcousticPiano, + ElectricGrandPiano, + HonkyTonkPiano, + ElectricPiano1, + ElectricPiano2, + Harpsichord, + Clavinet, + Celesta, + Glockenspiel, + MusicBox, + Vibraphone, + Marimba, + Xylophone, + TubularBells, + Dulcimer, + DrawbarOrgan, + PercussiveOrgan, + RockOrgan, + ChurchOrgan, + ReedOrgan, + Accordion, + Harmonica, + TangoAccordion, + AcousticGuitarNylon, + AcousticGuitarSteel, + ElectricGuitarJazz, + ElectricGuitarClean, + ElectricGuitarMuted, + OverdrivenGuitar, + DistortionGuitar, + GuitarHarmonics, + AcousticBass, + ElectricBassFinger, + ElectricBassPick, + FretlessBass, + SlapBass1, + SlapBass2, + SynthBass1, + SynthBass2, + Violin, + Viola, + Cello, + Contrabass, + TremoloStrings, + PizzicatoStrings, + OrchestralHarp, + Timpani, + StringEnsemble1, + StringEnsemble2, + SynthStrings1, + SynthStrings2, + ChoirAahs, + VoiceOohs, + SynthVoice, + OrchestraHit, + Trumpet, + Trombone, + Tuba, + MutedTrumpet, + FrenchHorn, + BrassSection, + SynthBrass1, + SynthBrass2, + SopranoSax, + AltoSax, + TenorSax, + BaritoneSax, + Oboe, + EnglishHorn, + Bassoon, + Clarinet, + Piccolo, + Flute, + Recorder, + PanFlute, + BlownBottle, + Shakuhachi, + Whistle, + Ocarina, + Lead1Square, + Lead2Sawtooth, + Lead3Calliope, + Lead4Chiff, + Lead5Charang, + Lead6Voice, + Lead7Fifths, + Lead8BassAndLead, + Pad1NewAge, + Pad2Warm, + Pad3Polysynth, + Pad4Choir, + Pad5Bowed, + Pad6Metallic, + Pad7Halo, + Pad8Sweep, + Fx1Rain, + Fx2Soundtrack, + Fx3Crystal, + Fx4Atmosphere, + Fx5Brightness, + Fx6Goblins, + Fx7Echoes, + Fx8SciFi, + Sitar, + Banjo, + Shamisen, + Koto, + Kalimba, + BagPipe, + Fiddle, + Shanai, + TinkleBell, + Agogo, + SteelDrums, + Woodblock, + TaikoDrum, + MelodicTom, + SynthDrum, + ReverseCymbal, + GuitarFretNoise, + BreathNoise, + Seashore, + BirdTweet, + TelephoneRing, + Helicopter, + Applause, + Gunshot + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ChannelMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ChannelMessage.cs new file mode 100644 index 00000000..95b69d21 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ChannelMessage.cs @@ -0,0 +1,746 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Multimedia.Midi +{ + #region Channel Command Types + + /// + /// Defines constants for ChannelMessage types. + /// + public enum ChannelCommand + { + /// + /// Represents the note-off command type. + /// + NoteOff = 0x80, + + /// + /// Represents the note-on command type. + /// + NoteOn = 0x90, + + /// + /// Represents the poly pressure (aftertouch) command type. + /// + PolyPressure = 0xA0, + + /// + /// Represents the controller command type. + /// + Controller = 0xB0, + + /// + /// Represents the program change command type. + /// + ProgramChange = 0xC0, + + /// + /// Represents the channel pressure (aftertouch) command + /// type. + /// + ChannelPressure = 0xD0, + + /// + /// Represents the pitch wheel command type. + /// + PitchWheel = 0xE0 + } + + #endregion + + #region Controller Types + + /// + /// Defines constants for controller types. + /// + public enum ControllerType + { + /// + /// The Bank Select coarse. + /// + BankSelect, + + /// + /// The Modulation Wheel coarse. + /// + ModulationWheel, + + /// + /// The Breath Control coarse. + /// + BreathControl, + + /// + /// The Foot Pedal coarse. + /// + FootPedal = 4, + + /// + /// The Portamento Time coarse. + /// + PortamentoTime, + + /// + /// The Data Entry Slider coarse. + /// + DataEntrySlider, + + /// + /// The Volume coarse. + /// + Volume, + + /// + /// The Balance coarse. + /// + Balance, + + /// + /// The Pan position coarse. + /// + Pan = 10, + + /// + /// The Expression coarse. + /// + Expression, + + /// + /// The Effect Control 1 coarse. + /// + EffectControl1, + + /// + /// The Effect Control 2 coarse. + /// + EffectControl2, + + /// + /// The General Puprose Slider 1 + /// + GeneralPurposeSlider1 = 16, + + /// + /// The General Puprose Slider 2 + /// + GeneralPurposeSlider2, + + /// + /// The General Puprose Slider 3 + /// + GeneralPurposeSlider3, + + /// + /// The General Puprose Slider 4 + /// + GeneralPurposeSlider4, + + /// + /// The Bank Select fine. + /// + BankSelectFine = 32, + + /// + /// The Modulation Wheel fine. + /// + ModulationWheelFine, + + /// + /// The Breath Control fine. + /// + BreathControlFine, + + /// + /// The Foot Pedal fine. + /// + FootPedalFine = 36, + + /// + /// The Portamento Time fine. + /// + PortamentoTimeFine, + + /// + /// The Data Entry Slider fine. + /// + DataEntrySliderFine, + + /// + /// The Volume fine. + /// + VolumeFine, + + /// + /// The Balance fine. + /// + BalanceFine, + + /// + /// The Pan position fine. + /// + PanFine = 42, + + /// + /// The Expression fine. + /// + ExpressionFine, + + /// + /// The Effect Control 1 fine. + /// + EffectControl1Fine, + + /// + /// The Effect Control 2 fine. + /// + EffectControl2Fine, + + /// + /// The Hold Pedal 1. + /// + HoldPedal1 = 64, + + /// + /// The Portamento. + /// + Portamento, + + /// + /// The Sustenuto Pedal. + /// + SustenutoPedal, + + /// + /// The Soft Pedal. + /// + SoftPedal, + + /// + /// The Legato Pedal. + /// + LegatoPedal, + + /// + /// The Hold Pedal 2. + /// + HoldPedal2, + + /// + /// The Sound Variation. + /// + SoundVariation, + + /// + /// The Sound Timbre. + /// + SoundTimbre, + + /// + /// The Sound Release Time. + /// + SoundReleaseTime, + + /// + /// The Sound Attack Time. + /// + SoundAttackTime, + + /// + /// The Sound Brightness. + /// + SoundBrightness, + + /// + /// The Sound Control 6. + /// + SoundControl6, + + /// + /// The Sound Control 7. + /// + SoundControl7, + + /// + /// The Sound Control 8. + /// + SoundControl8, + + /// + /// The Sound Control 9. + /// + SoundControl9, + + /// + /// The Sound Control 10. + /// + SoundControl10, + + /// + /// The General Purpose Button 1. + /// + GeneralPurposeButton1, + + /// + /// The General Purpose Button 2. + /// + GeneralPurposeButton2, + + /// + /// The General Purpose Button 3. + /// + GeneralPurposeButton3, + + /// + /// The General Purpose Button 4. + /// + GeneralPurposeButton4, + + /// + /// The Effects Level. + /// + EffectsLevel = 91, + + /// + /// The Tremolo Level. + /// + TremoloLevel, + + /// + /// The Chorus Level. + /// + ChorusLevel, + + /// + /// The Celeste Level. + /// + CelesteLevel, + + /// + /// The Phaser Level. + /// + PhaserLevel, + + /// + /// The Data Button Increment. + /// + DataButtonIncrement, + + /// + /// The Data Button Decrement. + /// + DataButtonDecrement, + + /// + /// The NonRegistered Parameter Fine. + /// + NonRegisteredParameterFine, + + /// + /// The NonRegistered Parameter Coarse. + /// + NonRegisteredParameterCoarse, + + /// + /// The Registered Parameter Fine. + /// + RegisteredParameterFine, + + /// + /// The Registered Parameter Coarse. + /// + RegisteredParameterCoarse, + + /// + /// The All Sound Off. + /// + AllSoundOff = 120, + + /// + /// The All Controllers Off. + /// + AllControllersOff, + + /// + /// The Local Keyboard. + /// + LocalKeyboard, + + /// + /// The All Notes Off. + /// + AllNotesOff, + + /// + /// The Omni Mode Off. + /// + OmniModeOff, + + /// + /// The Omni Mode On. + /// + OmniModeOn, + + /// + /// The Mono Operation. + /// + MonoOperation, + + /// + /// The Poly Operation. + /// + PolyOperation + } + + #endregion + + /// + /// Represents MIDI channel messages. + /// + [ImmutableObject(true)] + public sealed class ChannelMessage : ShortMessage + { + #region ChannelEventArgs Members + + #region Constants + + // + // Bit manipulation constants. + // + + private const int MidiChannelMask = ~15; + private const int CommandMask = ~240; + + /// + /// Maximum value allowed for MIDI channels. + /// + public const int MidiChannelMaxValue = 15; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the ChannelEventArgs class with the + /// specified command, MIDI channel, and data 1 values. + /// + /// + /// The command value. + /// + /// + /// The MIDI channel. + /// + /// + /// The data 1 value. + /// + /// + /// If midiChannel is less than zero or greater than 15. Or if + /// data1 is less than zero or greater than 127. + /// + public ChannelMessage(ChannelCommand command, int midiChannel, int data1) + { + msg = 0; + + msg = PackCommand(msg, command); + msg = PackMidiChannel(msg, midiChannel); + msg = PackData1(msg, data1); + + #region Ensure + + Debug.Assert(Command == command); + Debug.Assert(MidiChannel == midiChannel); + Debug.Assert(Data1 == data1); + + #endregion + } + + /// + /// Initializes a new instance of the ChannelEventArgs class with the + /// specified command, MIDI channel, data 1, and data 2 values. + /// + /// + /// The command value. + /// + /// + /// The MIDI channel. + /// + /// + /// The data 1 value. + /// + /// + /// The data 2 value. + /// + /// + /// If midiChannel is less than zero or greater than 15. Or if + /// data1 or data 2 is less than zero or greater than 127. + /// + public ChannelMessage(ChannelCommand command, int midiChannel, + int data1, int data2) + { + msg = 0; + + msg = PackCommand(msg, command); + msg = PackMidiChannel(msg, midiChannel); + msg = PackData1(msg, data1); + msg = PackData2(msg, data2); + + #region Ensure + + Debug.Assert(Command == command); + Debug.Assert(MidiChannel == midiChannel); + Debug.Assert(Data1 == data1); + Debug.Assert(Data2 == data2); + + #endregion + } + + internal ChannelMessage(int message) + { + this.msg = message; + } + + #endregion + + #region Methods + + /// + /// Returns a value for the current ChannelEventArgs suitable for use in + /// hashing algorithms. + /// + /// + /// A hash code for the current ChannelEventArgs. + /// + public override int GetHashCode() + { + return msg; + } + + /// + /// Determines whether two ChannelEventArgs instances are equal. + /// + /// + /// The ChannelMessageEventArgs to compare with the current ChannelEventArgs. + /// + /// + /// true if the specified object is equal to the current + /// ChannelMessageEventArgs; otherwise, false. + /// + public override bool Equals(object obj) + { + #region Guard + + if(!(obj is ChannelMessage)) + { + return false; + } + + #endregion + + ChannelMessage e = (ChannelMessage)obj; + + return this.msg == e.msg; + } + + /// + /// Returns a value indicating how many bytes are used for the + /// specified ChannelCommand. + /// + /// + /// The ChannelCommand value to test. + /// + /// + /// The number of bytes used for the specified ChannelCommand. + /// + internal static int DataBytesPerType(ChannelCommand command) + { + int result; + + if(command == ChannelCommand.ChannelPressure || + command == ChannelCommand.ProgramChange) + { + result = 1; + } + else + { + result = 2; + } + + return result; + } + + /// + /// Unpacks the command value from the specified integer channel + /// message. + /// + /// + /// The message to unpack. + /// + /// + /// The command value for the packed message. + /// + internal static ChannelCommand UnpackCommand(int message) + { + return (ChannelCommand)(message & DataMask & MidiChannelMask); + } + + /// + /// Unpacks the MIDI channel from the specified integer channel + /// message. + /// + /// + /// The message to unpack. + /// + /// + /// The MIDI channel for the pack message. + /// + internal static int UnpackMidiChannel(int message) + { + return message & DataMask & CommandMask; + } + + /// + /// Packs the MIDI channel into the specified integer message. + /// + /// + /// The message into which the MIDI channel is packed. + /// + /// + /// The MIDI channel to pack into the message. + /// + /// + /// An integer message. + /// + /// + /// If midiChannel is less than zero or greater than 15. + /// + internal static int PackMidiChannel(int message, int midiChannel) + { + #region Preconditons + + if(midiChannel < 0 || midiChannel > MidiChannelMaxValue) + { + throw new ArgumentOutOfRangeException("midiChannel", midiChannel, + "MIDI channel out of range."); + } + + #endregion + + return (message & MidiChannelMask) | midiChannel; + } + + /// + /// Packs the command value into an integer message. + /// + /// + /// The message into which the command is packed. + /// + /// + /// The command value to pack into the message. + /// + /// + /// An integer message. + /// + internal static int PackCommand(int message, ChannelCommand command) + { + return (message & CommandMask) | (int)command; + } + + #endregion + + #region Properties + + /// + /// Gets the channel command value. + /// + public ChannelCommand Command + { + get + { + return UnpackCommand(msg); + } + } + + /// + /// Gets the MIDI channel. + /// + public int MidiChannel + { + get + { + return UnpackMidiChannel(msg); + } + } + + /// + /// Gets the first data value. + /// + public int Data1 + { + get + { + return UnpackData1(msg); + } + } + + /// + /// Gets the second data value. + /// + public int Data2 + { + get + { + return UnpackData2(msg); + } + } + + /// + /// Gets the EventType. + /// + public override MessageType MessageType + { + get + { + return MessageType.Channel; + } + } + + #endregion + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ChannelMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ChannelMessageEventArgs.cs new file mode 100644 index 00000000..f5ce09b6 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ChannelMessageEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class ChannelMessageEventArgs : MidiEventArgs + { + private ChannelMessage message; + + public ChannelMessageEventArgs(ChannelMessage message, int absoluteTicks = -1) + { + this.message = message; + this.AbsoluteTicks = absoluteTicks; + } + + public ChannelMessage Message + { + get + { + return message; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidShortMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidShortMessageEventArgs.cs new file mode 100644 index 00000000..6b87f2ec --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidShortMessageEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class InvalidShortMessageEventArgs : MidiEventArgs + { + private int message; + + public InvalidShortMessageEventArgs(int message, int absoluteTicks = -1) + { + this.message = message; + this.AbsoluteTicks = absoluteTicks; + } + + public int Message + { + get + { + return message; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidSysExMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidSysExMessageEventArgs.cs new file mode 100644 index 00000000..81b297ec --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidSysExMessageEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class InvalidSysExMessageEventArgs : MidiEventArgs + { + private byte[] messageData; + + public InvalidSysExMessageEventArgs(byte[] messageData, int absoluteTicks = -1) + { + this.messageData = messageData; + this.AbsoluteTicks = absoluteTicks; + } + + public ICollection MessageData + { + get + { + return messageData; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MetaMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MetaMessageEventArgs.cs new file mode 100644 index 00000000..d935b7cb --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MetaMessageEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class MetaMessageEventArgs : MidiEventArgs + { + private MetaMessage message; + + public MetaMessageEventArgs(MetaMessage message, int absoluteTicks = -1) + { + this.message = message; + this.AbsoluteTicks = absoluteTicks; + } + + public MetaMessage Message + { + get + { + return message; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MidiEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MidiEventArgs.cs new file mode 100644 index 00000000..a1e0921c --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MidiEventArgs.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class MidiEventArgs : EventArgs + { + public int AbsoluteTicks { get; set; } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ShortMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ShortMessageEventArgs.cs new file mode 100644 index 00000000..60f26ee9 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ShortMessageEventArgs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Raw short message as int or byte array, useful when working with VST. + /// + public class ShortMessageEventArgs : MidiEventArgs + { + ShortMessage message; + + public ShortMessageEventArgs(ShortMessage message, int absoluteTicks = -1) + { + this.message = message; + this.AbsoluteTicks = absoluteTicks; + } + + public ShortMessageEventArgs(int message, int timestamp = 0, int absoluteTicks = -1) + { + this.message = new ShortMessage(message); + this.message.Timestamp = timestamp; + this.AbsoluteTicks = absoluteTicks; + } + + public ShortMessageEventArgs(byte status, byte data1, byte data2, int absoluteTicks = -1) + { + this.message = new ShortMessage(status, data1, data2); + this.AbsoluteTicks = absoluteTicks; + } + + public ShortMessage Message + { + get + { + return message; + } + } + + public static ShortMessageEventArgs FromChannelMessage(ChannelMessageEventArgs arg) + { + return new ShortMessageEventArgs(arg.Message); + } + + public static ShortMessageEventArgs FromSysCommonMessage(SysCommonMessageEventArgs arg) + { + return new ShortMessageEventArgs(arg.Message); + } + + public static ShortMessageEventArgs FromSysRealtimeMessage(SysRealtimeMessageEventArgs arg) + { + return new ShortMessageEventArgs(arg.Message); + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysCommonMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysCommonMessageEventArgs.cs new file mode 100644 index 00000000..b7a8f4bb --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysCommonMessageEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class SysCommonMessageEventArgs : MidiEventArgs + { + private SysCommonMessage message; + + public SysCommonMessageEventArgs(SysCommonMessage message, int absoluteTicks = -1) + { + this.message = message; + this.AbsoluteTicks = absoluteTicks; + } + + public SysCommonMessage Message + { + get + { + return message; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysExMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysExMessageEventArgs.cs new file mode 100644 index 00000000..31288048 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysExMessageEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class SysExMessageEventArgs : MidiEventArgs + { + private SysExMessage message; + + public SysExMessageEventArgs(SysExMessage message, int absoluteTicks = -1) + { + this.message = message; + this.AbsoluteTicks = absoluteTicks; + } + + public SysExMessage Message + { + get + { + return message; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysRealtimeMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysRealtimeMessageEventArgs.cs new file mode 100644 index 00000000..32ef7269 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysRealtimeMessageEventArgs.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class SysRealtimeMessageEventArgs : EventArgs + { + public static readonly SysRealtimeMessageEventArgs Start = new SysRealtimeMessageEventArgs(SysRealtimeMessage.StartMessage); + + public static readonly SysRealtimeMessageEventArgs Continue = new SysRealtimeMessageEventArgs(SysRealtimeMessage.ContinueMessage); + + public static readonly SysRealtimeMessageEventArgs Stop = new SysRealtimeMessageEventArgs(SysRealtimeMessage.StopMessage); + + public static readonly SysRealtimeMessageEventArgs Clock = new SysRealtimeMessageEventArgs(SysRealtimeMessage.ClockMessage); + + public static readonly SysRealtimeMessageEventArgs Tick = new SysRealtimeMessageEventArgs(SysRealtimeMessage.TickMessage); + + public static readonly SysRealtimeMessageEventArgs ActiveSense = new SysRealtimeMessageEventArgs(SysRealtimeMessage.ActiveSenseMessage); + + public static readonly SysRealtimeMessageEventArgs Reset = new SysRealtimeMessageEventArgs(SysRealtimeMessage.ResetMessage); + + private SysRealtimeMessage message; + + private SysRealtimeMessageEventArgs(SysRealtimeMessage message) + { + this.message = message; + } + + public SysRealtimeMessage Message + { + get + { + return message; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/IMidiMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/IMidiMessage.cs new file mode 100644 index 00000000..a41134d1 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/IMidiMessage.cs @@ -0,0 +1,96 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Defines constants representing MIDI message types. + /// + public enum MessageType + { + Channel, + + SystemExclusive, + + SystemCommon, + + SystemRealtime, + + Meta, + + Short + } + + /// + /// Represents the basic functionality for all MIDI messages. + /// + public interface IMidiMessage + { + /// + /// Gets a byte array representation of the MIDI message. + /// + /// + /// A byte array representation of the MIDI message. + /// + byte[] GetBytes(); + + /// + /// Gets the MIDI message's status value. + /// + int Status + { + get; + } + + /// + /// Gets the MIDI event's type. + /// + MessageType MessageType + { + get; + } + + /// + /// Delta samples when the event should be processed in the next audio buffer. + /// Leave at 0 for realtime input to play as fast as possible. + /// Set to the desired sample in the next buffer if you play a midi sequence synchronized to the audio callback + /// + int DeltaFrames + { + get; + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/ChannelMessageBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/ChannelMessageBuilder.cs new file mode 100644 index 00000000..086a9236 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/ChannelMessageBuilder.cs @@ -0,0 +1,256 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Provides functionality for building ChannelMessages. + /// + public class ChannelMessageBuilder : IMessageBuilder + { + #region ChannelMessageBuilder Members + + #region Class Fields + + // Stores the ChannelMessages. + private static Hashtable messageCache = Hashtable.Synchronized(new Hashtable()); + + #endregion + + #region Fields + + // The channel message as a packed integer. + private int message = 0; + + // The built ChannelMessage + private ChannelMessage result = null; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the ChannelMessageBuilder class. + /// + public ChannelMessageBuilder() + { + Command = ChannelCommand.Controller; + MidiChannel = 0; + Data1 = (int)ControllerType.AllSoundOff; + Data2 = 0; + } + + /// + /// Initializes a new instance of the ChannelMessageBuilder class with + /// the specified ChannelMessageEventArgs. + /// + /// + /// The ChannelMessageEventArgs to use for initializing the ChannelMessageBuilder. + /// + /// + /// The ChannelMessageBuilder uses the specified ChannelMessageEventArgs to + /// initialize its property values. + /// + public ChannelMessageBuilder(ChannelMessage message) + { + Initialize(message); + } + + #endregion + + #region Methods + + /// + /// Initializes the ChannelMessageBuilder with the specified + /// ChannelMessageEventArgs. + /// + /// + /// The ChannelMessageEventArgs to use for initializing the ChannelMessageBuilder. + /// + public void Initialize(ChannelMessage message) + { + this.message = message.Message; + } + + /// + /// Clears the ChannelMessageEventArgs cache. + /// + public static void Clear() + { + messageCache.Clear(); + } + + #endregion + + #region Properties + + /// + /// Gets the number of messages in the ChannelMessageEventArgs cache. + /// + public static int Count + { + get + { + return messageCache.Count; + } + } + + /// + /// Gets the built ChannelMessageEventArgs. + /// + public ChannelMessage Result + { + get + { + return result; + } + } + + /// + /// Gets or sets the ChannelMessageEventArgs as a packed integer. + /// + internal int Message + { + get + { + return message; + } + set + { + message = value; + } + } + + /// + /// Gets or sets the Command value to use for building the + /// ChannelMessageEventArgs. + /// + public ChannelCommand Command + { + get + { + return ChannelMessage.UnpackCommand(message); + } + set + { + message = ChannelMessage.PackCommand(message, value); + } + } + + /// + /// Gets or sets the MIDI channel to use for building the + /// ChannelMessageEventArgs. + /// + /// + /// MidiChannel is set to a value less than zero or greater than 15. + /// + public int MidiChannel + { + get + { + return ChannelMessage.UnpackMidiChannel(message); + } + set + { + message = ChannelMessage.PackMidiChannel(message, value); + } + } + + /// + /// Gets or sets the first data value to use for building the + /// ChannelMessageEventArgs. + /// + /// + /// Data1 is set to a value less than zero or greater than 127. + /// + public int Data1 + { + get + { + return ShortMessage.UnpackData1(message); + } + set + { + message = ShortMessage.PackData1(message, value); + } + } + + /// + /// Gets or sets the second data value to use for building the + /// ChannelMessageEventArgs. + /// + /// + /// Data2 is set to a value less than zero or greater than 127. + /// + public int Data2 + { + get + { + return ShortMessage.UnpackData2(message); + } + set + { + message = ShortMessage.PackData2(message, value); + } + } + + #endregion + + #endregion + + #region IMessageBuilder Members + + /// + /// Builds a ChannelMessageEventArgs. + /// + public void Build() + { + result = (ChannelMessage)messageCache[message]; + + // If the message does not exist. + if(result == null) + { + result = new ChannelMessage(message); + + // Add message to cache. + messageCache.Add(message, result); + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/IMessageBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/IMessageBuilder.cs new file mode 100644 index 00000000..ae75c49a --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/IMessageBuilder.cs @@ -0,0 +1,51 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +namespace Sanford.Multimedia.Midi +{ + /// + /// Represents functionality for building MIDI messages. + /// + public interface IMessageBuilder + { + #region IMessageBuilder Members + + /// + /// Builds the MIDI message. + /// + void Build(); + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/KeySignatureBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/KeySignatureBuilder.cs new file mode 100644 index 00000000..b1eb6415 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/KeySignatureBuilder.cs @@ -0,0 +1,424 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using Sanford.Multimedia; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Builds key signature MetaMessages. + /// + public class KeySignatureBuilder : IMessageBuilder + { + private Key key = Key.CMajor; + + private MetaMessage result = null; + + /// + /// Initializes a new instance of the KeySignatureBuilder class. + /// + public KeySignatureBuilder() + { + } + + /// + /// Initializes a new instance of the KeySignatureBuilder class with + /// the specified key signature MetaMessage. + /// + /// + /// The key signature MetaMessage to use for initializing the + /// KeySignatureBuilder class. + /// + public KeySignatureBuilder(MetaMessage message) + { + Initialize(message); + } + + /// + /// Initializes the KeySignatureBuilder with the specified MetaMessage. + /// + /// + /// The key signature MetaMessage to use for initializing the + /// KeySignatureBuilder. + /// + public void Initialize(MetaMessage message) + { + #region Require + + if(message == null) + { + throw new ArgumentNullException("message"); + } + else if(message.MetaType != MetaType.KeySignature) + { + throw new ArgumentException("Wrong meta event type.", "messaege"); + } + + #endregion + + sbyte b = (sbyte)message[0]; + + // If the key is major. + if(message[1] == 0) + { + switch(b) + { + case -7: + key = Key.CFlatMajor; + break; + + case -6: + key = Key.GFlatMajor; + break; + + case -5: + key = Key.DFlatMajor; + break; + + case -4: + key = Key.AFlatMajor; + break; + + case -3: + key = Key.EFlatMajor; + break; + + case -2: + key = Key.BFlatMajor; + break; + + case -1: + key = Key.FMajor; + break; + + case 0: + key = Key.CMajor; + break; + + case 1: + key = Key.GMajor; + break; + + case 2: + key = Key.DMajor; + break; + + case 3: + key = Key.AMajor; + break; + + case 4: + key = Key.EMajor; + break; + + case 5: + key = Key.BMajor; + break; + + case 6: + key = Key.FSharpMajor; + break; + + case 7: + key = Key.CSharpMajor; + break; + } + + } + // Else the key is minor. + else + { + switch(b) + { + case -7: + key = Key.AFlatMinor; + break; + + case -6: + key = Key.EFlatMinor; + break; + + case -5: + key = Key.BFlatMinor; + break; + + case -4: + key = Key.FMinor; + break; + + case -3: + key = Key.CMinor; + break; + + case -2: + key = Key.GMinor; + break; + + case -1: + key = Key.DMinor; + break; + + case 0: + key = Key.AMinor; + break; + + case 1: + key = Key.EMinor; + break; + + case 2: + key = Key.BMinor; + break; + + case 3: + key = Key.FSharpMinor; + break; + + case 4: + key = Key.CSharpMinor; + break; + + case 5: + key = Key.GSharpMinor; + break; + + case 6: + key = Key.DSharpMinor; + break; + + case 7: + key = Key.ASharpMinor; + break; + } + } + } + + /// + /// Gets or sets the key. + /// + public Key Key + { + get + { + return key; + } + set + { + key = value; + } + } + + /// + /// The build key signature MetaMessage. + /// + public MetaMessage Result + { + get + { + return result; + } + } + + #region IMessageBuilder Members + + /// + /// Builds the key signature MetaMessage. + /// + public void Build() + { + byte[] data = new byte[MetaMessage.KeySigLength]; + + unchecked + { + switch(Key) + { + case Key.CFlatMajor: + data[0] = (byte)-7; + data[1] = 0; + break; + + case Key.GFlatMajor: + data[0] = (byte)-6; + data[1] = 0; + break; + + case Key.DFlatMajor: + data[0] = (byte)-5; + data[1] = 0; + break; + + case Key.AFlatMajor: + data[0] = (byte)-4; + data[1] = 0; + break; + + case Key.EFlatMajor: + data[0] = (byte)-3; + data[1] = 0; + break; + + case Key.BFlatMajor: + data[0] = (byte)-2; + data[1] = 0; + break; + + case Key.FMajor: + data[0] = (byte)-1; + data[1] = 0; + break; + + case Key.CMajor: + data[0] = 0; + data[1] = 0; + break; + + case Key.GMajor: + data[0] = 1; + data[1] = 0; + break; + + case Key.DMajor: + data[0] = 2; + data[1] = 0; + break; + + case Key.AMajor: + data[0] = 3; + data[1] = 0; + break; + + case Key.EMajor: + data[0] = 4; + data[1] = 0; + break; + + case Key.BMajor: + data[0] = 5; + data[1] = 0; + break; + + case Key.FSharpMajor: + data[0] = 6; + data[1] = 0; + break; + + case Key.CSharpMajor: + data[0] = 7; + data[1] = 0; + break; + + case Key.AFlatMinor: + data[0] = (byte)-7; + data[1] = 1; + break; + + case Key.EFlatMinor: + data[0] = (byte)-6; + data[1] = 1; + break; + + case Key.BFlatMinor: + data[0] = (byte)-5; + data[1] = 1; + break; + + case Key.FMinor: + data[0] = (byte)-4; + data[1] = 1; + break; + + case Key.CMinor: + data[0] = (byte)-3; + data[1] = 1; + break; + + case Key.GMinor: + data[0] = (byte)-2; + data[1] = 1; + break; + + case Key.DMinor: + data[0] = (byte)-1; + data[1] = 1; + break; + + case Key.AMinor: + data[0] = 1; + data[1] = 0; + break; + + case Key.EMinor: + data[0] = 1; + data[1] = 1; + break; + + case Key.BMinor: + data[0] = 2; + data[1] = 1; + break; + + case Key.FSharpMinor: + data[0] = 3; + data[1] = 1; + break; + + case Key.CSharpMinor: + data[0] = 4; + data[1] = 1; + break; + + case Key.GSharpMinor: + data[0] = 5; + data[1] = 1; + break; + + case Key.DSharpMinor: + data[0] = 6; + data[1] = 1; + break; + + case Key.ASharpMinor: + data[0] = 7; + data[1] = 1; + break; + } + } + + result = new MetaMessage(MetaType.KeySignature, data); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/MetaTextBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/MetaTextBuilder.cs new file mode 100644 index 00000000..e9809615 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/MetaTextBuilder.cs @@ -0,0 +1,416 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Provides functionality for building meta text messages. + /// + public class MetaTextBuilder : IMessageBuilder + { + #region MetaTextBuilder Members + + #region Fields + + // The text represented by the MetaMessage. + private string text; + + // The MetaMessage type - must be one of the text based types. + private MetaType type = MetaType.Text; + + // The built MetaMessage. + private MetaMessage result = null; + + // Indicates whether or not the text has changed since the message was + // last built. + private bool changed = true; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the MetaMessageTextBuilder class. + /// + public MetaTextBuilder() + { + text = string.Empty; + } + + /// + /// Initializes a new instance of the MetaMessageTextBuilder class with the + /// specified type. + /// + /// + /// The type of MetaMessage. + /// + /// + /// If the MetaMessage type is not a text based type. + /// + /// + /// The MetaMessage type must be one of the following text based + /// types: + /// + /// + /// Copyright + /// + /// + /// Cuepoint + /// + /// + /// DeviceName + /// + /// + /// InstrumentName + /// + /// + /// Lyric + /// + /// + /// Marker + /// + /// + /// ProgramName + /// + /// + /// Text + /// + /// + /// TrackName + /// + /// + /// If the MetaMessage is not a text based type, an exception + /// will be thrown. + /// + public MetaTextBuilder(MetaType type) + { + #region Require + + if(!IsTextType(type)) + { + throw new ArgumentException("Not text based meta message type.", + "message"); + } + + #endregion + + this.text = string.Empty; + } + + /// + /// Initializes a new instance of the MetaMessageTextBuilder class with the + /// specified type. + /// + /// + /// The type of MetaMessage. + /// + /// + /// If the MetaMessage type is not a text based type. + /// + /// + /// The MetaMessage type must be one of the following text based + /// types: + /// + /// + /// Copyright + /// + /// + /// Cuepoint + /// + /// + /// DeviceName + /// + /// + /// InstrumentName + /// + /// + /// Lyric + /// + /// + /// Marker + /// + /// + /// ProgramName + /// + /// + /// Text + /// + /// + /// TrackName + /// + /// + /// If the MetaMessage is not a text based type, an exception + /// will be thrown. + /// + public MetaTextBuilder(MetaType type, string text) + { + #region Require + + if(!IsTextType(type)) + { + throw new ArgumentException("Not text based meta message type.", + "message"); + } + + #endregion + + this.type = type; + + if(text != null) + { + this.text = text; + } + else + { + this.text = string.Empty; + } + } + + + /// + /// Initializes a new instance of the MetaMessageTextBuilder class with the + /// specified MetaMessage. + /// + /// + /// The MetaMessage to use for initializing the MetaMessageTextBuilder. + /// + /// + /// If the MetaMessage is not a text based type. + /// + /// + /// The MetaMessage must be one of the following text based types: + /// + /// + /// Copyright + /// + /// + /// Cuepoint + /// + /// + /// DeviceName + /// + /// + /// InstrumentName + /// + /// + /// Lyric + /// + /// + /// Marker + /// + /// + /// ProgramName + /// + /// + /// Text + /// + /// + /// TrackName + /// + /// + /// If the MetaMessage is not a text based type, an exception will be + /// thrown. + /// + public MetaTextBuilder(MetaMessage message) + { + Initialize(message); + } + + #endregion + + #region Methods + + /// + /// Initializes the MetaMessageTextBuilder with the specified MetaMessage. + /// + /// + /// The MetaMessage to use for initializing the MetaMessageTextBuilder. + /// + /// + /// If the MetaMessage is not a text based type. + /// + public void Initialize(MetaMessage message) + { + #region Require + + if(!IsTextType(message.MetaType)) + { + throw new ArgumentException("Not text based meta message.", + "message"); + } + + #endregion + + ASCIIEncoding encoding = new ASCIIEncoding(); + + text = encoding.GetString(message.GetBytes()); + this.type = message.MetaType; + } + + /// + /// Indicates whether or not the specified MetaType is a text based + /// type. + /// + /// + /// The MetaType to test. + /// + /// + /// true if the MetaType is a text based type; + /// otherwise, false. + /// + private bool IsTextType(MetaType type) + { + bool result; + + if(type == MetaType.Copyright || + type == MetaType.CuePoint || + type == MetaType.DeviceName || + type == MetaType.InstrumentName || + type == MetaType.Lyric || + type == MetaType.Marker || + type == MetaType.ProgramName || + type == MetaType.Text || + type == MetaType.TrackName) + { + result = true; + } + else + { + result = false; + } + + return result; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the text for the MetaMessage. + /// + public string Text + { + get + { + return text; + } + set + { + if(value != null) + { + text = value; + } + else + { + text = string.Empty; + } + + changed = true; + } + } + + /// + /// Gets or sets the MetaMessage type. + /// + /// + /// If the type is not a text based type. + /// + public MetaType Type + { + get + { + return type; + } + set + { + #region Require + + if(!IsTextType(value)) + { + throw new ArgumentException("Not text based meta message type.", + "message"); + } + + #endregion + + type = value; + + changed = true; + } + } + + /// + /// Gets the built MetaMessage. + /// + public MetaMessage Result + { + get + { + return result; + } + } + + #endregion + + #endregion + + #region IMessageBuilder Members + + /// + /// Builds the text MetaMessage. + /// + public void Build() + { + // If the text has changed since the last time this method was + // called. + if(changed) + { + // + // Build text MetaMessage. + // + + ASCIIEncoding encoding = new ASCIIEncoding(); + byte[] data = encoding.GetBytes(text); + result = new MetaMessage(Type, data); + changed = false; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/SongPositionPointerBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/SongPositionPointerBuilder.cs new file mode 100644 index 00000000..d0d5b076 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/SongPositionPointerBuilder.cs @@ -0,0 +1,266 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Provides functionality for building song position pointer messages. + /// + public class SongPositionPointerBuilder : IMessageBuilder + { + #region SongPositionPointerBuilder Members + + #region Constants + + // The number of ticks per 16th note. + private const int TicksPer16thNote = 6; + + // Used for packing and unpacking the song position. + private const int Shift = 7; + + // Used for packing and unpacking the song position. + private const int Mask = 127; + + #endregion + + #region Fields + + // The scale used for converting from the song position to the position + // in ticks. + private int tickScale; + + // Pulses per quarter note resolution. + private int ppqn; + + // Used for building the SysCommonMessage to represent the song + // position pointer. + private SysCommonMessageBuilder builder; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the SongPositionPointerBuilder class. + /// + public SongPositionPointerBuilder() + { + builder = new SysCommonMessageBuilder(); + builder.Type = SysCommonType.SongPositionPointer; + + Ppqn = PpqnClock.PpqnMinValue; + } + + /// + /// Initializes a new instance of the SongPositionPointerBuilder class + /// with the specified song position pointer message. + /// + /// + /// The song position pointer message to use for initializing the + /// SongPositionPointerBuilder. + /// + /// + /// If message is not a song position pointer message. + /// + public SongPositionPointerBuilder(SysCommonMessage message) + { + builder = new SysCommonMessageBuilder(); + builder.Type = SysCommonType.SongPositionPointer; + + Initialize(message); + + Ppqn = PpqnClock.PpqnMinValue; + } + + #endregion + + #region Methods + + /// + /// Initializes the SongPositionPointerBuilder with the specified + /// SysCommonMessage. + /// + /// + /// The SysCommonMessage to use to initialize the + /// SongPositionPointerBuilder. + /// + /// + /// If the SysCommonMessage is not a song position pointer message. + /// + public void Initialize(SysCommonMessage message) + { + #region Require + + if(message == null) + { + throw new ArgumentNullException("message"); + } + else if(message.SysCommonType != SysCommonType.SongPositionPointer) + { + throw new ArgumentException( + "Message is not a song position pointer message."); + } + + #endregion + + builder.Initialize(message); + } + + #endregion + + #region Properties + + /// + /// Gets or sets the sequence position in ticks. + /// + /// + /// Value is set to less than zero. + /// + /// + /// Note: the position in ticks value is converted to the song position + /// pointer value. Since the song position pointer has a lower + /// resolution than the position in ticks, there is a probable loss of + /// resolution when setting the position in ticks value. + /// + public int PositionInTicks + { + get + { + return SongPosition * tickScale * TicksPer16thNote; + } + set + { + #region Require + + if(value < 0) + { + throw new ArgumentOutOfRangeException("PositionInTicks", value, + "Position in ticks out of range."); + } + + #endregion + + SongPosition = value / (tickScale * TicksPer16thNote); + } + } + + /// + /// Gets or sets the PulsesPerQuarterNote object. + /// + /// + /// Value is not a multiple of 24. + /// + public int Ppqn + { + get + { + return ppqn; + } + set + { + #region Require + + if(value < PpqnClock.PpqnMinValue) + { + throw new ArgumentOutOfRangeException("Ppqn", value, + "Pulses per quarter note is smaller than 24."); + } + + #endregion + + ppqn = value; + + tickScale = ppqn / PpqnClock.PpqnMinValue; + } + } + + /// + /// Gets or sets the song position. + /// + /// + /// Value is set to less than zero. + /// + public int SongPosition + { + get + { + return (builder.Data2 << Shift) | builder.Data1; + } + set + { + #region Require + + if(value < 0) + { + throw new ArgumentOutOfRangeException("SongPosition", value, + "Song position pointer out of range."); + } + + #endregion + + builder.Data1 = value & Mask; + builder.Data2 = value >> Shift; + } + } + + /// + /// Gets the built song position pointer message. + /// + public SysCommonMessage Result + { + get + { + return builder.Result; + } + } + + #endregion + + #endregion + + #region IMessageBuilder Members + + /// + /// Builds a song position pointer message. + /// + public void Build() + { + builder.Build(); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/SysCommonMessageBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/SysCommonMessageBuilder.cs new file mode 100644 index 00000000..d79a1b9a --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/SysCommonMessageBuilder.cs @@ -0,0 +1,233 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Provides functionality for building SysCommonMessages. + /// + public class SysCommonMessageBuilder : IMessageBuilder + { + #region SysCommonMessageBuilder Members + + #region Class Fields + + // Stores the SystemCommonMessages. + private static Hashtable messageCache = Hashtable.Synchronized(new Hashtable()); + + #endregion + + #region Fields + + // The SystemCommonMessage as a packed integer. + private int message = 0; + + // The built SystemCommonMessage. + private SysCommonMessage result = null; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the SysCommonMessageBuilder class. + /// + public SysCommonMessageBuilder() + { + Type = SysCommonType.TuneRequest; + } + + /// + /// Initializes a new instance of the SysCommonMessageBuilder class + /// with the specified SystemCommonMessage. + /// + /// + /// The SysCommonMessage to use for initializing the + /// SysCommonMessageBuilder. + /// + /// + /// The SysCommonMessageBuilder uses the specified SysCommonMessage to + /// initialize its property values. + /// + public SysCommonMessageBuilder(SysCommonMessage message) + { + Initialize(message); + } + + #endregion + + #region Methods + + /// + /// Initializes the SysCommonMessageBuilder with the specified + /// SysCommonMessage. + /// + /// + /// The SysCommonMessage to use for initializing the + /// SysCommonMessageBuilder. + /// + public void Initialize(SysCommonMessage message) + { + this.message = message.Message; + } + + /// + /// Clears the SysCommonMessageBuilder cache. + /// + public static void Clear() + { + messageCache.Clear(); + } + + #endregion + + #region Properties + + /// + /// Gets the number of messages in the SysCommonMessageBuilder cache. + /// + public static int Count + { + get + { + return messageCache.Count; + } + } + + /// + /// Gets the built SysCommonMessage. + /// + public SysCommonMessage Result + { + get + { + return result; + } + } + + /// + /// Gets or sets the SysCommonMessage as a packed integer. + /// + internal int Message + { + get + { + return message; + } + set + { + message = value; + } + } + + /// + /// Gets or sets the type of SysCommonMessage. + /// + public SysCommonType Type + { + get + { + return (SysCommonType)ShortMessage.UnpackStatus(message); + } + set + { + message = ShortMessage.PackStatus(message, (int)value); + } + } + + /// + /// Gets or sets the first data value to use for building the + /// SysCommonMessage. + /// + /// + /// Data1 is set to a value less than zero or greater than 127. + /// + public int Data1 + { + get + { + return ShortMessage.UnpackData1(message); + } + set + { + message = ShortMessage.PackData1(message, value); + } + } + + /// + /// Gets or sets the second data value to use for building the + /// SysCommonMessage. + /// + /// + /// Data2 is set to a value less than zero or greater than 127. + /// + public int Data2 + { + get + { + return ShortMessage.UnpackData2(message); + } + set + { + message = ShortMessage.PackData2(message, value); + } + } + + #endregion + + #endregion + + #region IMessageBuilder Members + + /// + /// Builds a SysCommonMessage. + /// + public void Build() + { + result = (SysCommonMessage)messageCache[message]; + + if(result == null) + { + result = new SysCommonMessage(message); + + messageCache.Add(message, result); + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TempoChangeBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TempoChangeBuilder.cs new file mode 100644 index 00000000..0c0cbc61 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TempoChangeBuilder.cs @@ -0,0 +1,242 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Provides functionality for building tempo messages. + /// + public class TempoChangeBuilder : IMessageBuilder + { + #region TempoChangeBuilder Members + + #region Constants + + // Value used for shifting bits for packing and unpacking tempo values. + private const int Shift = 8; + + #endregion + + #region Fields + + // The mesage's tempo. + private int tempo = PpqnClock.DefaultTempo; + + // The built MetaMessage. + private MetaMessage result = null; + + // Indicates whether the tempo property has been changed since + // the last time the message was built. + private bool changed = true; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the TempoChangeBuilder class. + /// + public TempoChangeBuilder() + { + } + + /// + /// Initialize a new instance of the TempoChangeBuilder class with the + /// specified MetaMessage. + /// + /// + /// The MetaMessage to use for initializing the TempoChangeBuilder class. + /// + /// + /// If the specified MetaMessage is not a tempo type. + /// + /// + /// The TempoChangeBuilder uses the specified MetaMessage to initialize + /// its property values. + /// + public TempoChangeBuilder(MetaMessage e) + { + Initialize(e); + } + + #endregion + + #region Methods + + /// + /// Initializes the TempoChangeBuilder with the specified MetaMessage. + /// + /// + /// The MetaMessage to use for initializing the TempoChangeBuilder. + /// + /// + /// If the specified MetaMessage is not a tempo type. + /// + public void Initialize(MetaMessage e) + { + #region Require + + if(e == null) + { + throw new ArgumentNullException("e"); + } + else if(e.MetaType != MetaType.Tempo) + { + throw new ArgumentException("Wrong meta message type.", "e"); + } + + #endregion + + int t = 0; + + // If this platform uses little endian byte order. + if(BitConverter.IsLittleEndian) + { + int d = e.Length - 1; + + // Pack tempo. + for(int i = 0; i < e.Length; i++) + { + t |= e[d] << (Shift * i); + d--; + } + } + // Else this platform uses big endian byte order. + else + { + // Pack tempo. + for(int i = 0; i < e.Length; i++) + { + t |= e[i] << (Shift * i); + } + } + + tempo = t; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the tempo. + /// + /// + /// Value is set to less than zero. + /// + public int Tempo + { + get + { + return tempo; + } + set + { + #region Require + + if(value < 0) + { + throw new ArgumentOutOfRangeException("Tempo", value, + "Tempo is out of range."); + } + + #endregion + + tempo = value; + + changed = true; + } + } + + /// + /// Gets the built message. + /// + public MetaMessage Result + { + get + { + return result; + } + } + + #endregion + + #endregion + + #region IMessageBuilder Members + + /// + /// Builds the tempo change MetaMessage. + /// + public void Build() + { + // If the tempo has been changed since the last time the message + // was built. + if(changed) + { + byte[] data = new byte[MetaMessage.TempoLength]; + + // If this platform uses little endian byte order. + if(BitConverter.IsLittleEndian) + { + int d = data.Length - 1; + + // Unpack tempo. + for(int i = 0; i < data.Length; i++) + { + data[d] = (byte)(tempo >> (Shift * i)); + d--; + } + } + // Else this platform uses big endian byte order. + else + { + // Unpack tempo. + for(int i = 0; i < data.Length; i++) + { + data[i] = (byte)(tempo >> (Shift * i)); + } + } + + changed = false; + + result = new MetaMessage(MetaType.Tempo, data); + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TimeSignatureBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TimeSignatureBuilder.cs new file mode 100644 index 00000000..90b17ec8 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TimeSignatureBuilder.cs @@ -0,0 +1,277 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Provides easy to use functionality for time signature MetaMessages. + /// + public class TimeSignatureBuilder : IMessageBuilder + { + #region TimeSignature Members + + #region Constants + + // Default numerator value. + private const byte DefaultNumerator = 4; + + // Default denominator value. + private const byte DefaultDenominator = 2; + + // Default clocks per metronome click value. + private const byte DefaultClocksPerMetronomeClick = 24; + + // Default thirty second notes per quarter note value. + private const byte DefaultThirtySecondNotesPerQuarterNote = 8; + + #endregion + + #region Fields + + // The raw data making up the time signature meta message. + private byte[] data = new byte[MetaMessage.TimeSigLength]; + + // The built time signature meta message. + private MetaMessage result = null; + + // Indicates whether any of the properties have changed since the + // last time the message was built. + private bool changed = true; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the TimeSignatureBuilder class. + /// + public TimeSignatureBuilder() + { + Numerator = DefaultNumerator; + Denominator = DefaultDenominator; + ClocksPerMetronomeClick = DefaultClocksPerMetronomeClick; + ThirtySecondNotesPerQuarterNote = DefaultThirtySecondNotesPerQuarterNote; + } + + /// + /// Initializes a new instance of the TimeSignatureBuilder class with the + /// specified MetaMessage. + /// + /// + /// The MetaMessage to use for initializing the TimeSignatureBuilder class. + /// + /// + /// If the specified MetaMessage is not a time signature type. + /// + /// + /// The TimeSignatureBuilder uses the specified MetaMessage to + /// initialize its property values. + /// + public TimeSignatureBuilder(MetaMessage message) + { + Initialize(message); + } + + #endregion + + #region Methods + + /// + /// Initializes the TimeSignatureBuilder with the specified MetaMessage. + /// + /// + /// The MetaMessage to use for initializing the TimeSignatureBuilder. + /// + /// + /// If the specified MetaMessage is not a time signature type. + /// + public void Initialize(MetaMessage message) + { + #region Require + + if(message.MetaType != MetaType.TimeSignature) + { + throw new ArgumentException("Wrong meta event type.", "message"); + } + + #endregion + + data = message.GetBytes(); + } + + #endregion + + #region Properties + + /// + /// Gets or sets the numerator. + /// + /// + /// Numerator is set to a value less than one. + /// + public byte Numerator + { + get + { + return data[0]; + } + set + { + #region Require + + if(value < 1) + { + throw new ArgumentOutOfRangeException("Numerator", value, + "Numerator out of range."); + } + + #endregion + + data[0] = value; + + changed = true; + } + } + + /// + /// Gets or sets the denominator. + /// + /// + /// Denominator is set to a value less than 2. + /// + /// + /// Denominator is set to a value that is not a power of 2. + /// + public byte Denominator + { + get + { + return Convert.ToByte(Math.Pow(2, data[1])); + } + set + { + #region Require + + if(value < 2 || value > 32) + { + throw new ArgumentOutOfRangeException("Denominator must be between 2 and 32."); + } + else if((value & (value - 1)) != 0) + { + throw new ArgumentException("Denominator must be a power of 2."); + } + + #endregion + + data[1] = Convert.ToByte(Math.Log(value, 2)); + + changed = true; + } + } + + /// + /// Gets or sets the clocks per metronome click. + /// + /// + /// Clocks per metronome click determines how many MIDI clocks occur + /// for each metronome click. + /// + public byte ClocksPerMetronomeClick + { + get + { + return data[2]; + } + set + { + data[2] = value; + + changed = true; + } + } + + /// + /// Gets or sets how many thirty second notes there are for each + /// quarter note. + /// + public byte ThirtySecondNotesPerQuarterNote + { + get + { + return data[3]; + } + set + { + data[3] = value; + + changed = true; + } + } + + /// + /// Gets the built message. + /// + public MetaMessage Result + { + get + { + return result; + } + } + + #endregion + + #endregion + + #region IMessageBuilder Members + + /// + /// Builds the time signature MetaMessage. + /// + public void Build() + { + // If any of the properties have changed since the last time the + // message was built. + if(changed) + { + result = new MetaMessage(MetaType.TimeSignature, data); + changed = false; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessageDispatcher.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessageDispatcher.cs new file mode 100644 index 00000000..92285c66 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessageDispatcher.cs @@ -0,0 +1,186 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Dispatches IMidiMessages to their corresponding sink. + /// + public class MessageDispatcher + { + #region MessageDispatcher Members + + #region Events + + public event EventHandler ChannelMessageDispatched; + + public event EventHandler SysExMessageDispatched; + + public event EventHandler SysCommonMessageDispatched; + + public event EventHandler SysRealtimeMessageDispatched; + + public event EventHandler MetaMessageDispatched; + + #endregion + + /// + /// Dispatches IMidiMessages to their corresponding sink. + /// + /// + /// The IMidiMessage to dispatch. + /// + public void Dispatch(MidiEvent evt, Track track) + { + #region Require + + var message = evt.MidiMessage; + + if(message == null) + { + throw new ArgumentNullException("message"); + } + + #endregion + + switch(message.MessageType) + { + case MessageType.Channel: + OnChannelMessageDispatched(new ChannelMessageEventArgs((ChannelMessage)message, evt.AbsoluteTicks), track); + break; + + case MessageType.SystemExclusive: + OnSysExMessageDispatched(new SysExMessageEventArgs((SysExMessage)message, evt.AbsoluteTicks), track); + break; + + case MessageType.Meta: + OnMetaMessageDispatched(new MetaMessageEventArgs((MetaMessage)message, evt.AbsoluteTicks), track); + break; + + case MessageType.SystemCommon: + OnSysCommonMessageDispatched(new SysCommonMessageEventArgs((SysCommonMessage)message, evt.AbsoluteTicks), track); + break; + + case MessageType.SystemRealtime: + switch(((SysRealtimeMessage)message).SysRealtimeType) + { + case SysRealtimeType.ActiveSense: + OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArgs.ActiveSense, track); + break; + + case SysRealtimeType.Clock: + OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArgs.Clock, track); + break; + + case SysRealtimeType.Continue: + OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArgs.Continue, track); + break; + + case SysRealtimeType.Reset: + OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArgs.Reset, track); + break; + + case SysRealtimeType.Start: + OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArgs.Start, track); + break; + + case SysRealtimeType.Stop: + OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArgs.Stop, track); + break; + + case SysRealtimeType.Tick: + OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArgs.Tick, track); + break; + } + + break; + } + } + + protected virtual void OnChannelMessageDispatched(ChannelMessageEventArgs e, Track track) + { + EventHandler handler = ChannelMessageDispatched; + + if(handler != null) + { + handler(track, e); + } + } + + protected virtual void OnSysExMessageDispatched(SysExMessageEventArgs e, Track track) + { + EventHandler handler = SysExMessageDispatched; + + if(handler != null) + { + handler(track, e); + } + } + + protected virtual void OnSysCommonMessageDispatched(SysCommonMessageEventArgs e, Track track) + { + EventHandler handler = SysCommonMessageDispatched; + + if(handler != null) + { + handler(track, e); + } + } + + protected virtual void OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArgs e, Track track) + { + EventHandler handler = SysRealtimeMessageDispatched; + + if(handler != null) + { + handler(track, e); + } + } + + protected virtual void OnMetaMessageDispatched(MetaMessageEventArgs e, Track track) + { + EventHandler handler = MetaMessageDispatched; + + if(handler != null) + { + handler(track, e); + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessagesHierarchy.cd b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessagesHierarchy.cd new file mode 100644 index 00000000..b9134fea --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessagesHierarchy.cd @@ -0,0 +1,120 @@ + + + + + + QIAFAAAAAAAEAAAAiAAABAACAAIAAIAAAAAAAAgAASA= + Sanford.Multimedia.Midi\Messages\ChannelMessage.cs + + + + + + AAiAEgAkAAAEQAgAiEAAIABAAAABAKAAAAAQAAAAAAA= + Sanford.Multimedia.Midi\Messages\MetaMessage.cs + + + + + + + AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA= + Sanford.Multimedia.Midi\Messages\ShortMessage.cs + + + + + + CICAIAAMAIQEEAggAAACCCAAAAAAAAAIAAQKIAAAACQ= + Sanford.Multimedia.Midi\Messages\ShortMessage.cs + + + + + + + AAAEAAAAAAAEAAAAgAAAAAACAAAAAIAAAAAAAAAAAAA= + Sanford.Multimedia.Midi\Messages\SysCommonMessage.cs + + + + + + + + + + + + + + AACAAAAEBAAEEBAAiAACAABAAAAAAIAAAAAQAAAAIAA= + Sanford.Multimedia.Midi\Messages\SysExMessage.cs + + + + + + + AAAAAACAAAAEEAAggAAAAAAAAAAABKAAAAAAMAAAAIA= + Sanford.Multimedia.Midi\Messages\SysRealtimeMessage.cs + + + + + + AACAAAAEAAAEAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA= + Sanford.Multimedia.Midi\Messages\IMidiMessage.cs + + + + + + AAAEAAAgAAAAEACAAAIAAAAAAAAAAIAAAAAAAAAAAAA= + Sanford.Multimedia.Midi\Messages\IMidiMessage.cs + + + + + + AACAIAAAAQAAAAQAgAAAAAAAAAAAAAAAAgAAIAAAAAA= + Sanford.Multimedia.Midi\Messages\ChannelMessage.cs + + + + + + MDokEAAABOiiowAIBJgAAFKCjIJQBAi2AEaAATASRAA= + Sanford.Multimedia.Midi\Messages\ChannelMessage.cs + + + + + + AAAAEAIBAAAAAAAAAAgAAQCAEAAABAAAIEEICACAAAA= + Sanford.Multimedia.Midi\Messages\MetaMessage.cs + + + + + + AAQACAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAEAAAA= + Sanford.Multimedia.Midi\Messages\SysCommonMessage.cs + + + + + + AAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAA= + Sanford.Multimedia.Midi\Messages\SysExMessage.cs + + + + + + AAAACAAAACAABAAAAAAAAAAAAAAIAAAAIAAAAAAAQgA= + Sanford.Multimedia.Midi\Messages\SysRealtimeMessage.cs + + + + \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MetaMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MetaMessage.cs new file mode 100644 index 00000000..ccb6f2dc --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MetaMessage.cs @@ -0,0 +1,513 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Multimedia.Midi +{ + #region Meta Message Types + + /// + /// Represents MetaMessage types. + /// + public enum MetaType + { + /// + /// Represents sequencer number type. + /// + SequenceNumber, + + /// + /// Represents the text type. + /// + Text, + + /// + /// Represents the copyright type. + /// + Copyright, + + /// + /// Represents the track name type. + /// + TrackName, + + /// + /// Represents the instrument name type. + /// + InstrumentName, + + /// + /// Represents the lyric type. + /// + Lyric, + + /// + /// Represents the marker type. + /// + Marker, + + /// + /// Represents the cue point type. + /// + CuePoint, + + /// + /// Represents the program name type. + /// + ProgramName, + + /// + /// Represents the device name type. + /// + DeviceName, + + /// + /// Represents then end of track type. + /// + EndOfTrack = 0x2F, + + /// + /// Represents the tempo type. + /// + Tempo = 0x51, + + /// + /// Represents the Smpte offset type. + /// + SmpteOffset = 0x54, + + /// + /// Represents the time signature type. + /// + TimeSignature = 0x58, + + /// + /// Represents the key signature type. + /// + KeySignature, + + /// + /// Represents the proprietary event type. + /// + ProprietaryEvent = 0x7F + } + + #endregion + + /// + /// Represents MIDI meta messages. + /// + /// + /// Meta messages are MIDI messages that are stored in MIDI files. These + /// messages are not sent or received via MIDI but are read and + /// interpretted from MIDI files. They provide information that describes + /// a MIDI file's properties. For example, tempo changes are implemented + /// using meta messages. + /// + [ImmutableObject(true)] + public sealed class MetaMessage : MidiMessageBase, IMidiMessage + { + #region MetaMessage Members + + #region Constants + + /// + /// The amount to shift data bytes when calculating the hash code. + /// + private const int Shift = 7; + + // + // Meta message length constants. + // + + /// + /// Length in bytes for tempo meta message data. + /// + public const int TempoLength = 3; + + /// + /// Length in bytes for SMPTE offset meta message data. + /// + public const int SmpteOffsetLength = 5; + + /// + /// Length in bytes for time signature meta message data. + /// + public const int TimeSigLength = 4; + + /// + /// Length in bytes for key signature meta message data. + /// + public const int KeySigLength = 2; + + #endregion + + #region Class Fields + + /// + /// End of track meta message. + /// + public static readonly MetaMessage EndOfTrackMessage = + new MetaMessage(MetaType.EndOfTrack, new byte[0]); + + #endregion + + #region Fields + + // The meta message type. + private MetaType type; + + // The meta message data. + private byte[] data; + + // The hash code value. + private int hashCode; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the MetaMessage class. + /// + /// + /// The type of MetaMessage. + /// + /// + /// The MetaMessage data. + /// + /// + /// The length of the MetaMessage is not valid for the MetaMessage type. + /// + /// + /// Each MetaMessage has type and length properties. For certain + /// types, the length of the message data must be a specific value. For + /// example, tempo messages must have a data length of exactly three. + /// Some MetaMessage types can have any data length. Text messages are + /// an example of a MetaMessage that can have a variable data length. + /// When a MetaMessage is created, the length of the data is checked + /// to make sure that it is valid for the specified type. If it is not, + /// an exception is thrown. + /// + public MetaMessage(MetaType type, byte[] data) + { + #region Require + + if(data == null) + { + throw new ArgumentNullException("data"); + } + else if(!ValidateDataLength(type, data.Length)) + { + throw new ArgumentException( + "Length of data not valid for meta message type."); + } + + #endregion + + this.type = type; + + // Create storage for meta message data. + this.data = new byte[data.Length]; + + // Copy data into storage. + data.CopyTo(this.data, 0); + + CalculateHashCode(); + } + + #endregion + + #region Methods + + /// + /// Gets a copy of the data bytes for this meta message. + /// + /// + /// A copy of the data bytes for this meta message. + /// + public byte[] GetBytes() + { + return (byte[])data.Clone(); + } + + /// + /// Returns a value for the current MetaMessage suitable for use in + /// hashing algorithms. + /// + /// + /// A hash code for the current MetaMessage. + /// + public override int GetHashCode() + { + return hashCode; + } + + /// + /// Determines whether two MetaMessage instances are equal. + /// + /// + /// The MetaMessage to compare with the current MetaMessage. + /// + /// + /// true if the specified MetaMessage is equal to the current + /// MetaMessage; otherwise, false. + /// + public override bool Equals(object obj) + { + #region Guard + + if(!(obj is MetaMessage)) + { + return false; + } + + #endregion + + bool equal = true; + MetaMessage message = (MetaMessage)obj; + + // If the types do not match. + if(MetaType != message.MetaType) + { + // The messages are not equal + equal = false; + } + + // If the message lengths are not equal. + if(equal && Length != message.Length) + { + // The message are not equal. + equal = false; + } + + // Check to see if the data is equal. + for(int i = 0; i < Length && equal; i++) + { + // If a data value does not match. + if(this[i] != message[i]) + { + // The messages are not equal. + equal = false; + } + } + + return equal; + } + + // Calculates the hash code. + private void CalculateHashCode() + { + // TODO: This algorithm may need work. + + hashCode = (int)MetaType; + + for(int i = 0; i < data.Length; i += 3) + { + hashCode ^= data[i]; + } + + for(int i = 1; i < data.Length; i += 3) + { + hashCode ^= data[i] << Shift; + } + + for(int i = 2; i < data.Length; i += 3) + { + hashCode ^= data[i] << Shift * 2; + } + } + + /// + /// Validates data length. + /// + /// + /// The MetaMessage type. + /// + /// + /// The length of the MetaMessage data. + /// + /// + /// true if the data length is valid for this type of + /// MetaMessage; otherwise, false. + /// + private bool ValidateDataLength(MetaType type, int length) + { + #region Require + + Debug.Assert(length >= 0); + + #endregion + + bool result = true; + + // Determine which type of meta message this is and check to make + // sure that the data length value is valid. + switch(type) + { + case MetaType.SequenceNumber: + if(length != 0 || length != 2) + { + result = false; + } + break; + + case MetaType.EndOfTrack: + if(length != 0) + { + result = false; + } + break; + + case MetaType.Tempo: + if(length != TempoLength) + { + result = false; + } + break; + + case MetaType.SmpteOffset: + if(length != SmpteOffsetLength) + { + result = false; + } + break; + + case MetaType.TimeSignature: + if(length != TimeSigLength) + { + result = false; + } + break; + + case MetaType.KeySignature: + if(length != KeySigLength) + { + result = false; + } + break; + + default: + result = true; + break; + } + + return result; + } + + #endregion + + #region Properties + + /// + /// Gets the element at the specified index. + /// + /// + /// index is less than zero or greater than or equal to Length. + /// + public byte this[int index] + { + get + { + #region Require + + if(index < 0 || index >= Length) + { + throw new ArgumentOutOfRangeException("index", index, + "Index into MetaMessage out of range."); + } + + #endregion + + return data[index]; + } + } + + /// + /// Gets the length of the meta message. + /// + public int Length + { + get + { + return data.Length; + } + } + + /// + /// Gets the type of meta message. + /// + public MetaType MetaType + { + get + { + return type; + } + } + + #endregion + + #endregion + + #region IMidiMessage Members + + /// + /// Gets the status value. + /// + public int Status + { + get + { + // All meta messages have the same status value (0xFF). + return 0xFF; + } + } + + /// + /// Gets the MetaMessage's MessageType. + /// + public MessageType MessageType + { + get + { + return MessageType.Meta; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/InputDeviceMidiEvents.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/InputDeviceMidiEvents.cs new file mode 100644 index 00000000..a87a3abb --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/InputDeviceMidiEvents.cs @@ -0,0 +1,127 @@ + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// MidiSignal provides all midi events from an input device + /// + public class InputDeviceMidiEvents : MidiEvents + { + readonly InputDevice FInDevice; + + public int DeviceID { + get { + if (FInDevice != null) { + return FInDevice.DeviceID; + } + else { + return -1; + } + } + } + + /// + /// Create Midisignal with an input device which fires the events + /// + /// + public InputDeviceMidiEvents(InputDevice inDevice) + { + FInDevice = inDevice; + FInDevice.StartRecording(); + } + + public void Dispose() + { + FInDevice.Dispose(); + } + + public static InputDeviceMidiEvents FromDeviceID(int deviceID) + { + var deviceCount = InputDevice.DeviceCount; + if (deviceCount > 0) + { + deviceID %= deviceCount; + return new InputDeviceMidiEvents(new InputDevice(deviceID)); + } + return null; + } + + /// + /// All incoming midi messages in short format + /// + public event MidiMessageEventHandler MessageReceived + { + add + { + FInDevice.MessageReceived += value; + } + remove + { + FInDevice.MessageReceived -= value; + } + } + + /// + /// All incoming midi messages in short format + /// + public event EventHandler ShortMessageReceived { + add { + FInDevice.ShortMessageReceived += value; + } + remove { + FInDevice.ShortMessageReceived -= value; + } + } + + /// + /// Channel messages like, note, controller, program, ... + /// + public event EventHandler ChannelMessageReceived { + add { + FInDevice.ChannelMessageReceived += value; + } + remove { + FInDevice.ChannelMessageReceived -= value; + } + } + + /// + /// SysEx messages + /// + public event EventHandler SysExMessageReceived { + add { + FInDevice.SysExMessageReceived += value; + } + remove { + FInDevice.SysExMessageReceived -= value; + } + } + + /// + /// Midi timecode, song position, song select, tune request + /// + public event EventHandler SysCommonMessageReceived { + add { + FInDevice.SysCommonMessageReceived += value; + } + remove { + FInDevice.SysCommonMessageReceived -= value; + } + } + + /// + /// Timing events, midi clock, start, stop, reset, active sense, tick + /// + public event EventHandler SysRealtimeMessageReceived { + add { + FInDevice.SysRealtimeMessageReceived += value; + } + remove { + FInDevice.SysRealtimeMessageReceived -= value; + } + } + } +} + + diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MergeMidiEvents.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MergeMidiEvents.cs new file mode 100644 index 00000000..d9532853 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MergeMidiEvents.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Takes a number of MidiEvents and merges them into a new single MidiEvent source + /// + public class MergeMidiEvents : MidiEvents + { + public int DeviceID + { + get + { + return -3; + } + } + + readonly List FMidiEventsList = new List(); + + public MergeMidiEvents(IEnumerable midiEvents) + { + foreach (var elem in midiEvents) + { + if (elem != null) + FMidiEventsList.Add(elem); + } + } + + public IEnumerable EventSources + { + get + { + return FMidiEventsList; + } + } + + public void Dispose() + { + } + + public event MidiMessageEventHandler MessageReceived + { + add + { + foreach (var elem in FMidiEventsList) + { + elem.MessageReceived += value; + } + } + remove + { + foreach (var elem in FMidiEventsList) + { + elem.MessageReceived -= value; + } + } + } + + public event EventHandler ShortMessageReceived + { + add + { + foreach (var elem in FMidiEventsList) + { + elem.ShortMessageReceived += value; + } + } + remove + { + foreach (var elem in FMidiEventsList) + { + elem.ShortMessageReceived -= value; + } + } + } + + public event EventHandler ChannelMessageReceived + { + add + { + foreach (var elem in FMidiEventsList) + { + elem.ChannelMessageReceived += value; + } + } + remove + { + foreach (var elem in FMidiEventsList) + { + elem.ChannelMessageReceived -= value; + } + } + } + + public event EventHandler SysExMessageReceived + { + add + { + foreach (var elem in FMidiEventsList) + { + elem.SysExMessageReceived += value; + } + } + remove + { + foreach (var elem in FMidiEventsList) + { + elem.SysExMessageReceived -= value; + } + } + } + + public event EventHandler SysCommonMessageReceived + { + add + { + foreach (var elem in FMidiEventsList) + { + elem.SysCommonMessageReceived += value; + } + } + remove + { + foreach (var elem in FMidiEventsList) + { + elem.SysCommonMessageReceived -= value; + } + } + } + + public event EventHandler SysRealtimeMessageReceived + { + add + { + foreach (var elem in FMidiEventsList) + { + elem.SysRealtimeMessageReceived += value; + } + } + remove + { + foreach (var elem in FMidiEventsList) + { + elem.SysRealtimeMessageReceived -= value; + } + } + } + + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MidiEvents.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MidiEvents.cs new file mode 100644 index 00000000..000433fb --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MidiEvents.cs @@ -0,0 +1,53 @@ + +using System; + +namespace Sanford.Multimedia.Midi +{ + + /// + /// An event source that combines all possible midi events + /// + public interface MidiEvents : IDisposable + { + /// + /// Gets the device identifier of the input devive. + /// Set it to any negative value for custom event sources. + /// + int DeviceID { get; } + + /// + /// Occurs when any message was received. The underlying type of the message should be as specific as possible. + /// Channel, Common, Realtime or SysEx. + /// + event MidiMessageEventHandler MessageReceived; + + /// + /// All incoming midi short messages + /// + event EventHandler ShortMessageReceived; + + /// + /// Channel messages like, note, controller, program, ... + /// + event EventHandler ChannelMessageReceived; + + /// + /// SysEx messages + /// + event EventHandler SysExMessageReceived; + + /// + /// Midi timecode, song position, song select, tune request + /// + event EventHandler SysCommonMessageReceived; + + /// + /// Timing events, midi clock, start, stop, reset, active sense, tick + /// + event EventHandler SysRealtimeMessageReceived; + } + + + + +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/OutputDeviceEventSink.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/OutputDeviceEventSink.cs new file mode 100644 index 00000000..142c9931 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/OutputDeviceEventSink.cs @@ -0,0 +1,118 @@ +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Event sink that sends midi messages to an output device + /// + public class OutputDeviceEventSink : IDisposable + { + readonly OutputDevice FOutDevice; + readonly MidiEvents FEventSource; + + public int DeviceID + { + get + { + if (FOutDevice != null) + { + return FOutDevice.DeviceID; + } + else + { + return -1; + } + } + } + + public OutputDeviceEventSink(OutputDevice outDevice, MidiEvents eventSource) + { + FOutDevice = outDevice; + FEventSource = eventSource; + + RegisterEvents(); + + } + + private void RegisterEvents() + { + FEventSource.MessageReceived += FEventSource_MessageReceived; + FEventSource.ShortMessageReceived += EventSource_RawMessageReceived; + FEventSource.ChannelMessageReceived += EventSource_ChannelMessageReceived; + FEventSource.SysCommonMessageReceived += EventSource_SysCommonMessageReceived; + FEventSource.SysExMessageReceived += EventSource_SysExMessageReceived; + FEventSource.SysRealtimeMessageReceived += EventSource_SysRealtimeMessageReceived; + } + + + private void UnRegisterEvents() + { + FEventSource.MessageReceived -= FEventSource_MessageReceived; + FEventSource.ShortMessageReceived -= EventSource_RawMessageReceived; + FEventSource.ChannelMessageReceived -= EventSource_ChannelMessageReceived; + FEventSource.SysCommonMessageReceived -= EventSource_SysCommonMessageReceived; + FEventSource.SysExMessageReceived -= EventSource_SysExMessageReceived; + FEventSource.SysRealtimeMessageReceived -= EventSource_SysRealtimeMessageReceived; + } + + private void FEventSource_MessageReceived(IMidiMessage message) + { + var shortMessage = message as ShortMessage; + if (shortMessage != null) + { + FOutDevice.SendShort(shortMessage.Message); + return; + } + + var sysExMessage = message as SysExMessage; + if (sysExMessage != null) + FOutDevice.Send(sysExMessage); + } + + + private void EventSource_SysRealtimeMessageReceived(object sender, SysRealtimeMessageEventArgs e) + { + FOutDevice.Send(e.Message); + } + + private void EventSource_SysExMessageReceived(object sender, SysExMessageEventArgs e) + { + FOutDevice.Send(e.Message); + } + + private void EventSource_SysCommonMessageReceived(object sender, SysCommonMessageEventArgs e) + { + FOutDevice.Send(e.Message); + } + + private void EventSource_ChannelMessageReceived(object sender, ChannelMessageEventArgs e) + { + FOutDevice.Send(e.Message); + } + + private void EventSource_RawMessageReceived(object sender, ShortMessageEventArgs e) + { + FOutDevice.SendShort(e.Message.Message); + } + + /// + /// Disposes the underying output device and removes the events from the source + /// + public void Dispose() + { + UnRegisterEvents(); + FOutDevice.Dispose(); + } + + public static OutputDeviceEventSink FromDeviceID(int deviceID, MidiEvents eventSource) + { + var deviceCount = OutputDevice.DeviceCount; + if (deviceCount > 0) + { + deviceID %= deviceCount; + return new OutputDeviceEventSink(new OutputDevice(deviceID), eventSource); + } + return null; + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ShortMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ShortMessage.cs new file mode 100644 index 00000000..0abfdbd8 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ShortMessage.cs @@ -0,0 +1,261 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + public abstract class MidiMessageBase + { + /// + /// Delta samples when the event should be processed in the next audio buffer. + /// Leave at 0 for realtime input to play as fast as possible. + /// Set to the desired sample in the next buffer if you play a midi sequence synchronized to the audio callback + /// + public int DeltaFrames + { + get; + set; + } + + } + + /// + /// Represents the basic class for all MIDI short messages. + /// + /// + /// MIDI short messages represent all MIDI messages except meta messages + /// and system exclusive messages. This includes channel messages, system + /// realtime messages, and system common messages. + /// + public class ShortMessage : MidiMessageBase, IMidiMessage + { + #region ShortMessage Members + + #region Constants + + public const int DataMaxValue= 255; + + public const int StatusMaxValue = 255; + + // + // Bit manipulation constants. + // + + private const int StatusMask = ~255; + protected const int DataMask = ~StatusMask; + private const int Data1Mask = ~65280; + private const int Data2Mask = ~Data1Mask + DataMask; + private const int Shift = 8; + + #endregion + + protected int msg = 0; + + byte[] message; + bool rawMessageBuilt; + + #region Methods + + public byte[] GetBytes() + { + return Bytes; + } + + public ShortMessage() + { + //sub classes will fill the msg field + } + + public ShortMessage(int message) + { + this.msg = message; + } + + public ShortMessage(byte status, byte data1, byte data2) + { + this.message = new byte[] { status, data1, data2 }; + rawMessageBuilt = true; + msg = BuildIntMessage(this.message); + } + + private static byte[] BuildByteMessage(int intMessage) + { + unchecked + { + return new byte[] { (byte)ShortMessage.UnpackStatus(intMessage), + (byte)ShortMessage.UnpackData1(intMessage), + (byte)ShortMessage.UnpackData2(intMessage) }; + } + } + + private static int BuildIntMessage(byte[] message) + { + var intMessage = 0; + intMessage = ShortMessage.PackStatus(intMessage, message[0]); + intMessage = ShortMessage.PackData1(intMessage, message[1]); + intMessage = ShortMessage.PackData2(intMessage, message[2]); + return intMessage; + } + + internal static int PackStatus(int message, int status) + { + #region Require + + if(status < 0 || status > StatusMaxValue) + { + throw new ArgumentOutOfRangeException("status", status, + "Status value out of range."); + } + + #endregion + + return (message & StatusMask) | status; + } + + internal static int PackData1(int message, int data1) + { + #region Require + + if(data1 < 0 || data1 > DataMaxValue) + { + throw new ArgumentOutOfRangeException("data1", data1, + "Data 1 value out of range."); + } + + #endregion + + return (message & Data1Mask) | (data1 << Shift); + } + + internal static int PackData2(int message, int data2) + { + #region Require + + if(data2 < 0 || data2 > DataMaxValue) + { + throw new ArgumentOutOfRangeException("data2", data2, + "Data 2 value out of range."); + } + + #endregion + + return (message & Data2Mask) | (data2 << (Shift * 2)); + } + + internal static int UnpackStatus(int message) + { + return message & DataMask; + } + + internal static int UnpackData1(int message) + { + return (message & ~Data1Mask) >> Shift; + } + + internal static int UnpackData2(int message) + { + return (message & ~Data2Mask) >> (Shift * 2); + } + + #endregion + + #region Properties + + /// + /// Gets the timestamp of the midi input driver in milliseconds since the midi input driver was started. + /// + /// + /// The timestamp in milliseconds since the midi input driver was started. + /// + public int Timestamp + { + get; + internal set; + } + + /// + /// Gets the short message as a packed integer. + /// + /// + /// The message is packed into an integer value with the low-order byte + /// of the low-word representing the status value. The high-order byte + /// of the low-word represents the first data value, and the low-order + /// byte of the high-word represents the second data value. + /// + public int Message + { + get + { + return msg; + } + } + + /// + /// Gets the messages's status value. + /// + public int Status + { + get + { + return UnpackStatus(msg); + } + } + + public byte[] Bytes + { + get + { + if (!rawMessageBuilt) + { + this.message = BuildByteMessage(msg); + rawMessageBuilt = true; + } + return message; + } + } + + public virtual MessageType MessageType + { + get + { + return MessageType.Short; + } + } + + #endregion + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysCommonMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysCommonMessage.cs new file mode 100644 index 00000000..767391c9 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysCommonMessage.cs @@ -0,0 +1,257 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Multimedia.Midi +{ + #region System Common Message Types + + /// + /// Defines constants representing the various system common message types. + /// + public enum SysCommonType + { + /// + /// Represents the MTC system common message type. + /// + MidiTimeCode = 0xF1, + + /// + /// Represents the song position pointer type. + /// + SongPositionPointer, + + /// + /// Represents the song select type. + /// + SongSelect, + + /// + /// Represents the tune request type. + /// + TuneRequest = 0xF6 + } + + #endregion + + /// + /// Represents MIDI system common messages. + /// + [ImmutableObject(true)] + public sealed class SysCommonMessage : ShortMessage + { + #region SysCommonMessage Members + + #region Construction + + /// + /// Initializes a new instance of the SysCommonMessage class with the + /// specified type. + /// + /// + /// The type of SysCommonMessage. + /// + public SysCommonMessage(SysCommonType type) + { + msg = (int)type; + + #region Ensure + + Debug.Assert(SysCommonType == type); + + #endregion + } + + /// + /// Initializes a new instance of the SysCommonMessage class with the + /// specified type and the first data value. + /// + /// + /// The type of SysCommonMessage. + /// + /// + /// The first data value. + /// + /// + /// If data1 is less than zero or greater than 127. + /// + public SysCommonMessage(SysCommonType type, int data1) + { + msg = (int)type; + msg = PackData1(msg, data1); + + #region Ensure + + Debug.Assert(SysCommonType == type); + Debug.Assert(Data1 == data1); + + #endregion + } + + /// + /// Initializes a new instance of the SysCommonMessage class with the + /// specified type, first data value, and second data value. + /// + /// + /// The type of SysCommonMessage. + /// + /// + /// The first data value. + /// + /// + /// The second data value. + /// + /// + /// If data1 or data2 is less than zero or greater than 127. + /// + public SysCommonMessage(SysCommonType type, int data1, int data2) + { + msg = (int)type; + msg = PackData1(msg, data1); + msg = PackData2(msg, data2); + + #region Ensure + + Debug.Assert(SysCommonType == type); + Debug.Assert(Data1 == data1); + Debug.Assert(Data2 == data2); + + #endregion + } + + internal SysCommonMessage(int message) + { + this.msg = message; + } + + #endregion + + #region Methods + + /// + /// Returns a value for the current SysCommonMessage suitable for use + /// in hashing algorithms. + /// + /// + /// A hash code for the current SysCommonMessage. + /// + public override int GetHashCode() + { + return msg; + } + + /// + /// Determines whether two SysCommonMessage instances are equal. + /// + /// + /// The SysCommonMessage to compare with the current SysCommonMessage. + /// + /// + /// true if the specified SysCommonMessage is equal to the + /// current SysCommonMessage; otherwise, false. + /// + public override bool Equals(object obj) + { + #region Guard + + if(!(obj is SysCommonMessage)) + { + return false; + } + + #endregion + + SysCommonMessage message = (SysCommonMessage)obj; + + return (this.SysCommonType == message.SysCommonType && + this.Data1 == message.Data1 && + this.Data2 == message.Data2); + } + + #endregion + + #region Properties + + /// + /// Gets the SysCommonType. + /// + public SysCommonType SysCommonType + { + get + { + return (SysCommonType)UnpackStatus(msg); + } + } + + /// + /// Gets the first data value. + /// + public int Data1 + { + get + { + return UnpackData1(msg); + } + } + + /// + /// Gets the second data value. + /// + public int Data2 + { + get + { + return UnpackData2(msg); + } + } + + /// + /// Gets the MessageType. + /// + public override MessageType MessageType + { + get + { + return MessageType.SystemCommon; + } + } + + #endregion + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysExMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysExMessage.cs new file mode 100644 index 00000000..115a9039 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysExMessage.cs @@ -0,0 +1,266 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Defines constants representing various system exclusive message types. + /// + public enum SysExType + { + /// + /// Represents the start of system exclusive message type. + /// + Start = 0xF0, + + /// + /// Represents the continuation of a system exclusive message. + /// + Continuation = 0xF7 + } + + /// + /// Represents MIDI system exclusive messages. + /// + public sealed class SysExMessage : MidiMessageBase, IMidiMessage, IEnumerable + { + #region SysExEventMessage Members + + #region Constants + + /// + /// Maximum value for system exclusive channels. + /// + public const int SysExChannelMaxValue = 127; + + #endregion + + #region Fields + + // The system exclusive data. + private byte[] data; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the SysExMessageEventArgs class with the + /// specified system exclusive data. + /// + /// + /// The system exclusive data. + /// + /// + /// The system exclusive data's status byte, the first byte in the + /// data, must have a value of 0xF0 or 0xF7. + /// + public SysExMessage(byte[] data) + { + #region Require + + if(data.Length < 1) + { + throw new ArgumentException( + "System exclusive data is too short.", "data"); + } + else if(data[0] != (byte)SysExType.Start && + data[0] != (byte)SysExType.Continuation) + { + throw new ArgumentException( + "Unknown status value.", "data"); + } + + #endregion + + this.data = new byte[data.Length]; + data.CopyTo(this.data, 0); + } + + #endregion + + #region Methods + + public byte[] GetBytes() + { + byte[] clone = new byte[data.Length]; + + data.CopyTo(clone, 0); + + return clone; + } + + public void CopyTo(byte[] buffer, int index) + { + data.CopyTo(buffer, index); + } + + public override bool Equals(object obj) + { + #region Guard + + if(!(obj is SysExMessage)) + { + return false; + } + + #endregion + + SysExMessage message = (SysExMessage)obj; + + bool equals = true; + + if(this.Length != message.Length) + { + equals = false; + } + + for(int i = 0; i < this.Length && equals; i++) + { + if(this[i] != message[i]) + { + equals = false; + } + } + + return equals; + } + + public override int GetHashCode() + { + return data.GetHashCode(); + } + + #endregion + + #region Properties + + /// + /// Gets the timestamp of the midi input driver in milliseconds since the midi input driver was started. + /// + /// + /// The timestamp in milliseconds since the midi input driver was started. + /// + public int Timestamp + { + get; + internal set; + } + + /// + /// Gets the element at the specified index. + /// + /// + /// If index is less than zero or greater than or equal to the length + /// of the message. + /// + public byte this[int index] + { + get + { + #region Require + + if(index < 0 || index >= Length) + { + throw new ArgumentOutOfRangeException("index", index, + "Index into system exclusive message out of range."); + } + + #endregion + + return data[index]; + } + } + + /// + /// Gets the length of the system exclusive data. + /// + public int Length + { + get + { + return data.Length; + } + } + + /// + /// Gets the system exclusive type. + /// + public SysExType SysExType + { + get + { + return (SysExType)data[0]; + } + } + + #endregion + + #endregion + + /// + /// Gets the status value. + /// + public int Status + { + get + { + return (int)data[0]; + } + } + + /// + /// Gets the MessageType. + /// + public MessageType MessageType + { + get + { + return MessageType.SystemExclusive; + } + } + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + return data.GetEnumerator(); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysRealtimeMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysRealtimeMessage.cs new file mode 100644 index 00000000..9544af6e --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysRealtimeMessage.cs @@ -0,0 +1,227 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Diagnostics; + +namespace Sanford.Multimedia.Midi +{ + #region System Realtime Message Types + + /// + /// Defines constants representing the various system realtime message types. + /// + public enum SysRealtimeType + { + /// + /// Represents the clock system realtime type. + /// + Clock = 0xF8, + + /// + /// Represents the tick system realtime type. + /// + Tick, + + /// + /// Represents the start system realtime type. + /// + Start, + + /// + /// Represents the continue system realtime type. + /// + Continue, + + /// + /// Represents the stop system realtime type. + /// + Stop, + + /// + /// Represents the active sense system realtime type. + /// + ActiveSense = 0xFE, + + /// + /// Represents the reset system realtime type. + /// + Reset + } + + #endregion + + /// + /// Represents MIDI system realtime messages. + /// + /// + /// System realtime messages are MIDI messages that are primarily concerned + /// with controlling and synchronizing MIDI devices. + /// + [ImmutableObject(true)] + public sealed class SysRealtimeMessage : ShortMessage + { + #region SysRealtimeMessage Members + + #region System Realtime Messages + + /// + /// The instance of the system realtime start message. + /// + public static readonly SysRealtimeMessage StartMessage = + new SysRealtimeMessage(SysRealtimeType.Start); + + /// + /// The instance of the system realtime continue message. + /// + public static readonly SysRealtimeMessage ContinueMessage = + new SysRealtimeMessage(SysRealtimeType.Continue); + + /// + /// The instance of the system realtime stop message. + /// + public static readonly SysRealtimeMessage StopMessage = + new SysRealtimeMessage(SysRealtimeType.Stop); + + /// + /// The instance of the system realtime clock message. + /// + public static readonly SysRealtimeMessage ClockMessage = + new SysRealtimeMessage(SysRealtimeType.Clock); + + /// + /// The instance of the system realtime tick message. + /// + public static readonly SysRealtimeMessage TickMessage = + new SysRealtimeMessage(SysRealtimeType.Tick); + + /// + /// The instance of the system realtime active sense message. + /// + public static readonly SysRealtimeMessage ActiveSenseMessage = + new SysRealtimeMessage(SysRealtimeType.ActiveSense); + + /// + /// The instance of the system realtime reset message. + /// + public static readonly SysRealtimeMessage ResetMessage = + new SysRealtimeMessage(SysRealtimeType.Reset); + + #endregion + + // Make construction private so that a system realtime message cannot + // be constructed directly. + private SysRealtimeMessage(SysRealtimeType type) + { + msg = (int)type; + + #region Ensure + + Debug.Assert(SysRealtimeType == type); + + #endregion + } + + #region Methods + + /// + /// Returns a value for the current SysRealtimeMessage suitable for use in + /// hashing algorithms. + /// + /// + /// A hash code for the current SysRealtimeMessage. + /// + public override int GetHashCode() + { + return msg; + } + + /// + /// Determines whether two SysRealtimeMessage instances are equal. + /// + /// + /// The SysRealtimeMessage to compare with the current SysRealtimeMessage. + /// + /// + /// true if the specified SysRealtimeMessage is equal to the current + /// SysRealtimeMessage; otherwise, false. + /// + public override bool Equals(object obj) + { + #region Guard + + if(!(obj is SysRealtimeMessage)) + { + return false; + } + + #endregion + + SysRealtimeMessage message = (SysRealtimeMessage)obj; + + return this.msg == message.msg; + } + + #endregion + + #region Properties + + /// + /// Gets the SysRealtimeType. + /// + public SysRealtimeType SysRealtimeType + { + get + { + return (SysRealtimeType)msg; + } + } + + /// + /// Gets the MessageType. + /// + public override MessageType MessageType + { + get + { + return MessageType.SystemRealtime; + } + } + + #endregion + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/MidiNoteConverter.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/MidiNoteConverter.cs new file mode 100644 index 00000000..942b54e7 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/MidiNoteConverter.cs @@ -0,0 +1,156 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Converts a MIDI note number to its corresponding frequency. + /// + public sealed class MidiNoteConverter + { + /// + /// The minimum value a note ID can have. + /// + public const int NoteIDMinValue = 0; + + /// + /// The maximum value a note ID can have. + /// + public const int NoteIDMaxValue = 127; + + // Table for holding frequency values. + private readonly static double[] NoteToFrequencyTable = new double[NoteIDMaxValue + 1]; + + static MidiNoteConverter() + { + // The number of notes per octave. + int notesPerOctave = 12; + + // Reference frequency used for calculations. + double referenceFrequency = 440; + + // The note ID of the reference frequency. + int referenceNoteID = 69; + + double exponent; + + // Fill table with the frequencies of all MIDI notes. + for(int i = 0; i < NoteToFrequencyTable.Length; i++) + { + exponent = (double)(i - referenceNoteID) / notesPerOctave; + + NoteToFrequencyTable[i] = referenceFrequency * Math.Pow(2.0, exponent); + } + } + + // Prevents instances of this class from being created - no need for + // an instance to be created since this class only has static methods. + private MidiNoteConverter() + { + } + + /// + /// Converts the specified note to a frequency. + /// + /// + /// The ID of the note to convert. + /// + /// + /// The frequency of the specified note. + /// + public static double NoteToFrequency(int noteID) + { + #region Require + + if(noteID < NoteIDMinValue || noteID > NoteIDMaxValue) + { + throw new ArgumentOutOfRangeException("Note ID out of range."); + } + + #endregion + + return NoteToFrequencyTable[noteID]; + } + + /// + /// Converts the specified frequency to a note. + /// + /// + /// The frequency to convert. + /// + /// + /// The ID of the note closest to the specified frequency. + /// + public static int FrequencyToNote(double frequency) + { + int noteID = 0; + bool found = false; + + // Search for the note with a frequency near the specified frequency. + for(int i = 0; i < NoteIDMaxValue && !found; i++) + { + noteID = i; + + // If the specified frequency is less than the frequency of + // the next note. + if(frequency < NoteToFrequency(noteID + 1)) + { + // Indicate that the note ID for the specified frequency + // has been found. + found = true; + } + } + + // If the note is not the first or last note, narrow the results. + if(noteID > 0 && noteID < NoteIDMaxValue) + { + // Get the frequency of the previous note. + double previousFrequncy = NoteToFrequency(noteID - 1); + // Get the frequency of the next note. + double nextFrequency = NoteToFrequency(noteID + 1); + + // If the next note is closer in frequency than the previous note. + if(nextFrequency - frequency < frequency - previousFrequncy) + { + // Move to the next note. + noteID++; + } + } + + return noteID; + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelChaser.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelChaser.cs new file mode 100644 index 00000000..feca5a68 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelChaser.cs @@ -0,0 +1,167 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; + +namespace Sanford.Multimedia.Midi +{ + public class ChannelChaser + { + private ChannelMessage[,] controllerMessages; + + private ChannelMessage[] programChangeMessages; + + private ChannelMessage[] pitchBendMessages; + + private ChannelMessage[] channelPressureMessages; + + private ChannelMessage[] polyPressureMessages; + + public event EventHandler Chased; + + public ChannelChaser() + { + int c = ChannelMessage.MidiChannelMaxValue + 1; + int d = ShortMessage.DataMaxValue + 1; + + controllerMessages = new ChannelMessage[c, d]; + + programChangeMessages = new ChannelMessage[c]; + pitchBendMessages = new ChannelMessage[c]; + channelPressureMessages = new ChannelMessage[c]; + polyPressureMessages = new ChannelMessage[c]; + } + + public void Process(ChannelMessage message) + { + switch(message.Command) + { + case ChannelCommand.Controller: + controllerMessages[message.MidiChannel, message.Data1] = message; + break; + + case ChannelCommand.ChannelPressure: + channelPressureMessages[message.MidiChannel] = message; + break; + + case ChannelCommand.PitchWheel: + pitchBendMessages[message.MidiChannel] = message; + break; + + case ChannelCommand.PolyPressure: + polyPressureMessages[message.MidiChannel] = message; + break; + + case ChannelCommand.ProgramChange: + programChangeMessages[message.MidiChannel] = message; + break; + } + } + + public void Chase() + { + ArrayList chasedMessages = new ArrayList(); + + for(int c = 0; c <= ChannelMessage.MidiChannelMaxValue; c++) + { + for(int n = 0; n <= ShortMessage.DataMaxValue; n++) + { + if(controllerMessages[c, n] != null) + { + chasedMessages.Add(controllerMessages[c, n]); + + controllerMessages[c, n] = null; + } + } + + if(programChangeMessages[c] != null) + { + chasedMessages.Add(programChangeMessages[c]); + + programChangeMessages[c] = null; + } + + if(pitchBendMessages[c] != null) + { + chasedMessages.Add(pitchBendMessages[c]); + + pitchBendMessages[c] = null; + } + + if(channelPressureMessages[c] != null) + { + chasedMessages.Add(channelPressureMessages[c]); + + channelPressureMessages[c] = null; + } + + if(polyPressureMessages[c] != null) + { + chasedMessages.Add(polyPressureMessages[c]); + + polyPressureMessages[c] = null; + } + } + + OnChased(new ChasedEventArgs(chasedMessages)); + } + + public void Reset() + { + for(int c = 0; c <= ChannelMessage.MidiChannelMaxValue; c++) + { + for(int n = 0; n <= ShortMessage.DataMaxValue; n++) + { + controllerMessages[c, n] = null; + } + + programChangeMessages[c] = null; + pitchBendMessages[c] = null; + channelPressureMessages[c] = null; + polyPressureMessages[c] = null; + } + } + + protected virtual void OnChased(ChasedEventArgs e) + { + EventHandler handler = Chased; + + if(handler != null) + { + handler(this, e); + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelStopper.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelStopper.cs new file mode 100644 index 00000000..a6635762 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelStopper.cs @@ -0,0 +1,211 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; + +namespace Sanford.Multimedia.Midi +{ + public class ChannelStopper + { + private ChannelMessage[,] noteOnMessage; + + private bool[] holdPedal1Message; + + private bool[] holdPedal2Message; + + private bool[] sustenutoMessage; + + private ChannelMessageBuilder builder = new ChannelMessageBuilder(); + + public event EventHandler Stopped; + + public ChannelStopper() + { + int c = ChannelMessage.MidiChannelMaxValue + 1; + int d = ShortMessage.DataMaxValue + 1; + + noteOnMessage = new ChannelMessage[c, d]; + + holdPedal1Message = new bool[c]; + holdPedal2Message = new bool[c]; + sustenutoMessage = new bool[c]; + } + + public void Process(ChannelMessage message) + { + switch(message.Command) + { + case ChannelCommand.NoteOn: + if(message.Data2 > 0) + { + noteOnMessage[message.MidiChannel, message.Data1] = message; + } + else + { + noteOnMessage[message.MidiChannel, message.Data1] = null; + } + break; + + case ChannelCommand.NoteOff: + noteOnMessage[message.MidiChannel, message.Data1] = null; + break; + + case ChannelCommand.Controller: + switch(message.Data1) + { + case (int)ControllerType.HoldPedal1: + if(message.Data2 > 63) + { + holdPedal1Message[message.MidiChannel] = true; + } + else + { + holdPedal1Message[message.MidiChannel] = false; + } + break; + + case (int)ControllerType.HoldPedal2: + if(message.Data2 > 63) + { + holdPedal2Message[message.MidiChannel] = true; + } + else + { + holdPedal2Message[message.MidiChannel] = false; + } + break; + + case (int)ControllerType.SustenutoPedal: + if(message.Data2 > 63) + { + sustenutoMessage[message.MidiChannel] = true; + } + else + { + sustenutoMessage[message.MidiChannel] = false; + } + break; + } + break; + } + } + + public void AllSoundOff() + { + ArrayList stoppedMessages = new ArrayList(); + + for(int c = 0; c <= ChannelMessage.MidiChannelMaxValue; c++) + { + for(int n = 0; n <= ShortMessage.DataMaxValue; n++) + { + if(noteOnMessage[c, n] != null) + { + builder.MidiChannel = c; + builder.Command = ChannelCommand.NoteOff; + builder.Data1 = noteOnMessage[c, n].Data1; + builder.Build(); + + stoppedMessages.Add(builder.Result); + + noteOnMessage[c, n] = null; + } + } + + if(holdPedal1Message[c]) + { + builder.MidiChannel = c; + builder.Command = ChannelCommand.Controller; + builder.Data1 = (int)ControllerType.HoldPedal1; + builder.Build(); + + stoppedMessages.Add(builder.Result); + + holdPedal1Message[c] = false; + } + + if(holdPedal2Message[c]) + { + builder.MidiChannel = c; + builder.Command = ChannelCommand.Controller; + builder.Data1 = (int)ControllerType.HoldPedal2; + builder.Build(); + + stoppedMessages.Add(builder.Result); + + holdPedal2Message[c] = false; + } + + if(sustenutoMessage[c]) + { + builder.MidiChannel = c; + builder.Command = ChannelCommand.Controller; + builder.Data1 = (int)ControllerType.SustenutoPedal; + builder.Build(); + + stoppedMessages.Add(builder.Result); + + sustenutoMessage[c] = false; + } + } + + OnStopped(new StoppedEventArgs(stoppedMessages)); + } + + public void Reset() + { + for(int c = 0; c <= ChannelMessage.MidiChannelMaxValue; c++) + { + for(int n = 0; n <= ShortMessage.DataMaxValue; n++) + { + noteOnMessage[c, n] = null; + } + + holdPedal1Message[c] = false; + holdPedal2Message[c] = false; + sustenutoMessage[c] = false; + } + } + + protected virtual void OnStopped(StoppedEventArgs e) + { + EventHandler handler = Stopped; + + if(handler != null) + { + handler(this, e); + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChasedEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChasedEventArgs.cs new file mode 100644 index 00000000..678d68b1 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChasedEventArgs.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class ChasedEventArgs : EventArgs + { + private ICollection messages; + + public ChasedEventArgs(ICollection messages) + { + this.messages = messages; + } + + public ICollection Messages + { + get + { + return messages; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/StoppedEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/StoppedEventArgs.cs new file mode 100644 index 00000000..07cbee74 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/StoppedEventArgs.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class StoppedEventArgs : EventArgs + { + private ICollection messages; + + public StoppedEventArgs(ICollection messages) + { + this.messages = messages; + } + + public ICollection Messages + { + get + { + return messages; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiEvent.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiEvent.cs new file mode 100644 index 00000000..cc3e84b9 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiEvent.cs @@ -0,0 +1,148 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia.Midi +{ + public class MidiEvent + { + private object owner = null; + + private int absoluteTicks; + + private IMidiMessage message; + + private MidiEvent next = null; + + private MidiEvent previous = null; + + internal MidiEvent(object owner, int absoluteTicks, IMidiMessage message) + { + #region Require + + if(owner == null) + { + throw new ArgumentNullException("owner"); + } + else if(absoluteTicks < 0) + { + throw new ArgumentOutOfRangeException("absoluteTicks", absoluteTicks, + "Absolute ticks out of range."); + } + else if(message == null) + { + throw new ArgumentNullException("e"); + } + + #endregion + + this.owner = owner; + this.absoluteTicks = absoluteTicks; + this.message = message; + } + + internal void SetAbsoluteTicks(int absoluteTicks) + { + this.absoluteTicks = absoluteTicks; + } + + internal object Owner + { + get + { + return owner; + } + } + + public int AbsoluteTicks + { + get + { + return absoluteTicks; + } + } + + public int DeltaTicks + { + get + { + int deltaTicks; + + if(Previous != null) + { + deltaTicks = AbsoluteTicks - previous.AbsoluteTicks; + } + else + { + deltaTicks = AbsoluteTicks; + } + + return deltaTicks; + } + } + + public IMidiMessage MidiMessage + { + get + { + return message; + } + } + + internal MidiEvent Next + { + get + { + return next; + } + set + { + next = value; + } + } + + internal MidiEvent Previous + { + get + { + return previous; + } + set + { + previous = value; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiFileProperties.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiFileProperties.cs new file mode 100644 index 00000000..0d00fd24 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiFileProperties.cs @@ -0,0 +1,384 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Diagnostics; +using System.IO; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Defintes constants representing SMPTE frame rates. + /// + public enum SmpteFrameRate + { + Smpte24 = 24, + Smpte25 = 25, + Smpte30Drop = 29, + Smpte30 = 30 + } + + /// + /// The different types of sequences. + /// + public enum SequenceType + { + Ppqn, + Smpte + } + + /// + /// Represents MIDI file properties. + /// + internal class MidiFileProperties + { + private const int PropertyLength = 2; + + private static readonly byte[] MidiFileHeader = + { + (byte)'M', + (byte)'T', + (byte)'h', + (byte)'d', + 0, + 0, + 0, + 6 + }; + + private int format = 1; + + private int trackCount = 0; + + private int division = PpqnClock.PpqnMinValue; + + private SequenceType sequenceType = SequenceType.Ppqn; + + public MidiFileProperties() + { + } + + public void Read(Stream strm) + { + #region Require + + if(strm == null) + { + throw new ArgumentNullException("strm"); + } + + #endregion + + format = trackCount = division = 0; + + FindHeader(strm); + Format = (int)ReadProperty(strm); + TrackCount = (int)ReadProperty(strm); + Division = (int)ReadProperty(strm); + + #region Invariant + + AssertValid(); + + #endregion + } + + private void FindHeader(Stream stream) + { + bool found = false; + int result; + + while(!found) + { + result = stream.ReadByte(); + + if(result == 'M') + { + result = stream.ReadByte(); + + if(result == 'T') + { + result = stream.ReadByte(); + + if(result == 'h') + { + result = stream.ReadByte(); + + if(result == 'd') + { + found = true; + } + } + } + } + + if(result < 0) + { + throw new MidiFileException("Unable to find MIDI file header."); + } + } + + // Eat the header length. + for(int i = 0; i < 4; i++) + { + if(stream.ReadByte() < 0) + { + throw new MidiFileException("Unable to find MIDI file header."); + } + } + } + + private ushort ReadProperty(Stream strm) + { + byte[] data = new byte[PropertyLength]; + + int result = strm.Read(data, 0, data.Length); + + if(result != data.Length) + { + throw new MidiFileException("End of MIDI file unexpectedly reached."); + } + + if(BitConverter.IsLittleEndian) + { + Array.Reverse(data); + } + + return BitConverter.ToUInt16(data, 0); + } + + public void Write(Stream strm) + { + #region Require + + if(strm == null) + { + throw new ArgumentNullException("strm"); + } + + #endregion + + strm.Write(MidiFileHeader, 0, MidiFileHeader.Length); + WriteProperty(strm, (ushort)Format); + WriteProperty(strm, (ushort)TrackCount); + WriteProperty(strm, (ushort)Division); + } + + private void WriteProperty(Stream strm, ushort property) + { + byte[] data = BitConverter.GetBytes(property); + + if(BitConverter.IsLittleEndian) + { + Array.Reverse(data); + } + + strm.Write(data, 0, PropertyLength); + } + + private static bool IsSmpte(int division) + { + bool result; + byte[] data = BitConverter.GetBytes((short)division); + + if(BitConverter.IsLittleEndian) + { + Array.Reverse(data); + } + + if((sbyte)data[0] < 0) + { + result = true; + } + else + { + result = false; + } + + return result; + } + + [Conditional("DEBUG")] + private void AssertValid() + { + if(trackCount > 1) + { + Debug.Assert(Format == 1 || Format == 2); + } + + if(IsSmpte(Division)) + { + Debug.Assert(SequenceType == SequenceType.Smpte); + } + else + { + Debug.Assert(SequenceType == SequenceType.Ppqn); + Debug.Assert(Division >= PpqnClock.PpqnMinValue); + } + } + + public int Format + { + get + { + return format; + } + set + { + #region Require + + if(value < 0 || value > 2) + { + throw new ArgumentOutOfRangeException("Format", value, + "MIDI file format out of range."); + } + else if(value == 0 && trackCount > 1) + { + throw new ArgumentException( + "MIDI file format invalid for this track count."); + } + + #endregion + + format = value; + + #region Invariant + + AssertValid(); + + #endregion + } + } + + public int TrackCount + { + get + { + return trackCount; + } + set + { + #region Require + + if(value < 0) + { + throw new ArgumentOutOfRangeException("TrackCount", value, + "Track count out of range."); + } + else if(value > 1 && Format == 0) + { + throw new ArgumentException( + "Track count invalid for this format."); + } + + #endregion + + trackCount = value; + + #region Invariant + + AssertValid(); + + #endregion + } + } + + public int Division + { + get + { + return division; + } + set + { + if(IsSmpte(value)) + { + byte[] data = BitConverter.GetBytes((short)value); + + if(BitConverter.IsLittleEndian) + { + Array.Reverse(data); + } + + if((sbyte)data[0] != -(int)SmpteFrameRate.Smpte24 && + (sbyte)data[0] != -(int)SmpteFrameRate.Smpte25 && + (sbyte)data[0] != -(int)SmpteFrameRate.Smpte30 && + (sbyte)data[0] != -(int)SmpteFrameRate.Smpte30Drop) + { + throw new ArgumentException("Invalid SMPTE frame rate."); + } + else + { + sequenceType = SequenceType.Smpte; + } + } + else + { + if(value < PpqnClock.PpqnMinValue) + { + throw new ArgumentOutOfRangeException("Ppqn", value, + "Pulses per quarter note is smaller than 24."); + } + else + { + sequenceType = SequenceType.Ppqn; + } + } + + division = value; + + #region Invariant + + AssertValid(); + + #endregion + } + } + + public SequenceType SequenceType + { + get + { + return sequenceType; + } + } + } + + public class MidiFileException : ApplicationException + { + public MidiFileException(string message) : base(message) + { + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/RecordingSession.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/RecordingSession.cs new file mode 100644 index 00000000..26c5f4f8 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/RecordingSession.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Midi +{ + public class RecordingSession + { + private IClock clock; + + private List buffer = new List(); + + private Track result = new Track(); + + public RecordingSession(IClock clock) + { + this.clock = clock; + } + + public void Build() + { + result = new Track(); + + buffer.Sort(new TimestampComparer()); + + foreach(TimestampedMessage tm in buffer) + { + result.Insert(tm.ticks, tm.message); + } + } + + public void Clear() + { + buffer.Clear(); + } + + public Track Result + { + get + { + return result; + } + } + + public void Record(ChannelMessage message) + { + if(clock.IsRunning) + { + buffer.Add(new TimestampedMessage(clock.Ticks, message)); + } + } + + public void Record(SysExMessage message) + { + if(clock.IsRunning) + { + buffer.Add(new TimestampedMessage(clock.Ticks, message)); + } + } + + private struct TimestampedMessage + { + public int ticks; + + public IMidiMessage message; + + public TimestampedMessage(int ticks, IMidiMessage message) + { + this.ticks = ticks; + this.message = message; + } + } + + private class TimestampComparer : IComparer + { + #region IComparer Members + + public int Compare(TimestampedMessage x, TimestampedMessage y) + { + if(x.ticks > y.ticks) + { + return 1; + } + else if(x.ticks < y.ticks) + { + return -1; + } + else + { + return 0; + } + } + + #endregion + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequence.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequence.cs new file mode 100644 index 00000000..af6e534b --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequence.cs @@ -0,0 +1,847 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Represents a collection of Tracks. + /// + public sealed class Sequence : IComponent, ICollection + { + #region Sequence Members + + #region Fields + + // The collection of Tracks for the Sequence. + private List tracks = new List(); + + // The Sequence's MIDI file properties. + private MidiFileProperties properties = new MidiFileProperties(); + + private BackgroundWorker loadWorker = new BackgroundWorker(); + + private BackgroundWorker saveWorker = new BackgroundWorker(); + + private ISite site = null; + + private bool disposed = false; + + #endregion + + #region Events + + public event EventHandler LoadCompleted; + + public event ProgressChangedEventHandler LoadProgressChanged; + + public event EventHandler SaveCompleted; + + public event ProgressChangedEventHandler SaveProgressChanged; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the Sequence class. + /// + public Sequence() + { + InitializeBackgroundWorkers(); + } + + /// + /// Initializes a new instance of the Sequence class with the specified division. + /// + /// + /// The Sequence's division value. + /// + public Sequence(int division) + { + properties.Division = division; + properties.Format = 1; + + InitializeBackgroundWorkers(); + } + + /// + /// Initializes a new instance of the Sequence class with the specified + /// file name of the MIDI file to load. + /// + /// + /// The name of the MIDI file to load. + /// + public Sequence(string fileName) + { + InitializeBackgroundWorkers(); + + Load(fileName); + } + + /// + /// Initializes a new instance of the Sequence class with the specified + /// file stream of the MIDI file to load. + /// + /// + /// The stream of the MIDI file to load. + /// + public Sequence(Stream fileStream) + { + InitializeBackgroundWorkers(); + + Load(fileStream); + } + + private void InitializeBackgroundWorkers() + { + loadWorker.DoWork += new DoWorkEventHandler(LoadDoWork); + loadWorker.ProgressChanged += new ProgressChangedEventHandler(OnLoadProgressChanged); + loadWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(OnLoadCompleted); + loadWorker.WorkerReportsProgress = true; + + saveWorker.DoWork += new DoWorkEventHandler(SaveDoWork); + saveWorker.ProgressChanged += new ProgressChangedEventHandler(OnSaveProgressChanged); + saveWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(OnSaveCompleted); + saveWorker.WorkerReportsProgress = true; + } + + #endregion + + #region Methods + + /// + /// Loads a MIDI file into the Sequence. + /// + /// + /// The MIDI file's name. + /// + public void Load(string fileName) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + else if(IsBusy) + { + throw new InvalidOperationException(); + } + else if(fileName == null) + { + throw new ArgumentNullException("fileName"); + } + + #endregion + + FileStream stream = new FileStream(fileName, FileMode.Open, + FileAccess.Read, FileShare.Read); + + using(stream) + { + MidiFileProperties newProperties = new MidiFileProperties(); + TrackReader reader = new TrackReader(); + List newTracks = new List(); + + newProperties.Read(stream); + + for(int i = 0; i < newProperties.TrackCount; i++) + { + reader.Read(stream); + newTracks.Add(reader.Track); + } + + properties = newProperties; + tracks = newTracks; + } + + #region Ensure + + Debug.Assert(Count == properties.TrackCount); + + #endregion + } + + /// + /// Loads a MIDI stream into the Sequence. + /// + /// + /// The MIDI file's stream. + /// + public void Load(Stream fileStream) + { + #region Require + + if (disposed) + { + throw new ObjectDisposedException("Sequence"); + } + else if (IsBusy) + { + throw new InvalidOperationException(); + } + else if (fileStream == null) + { + throw new ArgumentNullException("fileStream"); + } + + #endregion + + using (fileStream) + { + MidiFileProperties newProperties = new MidiFileProperties(); + TrackReader reader = new TrackReader(); + List newTracks = new List(); + + newProperties.Read(fileStream); + + for (int i = 0; i < newProperties.TrackCount; i++) + { + reader.Read(fileStream); + newTracks.Add(reader.Track); + } + + properties = newProperties; + tracks = newTracks; + } + + #region Ensure + + Debug.Assert(Count == properties.TrackCount); + + #endregion + } + + public void LoadAsync(string fileName) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + else if(IsBusy) + { + throw new InvalidOperationException(); + } + else if(fileName == null) + { + throw new ArgumentNullException("fileName"); + } + + #endregion + + loadWorker.RunWorkerAsync(fileName); + } + + public void LoadAsyncCancel() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + loadWorker.CancelAsync(); + } + + /// + /// Saves the Sequence as a MIDI file. + /// + /// + /// The name to use for saving the MIDI file. + /// + public void Save(string fileName) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + else if(fileName == null) + { + throw new ArgumentNullException("fileName"); + } + + #endregion + + FileStream stream = new FileStream(fileName, FileMode.Create, + FileAccess.Write, FileShare.None); + using (stream) + { + Save(stream); + } + } + + /// + /// Saves the Sequence as a Stream. + /// + /// + /// The stream to use for saving the sequence. + /// + public void Save(Stream stream) + { + properties.Write(stream); + + TrackWriter writer = new TrackWriter(); + + foreach(Track trk in tracks) + { + writer.Track = trk; + writer.Write(stream); + } + } + + public void SaveAsync(string fileName) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + else if(IsBusy) + { + throw new InvalidOperationException(); + } + else if(fileName == null) + { + throw new ArgumentNullException("fileName"); + } + + #endregion + + saveWorker.RunWorkerAsync(fileName); + } + + public void SaveAsyncCancel() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + saveWorker.CancelAsync(); + } + + /// + /// Gets the length in ticks of the Sequence. + /// + /// + /// The length in ticks of the Sequence. + /// + /// + /// The length in ticks of the Sequence is represented by the Track + /// with the longest length. + /// + public int GetLength() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + int length = 0; + + foreach(Track t in this) + { + if(t.Length > length) + { + length = t.Length; + } + } + + return length; + } + + private void OnLoadCompleted(object sender, RunWorkerCompletedEventArgs e) + { + EventHandler handler = LoadCompleted; + + if(handler != null) + { + handler(this, new AsyncCompletedEventArgs(e.Error, e.Cancelled, null)); + } + } + + private void OnLoadProgressChanged(object sender, ProgressChangedEventArgs e) + { + ProgressChangedEventHandler handler = LoadProgressChanged; + + if(handler != null) + { + handler(this, e); + } + } + + private void LoadDoWork(object sender, DoWorkEventArgs e) + { + string fileName = (string)e.Argument; + + FileStream stream = new FileStream(fileName, FileMode.Open, + FileAccess.Read, FileShare.Read); + + using(stream) + { + MidiFileProperties newProperties = new MidiFileProperties(); + TrackReader reader = new TrackReader(); + List newTracks = new List(); + + newProperties.Read(stream); + + float percentage; + + for(int i = 0; i < newProperties.TrackCount && !loadWorker.CancellationPending; i++) + { + reader.Read(stream); + newTracks.Add(reader.Track); + + percentage = (i + 1f) / newProperties.TrackCount; + + loadWorker.ReportProgress((int)(100 * percentage)); + } + + if(loadWorker.CancellationPending) + { + e.Cancel = true; + } + else + { + properties = newProperties; + tracks = newTracks; + } + } + } + + private void OnSaveCompleted(object sender, RunWorkerCompletedEventArgs e) + { + EventHandler handler = SaveCompleted; + + if(handler != null) + { + handler(this, new AsyncCompletedEventArgs(e.Error, e.Cancelled, null)); + } + } + + private void OnSaveProgressChanged(object sender, ProgressChangedEventArgs e) + { + ProgressChangedEventHandler handler = SaveProgressChanged; + + if(handler != null) + { + handler(this, e); + } + } + + private void SaveDoWork(object sender, DoWorkEventArgs e) + { + string fileName = (string)e.Argument; + + FileStream stream = new FileStream(fileName, FileMode.Create, + FileAccess.Write, FileShare.None); + + using(stream) + { + properties.Write(stream); + + TrackWriter writer = new TrackWriter(); + + float percentage; + + for(int i = 0; i < tracks.Count && !saveWorker.CancellationPending; i++) + { + writer.Track = tracks[i]; + writer.Write(stream); + + percentage = (i + 1f) / properties.TrackCount; + + saveWorker.ReportProgress((int)(100 * percentage)); + } + + if(saveWorker.CancellationPending) + { + e.Cancel = true; + } + } + } + + #endregion + + #region Properties + + /// + /// Gets the Track at the specified index. + /// + /// + /// The index of the Track to get. + /// + /// + /// The Track at the specified index. + /// + public Track this[int index] + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + else if(index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException("index", index, + "Sequence index out of range."); + } + + #endregion + + return tracks[index]; + } + } + + /// + /// Gets the Sequence's division value. + /// + public int Division + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + return properties.Division; + } + } + + /// + /// Gets or sets the Sequence's format value. + /// + public int Format + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + return properties.Format; + } + set + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + else if(IsBusy) + { + throw new InvalidOperationException(); + } + + #endregion + + properties.Format = value; + } + } + + /// + /// Gets the Sequence's type. + /// + public SequenceType SequenceType + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + return properties.SequenceType; + } + } + + public bool IsBusy + { + get + { + return loadWorker.IsBusy || saveWorker.IsBusy; + } + } + + #endregion + + #endregion + + #region ICollection Members + + public void Add(Track item) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + else if(item == null) + { + throw new ArgumentNullException("item"); + } + + #endregion + + tracks.Add(item); + + properties.TrackCount = tracks.Count; + } + + public void Clear() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + tracks.Clear(); + + properties.TrackCount = tracks.Count; + } + + public bool Contains(Track item) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + return tracks.Contains(item); + } + + public void CopyTo(Track[] array, int arrayIndex) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + tracks.CopyTo(array, arrayIndex); + } + + public int Count + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + return tracks.Count; + } + } + + public bool IsReadOnly + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + return false; + } + } + + public bool Remove(Track item) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + bool result = tracks.Remove(item); + + if(result) + { + properties.TrackCount = tracks.Count; + } + + return result; + } + + #endregion + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + return tracks.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Sequence"); + } + + #endregion + + return tracks.GetEnumerator(); + } + + #endregion + + #region IComponent Members + + public event EventHandler Disposed; + + public ISite Site + { + get + { + return site; + } + set + { + site = value; + } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + #region Guard + + if(disposed) + { + return; + } + + #endregion + + loadWorker.Dispose(); + saveWorker.Dispose(); + + disposed = true; + + EventHandler handler = Disposed; + + if(handler != null) + { + handler(this, EventArgs.Empty); + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequencer.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequencer.cs new file mode 100644 index 00000000..d19f33c0 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequencer.cs @@ -0,0 +1,386 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Sanford.Multimedia.Midi +{ + public class Sequencer : IComponent + { + private Sequence sequence = null; + + private List> enumerators = new List>(); + + private MessageDispatcher dispatcher = new MessageDispatcher(); + + private ChannelChaser chaser = new ChannelChaser(); + + private ChannelStopper stopper = new ChannelStopper(); + + private MidiInternalClock clock = new MidiInternalClock(); + + private int tracksPlayingCount; + + private readonly object lockObject = new object(); + + private bool playing = false; + + private bool disposed = false; + + private ISite site = null; + + #region Events + + public event EventHandler PlayingCompleted; + + public event EventHandler ChannelMessagePlayed + { + add + { + dispatcher.ChannelMessageDispatched += value; + } + remove + { + dispatcher.ChannelMessageDispatched -= value; + } + } + + public event EventHandler SysExMessagePlayed + { + add + { + dispatcher.SysExMessageDispatched += value; + } + remove + { + dispatcher.SysExMessageDispatched -= value; + } + } + + public event EventHandler MetaMessagePlayed + { + add + { + dispatcher.MetaMessageDispatched += value; + } + remove + { + dispatcher.MetaMessageDispatched -= value; + } + } + + public event EventHandler Chased + { + add + { + chaser.Chased += value; + } + remove + { + chaser.Chased -= value; + } + } + + public event EventHandler Stopped + { + add + { + stopper.Stopped += value; + } + remove + { + stopper.Stopped -= value; + } + } + + #endregion + + public Sequencer() + { + dispatcher.MetaMessageDispatched += delegate(object sender, MetaMessageEventArgs e) + { + if(e.Message.MetaType == MetaType.EndOfTrack) + { + tracksPlayingCount--; + + if(tracksPlayingCount == 0) + { + Stop(); + + OnPlayingCompleted(EventArgs.Empty); + } + } + else + { + clock.Process(e.Message); + } + }; + + dispatcher.ChannelMessageDispatched += delegate(object sender, ChannelMessageEventArgs e) + { + stopper.Process(e.Message); + }; + + clock.Tick += delegate(object sender, EventArgs e) + { + lock(lockObject) + { + if(!playing) + { + return; + } + + foreach(IEnumerator enumerator in enumerators) + { + enumerator.MoveNext(); + } + } + }; + } + + ~Sequencer() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + if(disposing) + { + lock(lockObject) + { + Stop(); + + clock.Dispose(); + + disposed = true; + + GC.SuppressFinalize(this); + } + } + } + + public void Start() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + lock(lockObject) + { + Stop(); + + Position = 0; + + Continue(); + } + } + + public void Continue() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + #region Guard + + if(Sequence == null) + { + return; + } + + #endregion + + lock(lockObject) + { + Stop(); + + enumerators.Clear(); + + foreach(Track t in Sequence) + { + enumerators.Add(t.TickIterator(Position, chaser, dispatcher).GetEnumerator()); + } + + tracksPlayingCount = Sequence.Count; + + playing = true; + clock.Ppqn = sequence.Division; + clock.Continue(); + } + } + + public void Stop() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + lock(lockObject) + { + #region Guard + + if(!playing) + { + return; + } + + #endregion + + playing = false; + clock.Stop(); + stopper.AllSoundOff(); + } + } + + protected virtual void OnPlayingCompleted(EventArgs e) + { + EventHandler handler = PlayingCompleted; + + if(handler != null) + { + handler(this, e); + } + } + + protected virtual void OnDisposed(EventArgs e) + { + EventHandler handler = Disposed; + + if(handler != null) + { + handler(this, e); + } + } + + public int Position + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + return clock.Ticks; + } + set + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + else if(value < 0) + { + throw new ArgumentOutOfRangeException(); + } + + #endregion + + bool wasPlaying; + + lock(lockObject) + { + wasPlaying = playing; + + Stop(); + + clock.SetTicks(value); + } + + lock(lockObject) + { + if(wasPlaying) + { + Continue(); + } + } + } + } + + public Sequence Sequence + { + get + { + return sequence; + } + set + { + #region Require + + if(value == null) + { + throw new ArgumentNullException(); + } + else if(value.SequenceType == SequenceType.Smpte) + { + throw new NotSupportedException(); + } + + #endregion + + lock(lockObject) + { + Stop(); + sequence = value; + } + } + } + + #region IComponent Members + + public event EventHandler Disposed; + + public ISite Site + { + get + { + return site; + } + set + { + site = value; + } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + #region Guard + + if(disposed) + { + return; + } + + #endregion + + Dispose(true); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Iterators.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Iterators.cs new file mode 100644 index 00000000..7115bb2c --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Iterators.cs @@ -0,0 +1,135 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Sanford.Multimedia.Midi +{ + public sealed partial class Track + { + #region Iterators + + public IEnumerable Iterator() + { + MidiEvent current = head; + + while(current != null) + { + yield return current; + + current = current.Next; + } + + current = endOfTrackMidiEvent; + + yield return current; + } + + public IEnumerable DispatcherIterator(MessageDispatcher dispatcher) + { + IEnumerator enumerator = Iterator().GetEnumerator(); + + while(enumerator.MoveNext()) + { + yield return enumerator.Current.AbsoluteTicks; + + dispatcher.Dispatch(enumerator.Current, this); + } + } + + public IEnumerable TickIterator(int startPosition, + ChannelChaser chaser, MessageDispatcher dispatcher) + { + #region Require + + if(startPosition < 0) + { + throw new ArgumentOutOfRangeException("startPosition", startPosition, + "Start position out of range."); + } + + #endregion + + IEnumerator enumerator = Iterator().GetEnumerator(); + + bool notFinished = enumerator.MoveNext(); + IMidiMessage message; + + while(notFinished && enumerator.Current.AbsoluteTicks < startPosition) + { + message = enumerator.Current.MidiMessage; + + if(message.MessageType == MessageType.Channel) + { + chaser.Process((ChannelMessage)message); + } + else if(message.MessageType == MessageType.Meta) + { + dispatcher.Dispatch(enumerator.Current, this); + } + + notFinished = enumerator.MoveNext(); + } + + chaser.Chase(); + + int ticks = startPosition; + + while(notFinished) + { + while(ticks < enumerator.Current.AbsoluteTicks) + { + yield return ticks; + + ticks++; + } + + yield return ticks; + + while(notFinished && enumerator.Current.AbsoluteTicks == ticks) + { + dispatcher.Dispatch(enumerator.Current, this); + + notFinished = enumerator.MoveNext(); + } + + ticks++; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Test.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Test.cs new file mode 100644 index 00000000..57192eef --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Test.cs @@ -0,0 +1,128 @@ +using System; +using System.Diagnostics; + +namespace Sanford.Multimedia.Midi +{ + public sealed partial class Track + { + [Conditional("DEBUG")] + public static void Test() + { + TestInsert(); + TestRemoveAt(); + TestMerge(); + } + + [Conditional("DEBUG")] + private static void TestInsert() + { + Track track = new Track(); + int midiEventCount = 2000; + int positionMax = 32000; + int endOfTrackOffset = 1000; + int length = 0; + int position = 0; + ChannelMessage message = new ChannelMessage(ChannelCommand.NoteOff, 0, 60, 0); + Random r = new Random(); + + for(int i = 0; i < midiEventCount; i++) + { + position = r.Next(positionMax); + + if(position > length) + { + length = position; + } + + track.Insert(position, message); + } + + track.EndOfTrackOffset = endOfTrackOffset; + + length += track.EndOfTrackOffset; + + Debug.Assert(track.Count == midiEventCount + 1); + Debug.Assert(track.Length == length); + } + + [Conditional("DEBUG")] + private static void TestRemoveAt() + { + Track a = new Track(); + ChannelMessage message = new ChannelMessage(ChannelCommand.NoteOff, 0, 60, 0); + + a.Insert(0, message); + a.Insert(10, message); + a.Insert(20, message); + a.Insert(30, message); + a.Insert(40, message); + + int count = a.Count; + + a.RemoveAt(0); + + Debug.Assert(a.Count == count - 1); + + a.RemoveAt(a.Count - 2); + + Debug.Assert(a.Count == count - 2); + Debug.Assert(a.GetMidiEvent(0).AbsoluteTicks == 10); + Debug.Assert(a.GetMidiEvent(a.Count - 2).AbsoluteTicks == 30); + + a.RemoveAt(0); + a.RemoveAt(0); + a.RemoveAt(0); + + Debug.Assert(a.Count == 1); + } + + [Conditional("DEBUG")] + private static void TestMerge() + { + Track a = new Track(); + Track b = new Track(); + + a.Merge(b); + + Debug.Assert(a.Count == 1); + + ChannelMessage message = new ChannelMessage(ChannelCommand.NoteOff, 0, 60, 0); + + b.Insert(0, message); + b.Insert(10, message); + b.Insert(20, message); + b.Insert(30, message); + b.Insert(40, message); + + a.Merge(b); + + Debug.Assert(a.Count == 1 + b.Count - 1); + + a.Clear(); + + Debug.Assert(a.Count == 1); + + a.Insert(0, message); + a.Insert(10, message); + a.Insert(20, message); + a.Insert(30, message); + a.Insert(40, message); + + int count = a.Count; + + a.Merge(b); + + Debug.Assert(a.Count == count + b.Count - 1); + Debug.Assert(a.GetMidiEvent(0).DeltaTicks == 0); + Debug.Assert(a.GetMidiEvent(1).DeltaTicks == 0); + Debug.Assert(a.GetMidiEvent(2).DeltaTicks == 10); + Debug.Assert(a.GetMidiEvent(3).DeltaTicks == 0); + Debug.Assert(a.GetMidiEvent(4).DeltaTicks == 10); + Debug.Assert(a.GetMidiEvent(5).DeltaTicks == 0); + Debug.Assert(a.GetMidiEvent(6).DeltaTicks == 10); + Debug.Assert(a.GetMidiEvent(7).DeltaTicks == 0); + Debug.Assert(a.GetMidiEvent(8).DeltaTicks == 10); + Debug.Assert(a.GetMidiEvent(9).DeltaTicks == 0); + } + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.cs new file mode 100644 index 00000000..0f7a8ad0 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.cs @@ -0,0 +1,607 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Diagnostics; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Represents a collection of MidiEvents and a MIDI track within a + /// Sequence. + /// + public sealed partial class Track + { + #region Track Members + + #region Fields + + // The number of MidiEvents in the Track. Will always be at least 1 + // because the Track will always have an end of track message. + private int count = 1; + + // The number of ticks to offset the end of track message. + private int endOfTrackOffset = 0; + + // The first MidiEvent in the Track. + private MidiEvent head = null; + + // The last MidiEvent in the Track, not including the end of track + // message. + private MidiEvent tail = null; + + // The end of track MIDI event. + private MidiEvent endOfTrackMidiEvent; + + #endregion + + #region Construction + + public Track() + { + endOfTrackMidiEvent = new MidiEvent(this, Length, MetaMessage.EndOfTrackMessage); + } + + #endregion + + #region Methods + + /// + /// Inserts an IMidiMessage at the specified position in absolute ticks. + /// + /// + /// The position in the Track in absolute ticks in which to insert the + /// IMidiMessage. + /// + /// + /// The IMidiMessage to insert. + /// + public void Insert(int position, IMidiMessage message) + { + #region Require + + if (position < 0) + { + throw new ArgumentOutOfRangeException("position", position, + "IMidiMessage position out of range."); + } + else if (message == null) + { + throw new ArgumentNullException("message"); + } + + #endregion + + MidiEvent newMidiEvent = new MidiEvent(this, position, message); + + if (head == null) + { + head = newMidiEvent; + tail = newMidiEvent; + } + else if (position < head.AbsoluteTicks) + { + newMidiEvent.Next = head; + head.Previous = newMidiEvent; + head = newMidiEvent; + } + else if (position >= tail.AbsoluteTicks) + { + newMidiEvent.Previous = tail; + tail.Next = newMidiEvent; + tail = newMidiEvent; + endOfTrackMidiEvent.SetAbsoluteTicks(Length); + endOfTrackMidiEvent.Previous = tail; + } + else + { + MidiEvent current = head; + + while (!(current.AbsoluteTicks > position)) + current = current.Next; + + newMidiEvent.Next = current; + newMidiEvent.Previous = current.Previous; + current.Previous.Next = newMidiEvent; + current.Previous = newMidiEvent; + } + + count++; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Clears all of the MidiEvents, with the exception of the end of track + /// message, from the Track. + /// + public void Clear() + { + head = tail = null; + + count = 1; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Merges the specified Track with the current Track. + /// + /// + /// The Track to merge with. + /// + public void Merge(Track trk) + { + #region Require + + if (trk == null) + { + throw new ArgumentNullException("trk"); + } + + #endregion + + #region Guard + + if (trk == this) + { + return; + } + else if (trk.Count == 1) + { + return; + } + + #endregion + +#if(DEBUG) + int oldCount = Count; +#endif + + count += trk.Count - 1; + + MidiEvent a = head; + MidiEvent b = trk.head; + MidiEvent current = null; + + Debug.Assert(b != null); + + if (a != null && a.AbsoluteTicks <= b.AbsoluteTicks) + { + current = new MidiEvent(this, a.AbsoluteTicks, a.MidiMessage); + a = a.Next; + } + else + { + current = new MidiEvent(this, b.AbsoluteTicks, b.MidiMessage); + b = b.Next; + } + + head = current; + + while (a != null && b != null) + { + while (a != null && a.AbsoluteTicks <= b.AbsoluteTicks) + { + current.Next = new MidiEvent(this, a.AbsoluteTicks, a.MidiMessage); + current.Next.Previous = current; + current = current.Next; + a = a.Next; + } + + if (a != null) + { + while (b != null && b.AbsoluteTicks <= a.AbsoluteTicks) + { + current.Next = new MidiEvent(this, b.AbsoluteTicks, b.MidiMessage); + current.Next.Previous = current; + current = current.Next; + b = b.Next; + } + } + } + + while (a != null) + { + current.Next = new MidiEvent(this, a.AbsoluteTicks, a.MidiMessage); + current.Next.Previous = current; + current = current.Next; + a = a.Next; + } + + while (b != null) + { + current.Next = new MidiEvent(this, b.AbsoluteTicks, b.MidiMessage); + current.Next.Previous = current; + current = current.Next; + b = b.Next; + } + + tail = current; + + endOfTrackMidiEvent.SetAbsoluteTicks(Length); + endOfTrackMidiEvent.Previous = tail; + + #region Ensure +#if(DEBUG) + Debug.Assert(count == oldCount + trk.Count - 1); +#endif + #endregion + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Removes the MidiEvent at the specified index. + /// + /// + /// The index into the Track at which to remove the MidiEvent. + /// + public void RemoveAt(int index) + { + #region Require + + if (index < 0) + { + throw new ArgumentOutOfRangeException("index", index, "Track index out of range."); + } + else if (index == Count - 1) + { + throw new ArgumentException("Cannot remove the end of track event.", "index"); + } + + #endregion + + MidiEvent current = GetMidiEvent(index); + + if (current.Previous != null) + { + current.Previous.Next = current.Next; + } + else + { + Debug.Assert(current == head); + + head = head.Next; + } + + if (current.Next != null) + { + current.Next.Previous = current.Previous; + } + else + { + Debug.Assert(current == tail); + + tail = tail.Previous; + + endOfTrackMidiEvent.SetAbsoluteTicks(Length); + endOfTrackMidiEvent.Previous = tail; + } + + current.Next = current.Previous = null; + + count--; + + #region Invariant + + AssertValid(); + + #endregion + } + + /// + /// Gets the MidiEvent at the specified index. + /// + /// + /// The index of the MidiEvent to get. + /// + /// + /// The MidiEvent at the specified index. + /// + public MidiEvent GetMidiEvent(int index) + { + #region Require + + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException("index", index, + "Track index out of range."); + } + + #endregion + + MidiEvent result; + + if (index == Count - 1) + { + result = endOfTrackMidiEvent; + } + else + { + if (index < Count / 2) + { + result = head; + + for (int i = 0; i < index; i++) + { + result = result.Next; + } + } + else + { + result = tail; + + for (int i = Count - 2; i > index; i--) + { + result = result.Previous; + } + } + } + + #region Ensure + +#if(DEBUG) + if (index == Count - 1) + { + Debug.Assert(result.AbsoluteTicks == Length); + Debug.Assert(result.MidiMessage == MetaMessage.EndOfTrackMessage); + } + else + { + MidiEvent t = head; + + for (int i = 0; i < index; i++) + { + t = t.Next; + } + + Debug.Assert(t == result); + } +#endif + + #endregion + + return result; + } + + public void Move(MidiEvent e, int newPosition) + { + #region Require + + if (e.Owner != this) + { + throw new ArgumentException("MidiEvent does not belong to this Track."); + } + else if (newPosition < 0) + { + throw new ArgumentOutOfRangeException("newPosition"); + } + else if (e == endOfTrackMidiEvent) + { + throw new InvalidOperationException( + "Cannot move end of track message. Use the EndOfTrackOffset property instead."); + } + + #endregion + + MidiEvent previous = e.Previous; + MidiEvent next = e.Next; + + if (e.Previous != null && e.Previous.AbsoluteTicks > newPosition) + { + e.Previous.Next = e.Next; + + if (e.Next != null) + { + e.Next.Previous = e.Previous; + } + + while (previous != null && previous.AbsoluteTicks > newPosition) + { + next = previous; + previous = previous.Previous; + } + } + else if (e.Next != null && e.Next.AbsoluteTicks < newPosition) + { + e.Next.Previous = e.Previous; + + if (e.Previous != null) + { + e.Previous.Next = e.Next; + } + + while (next != null && next.AbsoluteTicks < newPosition) + { + previous = next; + next = next.Next; + } + } + + if (previous != null) + { + previous.Next = e; + } + + if (next != null) + { + next.Previous = e; + } + + e.Previous = previous; + e.Next = next; + e.SetAbsoluteTicks(newPosition); + + if (newPosition < head.AbsoluteTicks) + { + head = e; + } + + if (newPosition > tail.AbsoluteTicks) + { + tail = e; + } + + endOfTrackMidiEvent.SetAbsoluteTicks(Length); + endOfTrackMidiEvent.Previous = tail; + + #region Invariant + + AssertValid(); + + #endregion + } + + [Conditional("DEBUG")] + private void AssertValid() + { + int c = 1; + MidiEvent current = head; + int ticks = 1; + + while (current != null) + { + ticks += current.DeltaTicks; + + if (current.Previous != null) + { + Debug.Assert(current.AbsoluteTicks >= current.Previous.AbsoluteTicks); + Debug.Assert(current.DeltaTicks == current.AbsoluteTicks - current.Previous.AbsoluteTicks); + } + + if (current.Next == null) + { + Debug.Assert(tail == current); + } + + current = current.Next; + + c++; + } + + ticks += EndOfTrackOffset; + + Debug.Assert(ticks == Length, "Length mismatch"); + Debug.Assert(c == Count, "Count mismatch"); + } + + #endregion + + #region Properties + + /// + /// Gets the number of MidiEvents in the Track. + /// + public int Count + { + get + { + return count; + } + } + + /// + /// Gets the length of the Track in ticks. + /// + public int Length + { + get + { + int length = EndOfTrackOffset; + + if (tail != null) + { + length += tail.AbsoluteTicks; + } + + return length; + } + } + + /// + /// Gets or sets the end of track meta message position offset. + /// + public int EndOfTrackOffset + { + get + { + return endOfTrackOffset; + } + set + { + #region Require + + if (value < 0) + { + throw new ArgumentOutOfRangeException("EndOfTrackOffset", value, + "End of track offset out of range."); + } + + #endregion + + endOfTrackOffset = value; + + endOfTrackMidiEvent.SetAbsoluteTicks(Length); + } + } + + /// + /// Gets an object that can be used to synchronize access to the Track. + /// + public object SyncRoot + { + get + { + return this; + } + } + + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/TrackReader.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/TrackReader.cs new file mode 100644 index 00000000..863b9631 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/TrackReader.cs @@ -0,0 +1,448 @@ +#region License + +/* Copyright (c) 2005 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.IO; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Reads a track from a stream. + /// + internal class TrackReader + { + private Track track = new Track(); + + private Track newTrack = new Track(); + + private ChannelMessageBuilder cmBuilder = new ChannelMessageBuilder(); + + private SysCommonMessageBuilder scBuilder = new SysCommonMessageBuilder(); + + private Stream stream; + + private byte[] trackData; + + private int trackIndex; + + private int previousTicks; + + private int ticks; + + private int status; + + private int runningStatus; + + public TrackReader() + { + } + + public void Read(Stream strm) + { + stream = strm; + FindTrack(); + + int trackLength = GetTrackLength(); + trackData = new byte[trackLength]; + + int result = strm.Read(trackData, 0, trackLength); + + if(result < 0) + { + throw new MidiFileException("End of MIDI file unexpectedly reached."); + } + + newTrack = new Track(); + + ParseTrackData(); + + track = newTrack; + } + + private void FindTrack() + { + bool found = false; + int result; + + while(!found) + { + result = stream.ReadByte(); + + if(result == 'M') + { + result = stream.ReadByte(); + + if(result == 'T') + { + result = stream.ReadByte(); + + if(result == 'r') + { + result = stream.ReadByte(); + + if(result == 'k') + { + found = true; + } + } + } + } + + if(result < 0) + { + throw new MidiFileException("Unable to find track in MIDI file."); + } + } + } + + private int GetTrackLength() + { + byte[] trackLength = new byte[4]; + + int result = stream.Read(trackLength, 0, trackLength.Length); + + if(result < trackLength.Length) + { + throw new MidiFileException("End of MIDI file unexpectedly reached."); + } + + if(BitConverter.IsLittleEndian) + { + Array.Reverse(trackLength); + } + + return BitConverter.ToInt32(trackLength, 0); + } + + private void ParseTrackData() + { + trackIndex = ticks = runningStatus = 0; + + while(trackIndex < trackData.Length) + { + previousTicks = ticks; + + ticks += ReadVariableLengthValue(); + + if((trackData[trackIndex] & 0x80) == 0x80) + { + status = trackData[trackIndex]; + trackIndex++; + } + else + { + status = runningStatus; + } + + ParseMessage(); + } + } + + private void ParseMessage() + { + // If this is a channel message. + if(status >= (int)ChannelCommand.NoteOff && + status <= (int)ChannelCommand.PitchWheel + + ChannelMessage.MidiChannelMaxValue) + { + ParseChannelMessage(); + } + // Else if this is a meta message. + else if(status == 0xFF) + { + ParseMetaMessage(); + } + // Else if this is the start of a system exclusive message. + else if(status == (int)SysExType.Start) + { + ParseSysExMessageStart(); + } + // Else if this is a continuation of a system exclusive message. + else if(status == (int)SysExType.Continuation) + { + ParseSysExMessageContinue(); + } + // Else if this is a system common message. + else if(status >= (int)SysCommonType.MidiTimeCode && + status <= (int)SysCommonType.TuneRequest) + { + ParseSysCommonMessage(); + } + // Else if this is a system realtime message. + else if(status >= (int)SysRealtimeType.Clock && + status <= (int)SysRealtimeType.Reset) + { + ParseSysRealtimeMessage(); + } + } + + private void ParseChannelMessage() + { + if(trackIndex >= trackData.Length) + { + throw new MidiFileException("End of track unexpectedly reached."); + } + + cmBuilder.Command = ChannelMessage.UnpackCommand(status); + cmBuilder.MidiChannel = ChannelMessage.UnpackMidiChannel(status); + cmBuilder.Data1 = trackData[trackIndex]; + + trackIndex++; + + if(ChannelMessage.DataBytesPerType(cmBuilder.Command) == 2) + { + if(trackIndex >= trackData.Length) + { + throw new MidiFileException("End of track unexpectedly reached."); + } + + cmBuilder.Data2 = trackData[trackIndex]; + + trackIndex++; + } + + cmBuilder.Build(); + newTrack.Insert(ticks, cmBuilder.Result); + runningStatus = status; + } + + private void ParseMetaMessage() + { + if(trackIndex >= trackData.Length) + { + throw new MidiFileException("End of track unexpectedly reached."); + } + + MetaType type = (MetaType)trackData[trackIndex]; + + trackIndex++; + + if(trackIndex >= trackData.Length) + { + throw new MidiFileException("End of track unexpectedly reached."); + } + + if(type == MetaType.EndOfTrack) + { + newTrack.EndOfTrackOffset = ticks - previousTicks; + + trackIndex++; + } + else + { + byte[] data = new byte[ReadVariableLengthValue()]; + Array.Copy(trackData, trackIndex, data, 0, data.Length); + newTrack.Insert(ticks, new MetaMessage(type, data)); + + trackIndex += data.Length; + } + } + + private void ParseSysExMessageStart() + { + // System exclusive cancels running status. + runningStatus = 0; + + byte[] data = new byte[ReadVariableLengthValue() + 1]; + data[0] = (byte)SysExType.Start; + + Array.Copy(trackData, trackIndex, data, 1, data.Length - 1); + newTrack.Insert(ticks, new SysExMessage(data)); + + trackIndex += data.Length - 1; + } + + private void ParseSysExMessageContinue() + { + trackIndex++; + + if(trackIndex >= trackData.Length) + { + throw new MidiFileException("End of track unexpectedly reached."); + } + + // System exclusive cancels running status. + runningStatus = 0; + + // If this is an escaped message rather than a system exclusive + // continuation message. + if((trackData[trackIndex] & 0x80) == 0x80) + { + status = trackData[trackIndex]; + trackIndex++; + + ParseMessage(); + } + else + { + byte[] data = new byte[ReadVariableLengthValue() + 1]; + data[0] = (byte)SysExType.Continuation; + + Array.Copy(trackData, trackIndex, data, 1, data.Length - 1); + newTrack.Insert(ticks, new SysExMessage(data)); + + trackIndex += data.Length - 1; + } + } + + private void ParseSysCommonMessage() + { + if(trackIndex >= trackData.Length) + { + throw new MidiFileException("End of track unexpectedly reached."); + } + + // System common cancels running status. + runningStatus = 0; + + scBuilder.Type = (SysCommonType)status; + + switch((SysCommonType)status) + { + case SysCommonType.MidiTimeCode: + scBuilder.Data1 = trackData[trackIndex]; + trackIndex++; + break; + + case SysCommonType.SongPositionPointer: + scBuilder.Data1 = trackData[trackIndex]; + trackIndex++; + + if(trackIndex >= trackData.Length) + { + throw new MidiFileException("End of track unexpectedly reached."); + } + + scBuilder.Data2 = trackData[trackIndex]; + trackIndex++; + break; + + case SysCommonType.SongSelect: + scBuilder.Data1 = trackData[trackIndex]; + trackIndex++; + break; + + case SysCommonType.TuneRequest: + // Nothing to do here. + break; + } + + scBuilder.Build(); + + newTrack.Insert(ticks, scBuilder.Result); + } + + private void ParseSysRealtimeMessage() + { + SysRealtimeMessage e = null; + + switch((SysRealtimeType)status) + { + case SysRealtimeType.ActiveSense: + e = SysRealtimeMessage.ActiveSenseMessage; + break; + + case SysRealtimeType.Clock: + e = SysRealtimeMessage.ClockMessage; + break; + + case SysRealtimeType.Continue: + e = SysRealtimeMessage.ContinueMessage; + break; + + case SysRealtimeType.Reset: + e = SysRealtimeMessage.ResetMessage; + break; + + case SysRealtimeType.Start: + e = SysRealtimeMessage.StartMessage; + break; + + case SysRealtimeType.Stop: + e = SysRealtimeMessage.StopMessage; + break; + + case SysRealtimeType.Tick: + e = SysRealtimeMessage.TickMessage; + break; + } + + newTrack.Insert(ticks, e); + } + + private int ReadVariableLengthValue() + { + if(trackIndex >= trackData.Length) + { + throw new MidiFileException("End of track unexpectedly reached."); + } + + int result = 0; + + result = trackData[trackIndex]; + + trackIndex++; + + if((result & 0x80) == 0x80) + { + result &= 0x7F; + + int temp; + + do + { + if(trackIndex >= trackData.Length) + { + throw new MidiFileException("End of track unexpectedly reached."); + } + + temp = trackData[trackIndex]; + trackIndex++; + result <<= 7; + result |= temp & 0x7F; + }while((temp & 0x80) == 0x80); + } + + return result; + } + + public Track Track + { + get + { + return track; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/TrackWriter.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/TrackWriter.cs new file mode 100644 index 00000000..470a90bf --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/TrackWriter.cs @@ -0,0 +1,256 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Sanford.Multimedia.Midi +{ + /// + /// Writes a Track to a Stream. + /// + internal class TrackWriter + { + private static readonly byte[] TrackHeader = + { + (byte)'M', + (byte)'T', + (byte)'r', + (byte)'k' + }; + + // The Track to write to the Stream. + private Track track = new Track(); + + // The Stream to write to. + private Stream stream; + + // Running status. + private int runningStatus = 0; + + // The Track data in raw bytes. + private List trackData = new List(); + + public void Write(Stream strm) + { + this.stream = strm; + + trackData.Clear(); + + stream.Write(TrackHeader, 0, TrackHeader.Length); + + foreach(MidiEvent e in track.Iterator()) + { + WriteVariableLengthValue(e.DeltaTicks); + + switch(e.MidiMessage.MessageType) + { + case MessageType.Channel: + Write((ChannelMessage)e.MidiMessage); + break; + + case MessageType.SystemExclusive: + Write((SysExMessage)e.MidiMessage); + break; + + case MessageType.Meta: + Write((MetaMessage)e.MidiMessage); + break; + + case MessageType.SystemCommon: + Write((SysCommonMessage)e.MidiMessage); + break; + + case MessageType.SystemRealtime: + Write((SysRealtimeMessage)e.MidiMessage); + break; + + case MessageType.Short: + Write((ShortMessage)e.MidiMessage); + break; + } + } + + byte[] trackLength = BitConverter.GetBytes(trackData.Count); + + if(BitConverter.IsLittleEndian) + { + Array.Reverse(trackLength); + } + + stream.Write(trackLength, 0, trackLength.Length); + + foreach(byte b in trackData) + { + stream.WriteByte(b); + } + } + + private void WriteVariableLengthValue(int value) + { + int v = value; + byte[] array = new byte[4]; + int count = 0; + + array[0] = (byte)(v & 0x7F); + + v >>= 7; + + while(v > 0) + { + count++; + array[count] = (byte)((v & 0x7F) | 0x80); + v >>= 7; + } + + while(count >= 0) + { + trackData.Add(array[count]); + count--; + } + } + + private void Write(ShortMessage message) + { + trackData.AddRange(message.GetBytes()); + } + + private void Write(ChannelMessage message) + { + if(runningStatus != message.Status) + { + trackData.Add((byte)message.Status); + runningStatus = message.Status; + } + + trackData.Add((byte)message.Data1); + + if(ChannelMessage.DataBytesPerType(message.Command) == 2) + { + trackData.Add((byte)message.Data2); + } + } + + private void Write(SysExMessage message) + { + // System exclusive message cancel running status. + runningStatus = 0; + + trackData.Add((byte)message.Status); + + WriteVariableLengthValue(message.Length - 1); + + for(int i = 1; i < message.Length; i++) + { + trackData.Add(message[i]); + } + } + + private void Write(MetaMessage message) + { + trackData.Add((byte)message.Status); + trackData.Add((byte)message.MetaType); + + WriteVariableLengthValue(message.Length); + + trackData.AddRange(message.GetBytes()); + } + + private void Write(SysCommonMessage message) + { + // Escaped messages cancel running status. + runningStatus = 0; + + // Escaped message. + trackData.Add((byte)0xF7); + + trackData.Add((byte)message.Status); + + switch(message.SysCommonType) + { + case SysCommonType.MidiTimeCode: + trackData.Add((byte)message.Data1); + break; + + case SysCommonType.SongPositionPointer: + trackData.Add((byte)message.Data1); + trackData.Add((byte)message.Data2); + break; + + case SysCommonType.SongSelect: + trackData.Add((byte)message.Data1); + break; + } + } + + private void Write(SysRealtimeMessage message) + { + // Escaped messages cancel running status. + runningStatus = 0; + + // Escaped message. + trackData.Add((byte)0xF7); + + trackData.Add((byte)message.Status); + } + + /// + /// Gets or sets the Track to write to the Stream. + /// + public Track Track + { + get + { + return track; + } + set + { + #region Require + + if(value == null) + { + throw new ArgumentNullException("Track"); + } + + #endregion + + runningStatus = 0; + trackData.Clear(); + + track = value; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ITimer.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ITimer.cs new file mode 100644 index 00000000..5ab564f1 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ITimer.cs @@ -0,0 +1,95 @@ +#region License + +/* Copyright (c) 2015 Andreas Grimme + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +using System; +using System.ComponentModel; + +namespace Sanford.Multimedia.Timers +{ + public interface ITimer : IComponent + { + /// + /// Gets a value indicating whether the Timer is running. + /// + bool IsRunning { get; } + + /// + /// Gets the timer mode. + /// + /// + /// If the timer has already been disposed. + /// + TimerMode Mode { get; set; } + + /// + /// Period between timer events in milliseconds. + /// + int Period { get; set; } + + /// + /// Resolution of the timer in milliseconds. + /// + int Resolution { get; set; } + + /// + /// Gets or sets the object used to marshal event-handler calls. + /// + ISynchronizeInvoke SynchronizingObject { get; set; } + + /// + /// Occurs when the Timer has started; + /// + event EventHandler Started; + + /// + /// Occurs when the Timer has stopped; + /// + event EventHandler Stopped; + + /// + /// Occurs when the time period has elapsed. + /// + event EventHandler Tick; + + /// + /// Starts the timer. + /// + /// + /// The timer has already been disposed. + /// + /// + /// The timer failed to start. + /// + void Start(); + + /// + /// Stops timer. + /// + /// + /// If the timer has already been disposed. + /// + void Stop(); + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ThreadTimer.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ThreadTimer.cs new file mode 100644 index 00000000..8545682d --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ThreadTimer.cs @@ -0,0 +1,387 @@ +#region License + +/* Copyright (c) 2015 Andreas Grimme + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Text; +using System.Threading; + +namespace Sanford.Multimedia.Timers +{ + /// + /// Replacement for the Windows multimedia timer that also runs on Mono + /// + sealed class ThreadTimer : ITimer + { + ThreadTimerQueue queue; + + bool isRunning; + TimerMode mode; + TimeSpan period; + TimeSpan resolution; + + static object[] emptyArgs = new object[] { EventArgs.Empty }; + + public ThreadTimer() + : this(ThreadTimerQueue.Instance) + { + if (!Stopwatch.IsHighResolution) + { + throw new NotImplementedException("Stopwatch is not IsHighResolution"); + } + + isRunning = false; + mode = TimerMode.Periodic; + resolution = TimeSpan.FromMilliseconds(1); + period = resolution; + + tickRaiser = new EventRaiser(OnTick); + } + + ThreadTimer(ThreadTimerQueue queue) + { + this.queue = queue; + } + + internal void DoTick() + { + if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) + { + SynchronizingObject.BeginInvoke(tickRaiser, emptyArgs); + } + else + { + OnTick(EventArgs.Empty); + } + } + + // Represents methods that raise events. + private delegate void EventRaiser(EventArgs e); + + // Represents the method that raises the Tick event. + private EventRaiser tickRaiser; + + // The ISynchronizeInvoke object to use for marshaling events. + private ISynchronizeInvoke synchronizingObject = null; + + public bool IsRunning + { + get + { + return isRunning; + } + } + + public TimerMode Mode + { + get + { + #region Require + + if (disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + return mode; + } + + set + { + #region Require + + if (disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + mode = value; + + if (IsRunning) + { + Stop(); + Start(); + } + } + } + + public int Period + { + get + { + #region Require + + if (disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + return (int) period.TotalMilliseconds; + } + set + { + #region Require + + if (disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + var wasRunning = IsRunning; + + if (wasRunning) + { + Stop(); + } + + period = TimeSpan.FromMilliseconds(value); + + if (wasRunning) + { + Start(); + } + } + } + + public TimeSpan PeriodTimeSpan + { + get { return period; } + } + + public int Resolution + { + get + { + return (int) resolution.TotalMilliseconds; + } + + set + { + resolution = TimeSpan.FromMilliseconds(value); + } + } + + // For implementing IComponent. + private ISite site = null; + + public ISite Site + { + get + { + return site; + } + + set + { + site = value; + } + } + + /// + /// Gets or sets the object used to marshal event-handler calls. + /// + public ISynchronizeInvoke SynchronizingObject + { + get + { + #region Require + + if (disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + return synchronizingObject; + } + set + { + #region Require + + if (disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + synchronizingObject = value; + } + } + + public event EventHandler Disposed; + public event EventHandler Started; + public event EventHandler Stopped; + public event EventHandler Tick; + + public void Dispose() + { + Stop(); + disposed = true; + OnDisposed(EventArgs.Empty); + } + + #region Event Raiser Methods + + // Raises the Disposed event. + private void OnDisposed(EventArgs e) + { + EventHandler handler = Disposed; + + if (handler != null) + { + handler(this, e); + } + } + + // Raises the Started event. + private void OnStarted(EventArgs e) + { + EventHandler handler = Started; + + if (handler != null) + { + handler(this, e); + } + } + + // Raises the Stopped event. + private void OnStopped(EventArgs e) + { + EventHandler handler = Stopped; + + if (handler != null) + { + handler(this, e); + } + } + + // Raises the Tick event. + private void OnTick(EventArgs e) + { + EventHandler handler = Tick; + + if (handler != null) + { + handler(this, e); + } + } + + #endregion + + bool disposed = false; + + public void Start() + { + #region Require + + if (disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + #region Guard + + if (IsRunning) + { + return; + } + + #endregion + + // If the periodic event callback should be used. + if (Mode == TimerMode.Periodic) + { + queue.Add(this); + isRunning = true; + } + // Else the one shot event callback should be used. + else + { + throw new NotImplementedException(); + } + + if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) + { + SynchronizingObject.BeginInvoke( + new EventRaiser(OnStarted), + new object[] { EventArgs.Empty }); + } + else + { + OnStarted(EventArgs.Empty); + } + } + + public void Stop() + { + #region Require + + if (disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + #region Guard + + if (!IsRunning) + { + return; + } + + #endregion + + queue.Remove(this); + isRunning = false; + + if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) + { + SynchronizingObject.BeginInvoke( + new EventRaiser(OnStopped), + new object[] { EventArgs.Empty }); + } + else + { + OnStopped(EventArgs.Empty); + } + } + + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ThreadTimerQueue.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ThreadTimerQueue.cs new file mode 100644 index 00000000..0956a6f1 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ThreadTimerQueue.cs @@ -0,0 +1,175 @@ +#region License + +/* Copyright (c) 2015 Andreas Grimme + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading; + +namespace Sanford.Multimedia.Timers +{ + /// + /// Queues and executes timer events in an internal worker thread. + /// + class ThreadTimerQueue + { + Stopwatch watch = Stopwatch.StartNew(); + Thread loop; + List tickQueue = new List(); + + public static ThreadTimerQueue Instance + { + get + { + if (instance == null) + { + instance = new ThreadTimerQueue(); + } + return instance; + + } + } + static ThreadTimerQueue instance; + + private ThreadTimerQueue() + { + } + + public void Add(ThreadTimer timer) + { + lock (this) + { + var tick = new Tick + { + Timer = timer, + Time = watch.Elapsed + }; + tickQueue.Add(tick); + tickQueue.Sort(); + + if (loop == null) + { + loop = new Thread(TimerLoop); + loop.Start(); + } + Monitor.PulseAll(this); + } + } + + public void Remove(ThreadTimer timer) + { + lock (this) + { + int i = 0; + for (; i < tickQueue.Count; ++i) + { + if (tickQueue[i].Timer == timer) + { + break; + } + } + if (i < tickQueue.Count) + { + tickQueue.RemoveAt(i); + } + Monitor.PulseAll(this); + } + } + + class Tick : IComparable + { + public ThreadTimer Timer; + public TimeSpan Time; + + public int CompareTo(object obj) + { + var r = obj as Tick; + if (r == null) + { + return -1; + } + return Time.CompareTo(r.Time); + } + } + + static TimeSpan Min(TimeSpan x0, TimeSpan x1) + { + if (x0 > x1) + { + return x1; + } + else + { + return x0; + } + } + + /// + /// The thread to execute the timer events + /// + private void TimerLoop() + { + lock (this) + { + TimeSpan maxTimeout = TimeSpan.FromMilliseconds(500); + + for (int queueEmptyCount = 0; queueEmptyCount < 3; ++queueEmptyCount) + { + var waitTime = maxTimeout; + if (tickQueue.Count > 0) + { + waitTime = Min(tickQueue[0].Time - watch.Elapsed, waitTime); + queueEmptyCount = 0; + } + + if (waitTime > TimeSpan.Zero) + { + Monitor.Wait(this, waitTime); + } + + if (tickQueue.Count > 0) + { + var tick = tickQueue[0]; + var mode = tick.Timer.Mode; + Monitor.Exit(this); + tick.Timer.DoTick(); + Monitor.Enter(this); + if (mode == TimerMode.Periodic) + { + tick.Time += tick.Timer.PeriodTimeSpan; + tickQueue.Sort(); + } + else + { + tickQueue.RemoveAt(0); + } + } + } + loop = null; + } + } + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Time.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Time.cs new file mode 100644 index 00000000..5621e7df --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Time.cs @@ -0,0 +1,109 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Runtime.InteropServices; + +namespace Sanford.Multimedia.Timers +{ + /// + /// Defines constants representing the timing format used by the Time struct. + /// + public enum TimeType + { + Milliseconds = 0x0001, + Samples = 0x0002, + Bytes = 0x0004, + Smpte = 0x0008, + Midi = 0x0010, + Ticks = 0x0020 + } + + /// + /// Represents the Windows Multimedia MMTIME structure. + /// + [StructLayout(LayoutKind.Explicit)] + public struct Time + { + [FieldOffset(0)] + public int type; + + [FieldOffset(4)] + public int milliseconds; + + [FieldOffset(4)] + public int samples; + + [FieldOffset(4)] + public int byteCount; + + [FieldOffset(4)] + public int ticks; + + // + // SMPTE + // + + [FieldOffset(4)] + public byte hours; + + [FieldOffset(5)] + public byte minutes; + + [FieldOffset(6)] + public byte seconds; + + [FieldOffset(7)] + public byte frames; + + [FieldOffset(8)] + public byte framesPerSecond; + + [FieldOffset(9)] + public byte dummy; + + [FieldOffset(10)] + public byte pad1; + + [FieldOffset(11)] + public byte pad2; + + // + // MIDI + // + + [FieldOffset(4)] + public int songPositionPointer; + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Timer.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Timer.cs new file mode 100644 index 00000000..fc148e97 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Timer.cs @@ -0,0 +1,719 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Sanford.Multimedia.Timers +{ + /// + /// Defines constants for the multimedia Timer's event types. + /// + public enum TimerMode + { + /// + /// Timer event occurs once. + /// + OneShot, + + /// + /// Timer event occurs periodically. + /// + Periodic + }; + + /// + /// Represents information about the multimedia Timer's capabilities. + /// + [StructLayout(LayoutKind.Sequential)] + public struct TimerCaps + { + /// + /// Minimum supported period in milliseconds. + /// + public int periodMin; + + /// + /// Maximum supported period in milliseconds. + /// + public int periodMax; + + public static TimerCaps Default + { + get + { + return new TimerCaps { periodMin = 1, periodMax = Int32.MaxValue }; + } + } + } + + /// + /// Represents the Windows multimedia timer. + /// + sealed class Timer : ITimer + { + #region Timer Members + + #region Delegates + + // Represents the method that is called by Windows when a timer event occurs. + private delegate void TimeProc(int id, int msg, int user, int param1, int param2); + + // Represents methods that raise events. + private delegate void EventRaiser(EventArgs e); + + #endregion + + #region Win32 Multimedia Timer Functions + + // Gets timer capabilities. + [DllImport("winmm.dll")] + private static extern int timeGetDevCaps(ref TimerCaps caps, + int sizeOfTimerCaps); + + // Creates and starts the timer. + [DllImport("winmm.dll")] + private static extern int timeSetEvent(int delay, int resolution, TimeProc proc, IntPtr user, int mode); + + // Stops and destroys the timer. + [DllImport("winmm.dll")] + private static extern int timeKillEvent(int id); + + // Indicates that the operation was successful. + private const int TIMERR_NOERROR = 0; + + #endregion + + #region Fields + + // Timer identifier. + private int timerID; + + // Timer mode. + private volatile TimerMode mode; + + // Period between timer events in milliseconds. + private volatile int period; + + // Timer resolution in milliseconds. + private volatile int resolution; + + // Called by Windows when a timer periodic event occurs. + private TimeProc timeProcPeriodic; + + // Called by Windows when a timer one shot event occurs. + private TimeProc timeProcOneShot; + + // Represents the method that raises the Tick event. + private EventRaiser tickRaiser; + + // The ISynchronizeInvoke object to use for marshaling events. + private ISynchronizeInvoke synchronizingObject = null; + + // Indicates whether or not the timer is running. + private bool running = false; + + // Indicates whether or not the timer has been disposed. + private volatile bool disposed = false; + + // For implementing IComponent. + private ISite site = null; + + // Multimedia timer capabilities. + private static TimerCaps caps; + + #endregion + + #region Events + + /// + /// Occurs when the Timer has started; + /// + public event EventHandler Started; + + /// + /// Occurs when the Timer has stopped; + /// + public event EventHandler Stopped; + + /// + /// Occurs when the time period has elapsed. + /// + public event EventHandler Tick; + + #endregion + + #region Construction + + /// + /// Initialize class. + /// + static Timer() + { + // Get multimedia timer capabilities. + timeGetDevCaps(ref caps, Marshal.SizeOf(caps)); + } + + /// + /// Initializes a new instance of the Timer class with the specified IContainer. + /// + /// + /// The IContainer to which the Timer will add itself. + /// + public Timer(IContainer container) + { + /// + /// Required for Windows.Forms Class Composition Designer support + /// + container.Add(this); + + Initialize(); + } + + /// + /// Initializes a new instance of the Timer class. + /// + public Timer() + { + Initialize(); + } + + ~Timer() + { + if(IsRunning) + { + // Stop and destroy timer. + timeKillEvent(timerID); + } + } + + // Initialize timer with default values. + private void Initialize() + { + this.mode = TimerMode.Periodic; + this.period = Capabilities.periodMin; + this.resolution = 1; + + running = false; + + timeProcPeriodic = new TimeProc(TimerPeriodicEventCallback); + timeProcOneShot = new TimeProc(TimerOneShotEventCallback); + tickRaiser = new EventRaiser(OnTick); + } + + #endregion + + #region Methods + + /// + /// Starts the timer. + /// + /// + /// The timer has already been disposed. + /// + /// + /// The timer failed to start. + /// + public void Start() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + #region Guard + + if(IsRunning) + { + return; + } + + #endregion + + // If the periodic event callback should be used. + if(Mode == TimerMode.Periodic) + { + // Create and start timer. + timerID = timeSetEvent(Period, Resolution, timeProcPeriodic, IntPtr.Zero, (int)Mode); + } + // Else the one shot event callback should be used. + else + { + // Create and start timer. + timerID = timeSetEvent(Period, Resolution, timeProcOneShot, IntPtr.Zero, (int)Mode); + } + + // If the timer was created successfully. + if(timerID != 0) + { + running = true; + + if(SynchronizingObject != null && SynchronizingObject.InvokeRequired) + { + SynchronizingObject.BeginInvoke( + new EventRaiser(OnStarted), + new object[] { EventArgs.Empty }); + } + else + { + OnStarted(EventArgs.Empty); + } + } + else + { + throw new TimerStartException("Unable to start multimedia Timer."); + } + } + + /// + /// Stops timer. + /// + /// + /// If the timer has already been disposed. + /// + public void Stop() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + #region Guard + + if(!running) + { + return; + } + + #endregion + + // Stop and destroy timer. + int result = timeKillEvent(timerID); + + Debug.Assert(result == TIMERR_NOERROR); + + running = false; + + if(SynchronizingObject != null && SynchronizingObject.InvokeRequired) + { + SynchronizingObject.BeginInvoke( + new EventRaiser(OnStopped), + new object[] { EventArgs.Empty }); + } + else + { + OnStopped(EventArgs.Empty); + } + } + + #region Callbacks + + // Callback method called by the Win32 multimedia timer when a timer + // periodic event occurs. + private void TimerPeriodicEventCallback(int id, int msg, int user, int param1, int param2) + { + #region Guard + + if(disposed) + { + return; + } + + #endregion + + if(synchronizingObject != null) + { + synchronizingObject.BeginInvoke(tickRaiser, new object[] { EventArgs.Empty }); + } + else + { + OnTick(EventArgs.Empty); + } + } + + // Callback method called by the Win32 multimedia timer when a timer + // one shot event occurs. + private void TimerOneShotEventCallback(int id, int msg, int user, int param1, int param2) + { + #region Guard + + if(disposed) + { + return; + } + + #endregion + + if(synchronizingObject != null) + { + synchronizingObject.BeginInvoke(tickRaiser, new object[] { EventArgs.Empty }); + Stop(); + } + else + { + OnTick(EventArgs.Empty); + Stop(); + } + } + + #endregion + + #region Event Raiser Methods + + // Raises the Disposed event. + private void OnDisposed(EventArgs e) + { + EventHandler handler = Disposed; + + if(handler != null) + { + handler(this, e); + } + } + + // Raises the Started event. + private void OnStarted(EventArgs e) + { + EventHandler handler = Started; + + if(handler != null) + { + handler(this, e); + } + } + + // Raises the Stopped event. + private void OnStopped(EventArgs e) + { + EventHandler handler = Stopped; + + if(handler != null) + { + handler(this, e); + } + } + + // Raises the Tick event. + private void OnTick(EventArgs e) + { + EventHandler handler = Tick; + + if(handler != null) + { + handler(this, e); + } + } + + #endregion + + #endregion + + #region Properties + + /// + /// Gets or sets the object used to marshal event-handler calls. + /// + public ISynchronizeInvoke SynchronizingObject + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + return synchronizingObject; + } + set + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + synchronizingObject = value; + } + } + + /// + /// Gets or sets the time between Tick events. + /// + /// + /// If the timer has already been disposed. + /// + public int Period + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + return period; + } + set + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + else if(value < Capabilities.periodMin || value > Capabilities.periodMax) + { + throw new ArgumentOutOfRangeException("Period", value, + "Multimedia Timer period out of range."); + } + + #endregion + + period = value; + + if(IsRunning) + { + Stop(); + Start(); + } + } + } + + /// + /// Gets or sets the timer resolution. + /// + /// + /// If the timer has already been disposed. + /// + /// + /// The resolution is in milliseconds. The resolution increases + /// with smaller values; a resolution of 0 indicates periodic events + /// should occur with the greatest possible accuracy. To reduce system + /// overhead, however, you should use the maximum value appropriate + /// for your application. + /// + public int Resolution + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + return resolution; + } + set + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + else if(value < 0) + { + throw new ArgumentOutOfRangeException("Resolution", value, + "Multimedia timer resolution out of range."); + } + + #endregion + + resolution = value; + + if(IsRunning) + { + Stop(); + Start(); + } + } + } + + /// + /// Gets the timer mode. + /// + /// + /// If the timer has already been disposed. + /// + public TimerMode Mode + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + return mode; + } + set + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("Timer"); + } + + #endregion + + mode = value; + + if(IsRunning) + { + Stop(); + Start(); + } + } + } + + /// + /// Gets a value indicating whether the Timer is running. + /// + public bool IsRunning + { + get + { + return running; + } + } + + /// + /// Gets the timer capabilities. + /// + public static TimerCaps Capabilities + { + get + { + return caps; + } + } + + #endregion + + #endregion + + #region IComponent Members + + public event System.EventHandler Disposed; + + public ISite Site + { + get + { + return site; + } + set + { + site = value; + } + } + + #endregion + + #region IDisposable Members + + /// + /// Frees timer resources. + /// + public void Dispose() + { + #region Guard + + if(disposed) + { + return; + } + + #endregion + + disposed = true; + + if(running) + { + // Stop and destroy timer. + timeKillEvent(timerID); + } + + OnDisposed(EventArgs.Empty); + } + + #endregion + } + + /// + /// The exception that is thrown when a timer fails to start. + /// + public class TimerStartException : ApplicationException + { + /// + /// Initializes a new instance of the TimerStartException class. + /// + /// + /// The error message that explains the reason for the exception. + /// + public TimerStartException(string message) : base(message) + { + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/TimerFactory.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/TimerFactory.cs new file mode 100644 index 00000000..b7c617f9 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/TimerFactory.cs @@ -0,0 +1,58 @@ +#region License + +/* Copyright (c) 2015 Andreas Grimme + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +using Sanford.Multimedia.Timers; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia.Timers +{ + /// + /// Use this factory to create ITimer instances. + /// + /// Caller is responsible for Dispose. + public static class TimerFactory + { + static bool IsRunningOnMono() + { + return Type.GetType("Mono.Runtime") != null; + } + + /// + /// Creates an instance of ITimer + /// + /// Newly created instance of ITimer + public static ITimer Create() + { + if (IsRunningOnMono()) + { + return new ThreadTimer(); + } + + return new Timer(); + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Device.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Device.cs new file mode 100644 index 00000000..85929b27 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Device.cs @@ -0,0 +1,166 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Sanford.Multimedia +{ + /// + /// Defines the public abstract 'Device' class to interface System.IDisposable. + /// + public abstract class Device : IDisposable + { + /// + /// This protected construct, uses a Callback Function integer if it's equal to value 0x30000. + /// + protected const int CALLBACK_FUNCTION = 0x30000; + + /// + /// This protected construct, uses a Callback Event integer if it's equal to value 0x50000. + /// + protected const int CALLBACK_EVENT = 0x50000; + + private int deviceID; + + /// + /// Synchronizes the context. + /// + protected SynchronizationContext context; + + // Indicates whether the device has been disposed. + private bool disposed = false; + + /// + /// Outputs an error via ErrorEventArgs, if the EventHandler encounters an issue. + /// + public event EventHandler Error; + + /// + /// This public function utilises the Device ID integer with SynchronizationContext. + /// + public Device(int deviceID) + { + this.deviceID = deviceID; + + if(SynchronizationContext.Current == null) + { + context = new SynchronizationContext(); + } + else + { + context = SynchronizationContext.Current; + } + } + + /// + /// Utilises system garbage collector (System.GC) to dispose memory when the boolean value is set to true. + /// + protected virtual void Dispose(bool disposing) + { + if(disposing) + { + disposed = true; + + GC.SuppressFinalize(this); + } + } + + /// + /// Error handling function. + /// + protected virtual void OnError(ErrorEventArgs e) + { + EventHandler handler = Error; + + if(handler != null) + { + context.Post(delegate(object dummy) + { + handler(this, e); + }, null); + } + } + + /// + /// Closes the MIDI device. + /// + public abstract void Close(); + + /// + /// Resets the device. + /// + public abstract void Reset(); + + /// + /// Gets the device handle. + /// + public abstract IntPtr Handle + { + get; + } + + /// + /// Calls the DeviceID public integer. + /// + public int DeviceID + { + get + { + return deviceID; + } + } + + /// + /// Declares the device as disposed. + /// + public bool IsDisposed + { + get + { + return disposed; + } + } + + #region IDisposable + + /// + /// Disposes of the device. + /// + public abstract void Dispose(); + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/DeviceException.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/DeviceException.cs new file mode 100644 index 00000000..e79bffc2 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/DeviceException.cs @@ -0,0 +1,117 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; + +namespace Sanford.Multimedia +{ + /// + /// Refers the System.ApplicationException as DeviceException. + /// + public abstract class DeviceException : ApplicationException + { + #region Error Codes + /// No error. + public const int MMSYSERR_NOERROR = 0; + /// Unspecified error. + public const int MMSYSERR_ERROR = 1; + /// Device ID out of range. + public const int MMSYSERR_BADDEVICEID = 2; + /// Driver failed enable. + public const int MMSYSERR_NOTENABLED = 3; + /// Device already allocated. + public const int MMSYSERR_ALLOCATED = 4; + /// Device handle is invalid. + public const int MMSYSERR_INVALHANDLE = 5; + /// No device driver present. + public const int MMSYSERR_NODRIVER = 6; + /// Memory allocation error. + public const int MMSYSERR_NOMEM = 7; + /// Function isn't supported. + public const int MMSYSERR_NOTSUPPORTED = 8; + /// Error value out of range. + public const int MMSYSERR_BADERRNUM = 9; + /// Invalid flag passed. + public const int MMSYSERR_INVALFLAG = 10; + /// Invalid parameter passed. + public const int MMSYSERR_INVALPARAM = 11; + /// + /// Handle being used.

+ /// Simultaneously on another.

+ /// Thread (eg callback).

+ ///
+ public const int MMSYSERR_HANDLEBUSY = 12; + /// Specified alias not found. + public const int MMSYSERR_INVALIDALIAS = 13; + /// Bad registry database. + public const int MMSYSERR_BADDB = 14; + /// Registry key not found. + public const int MMSYSERR_KEYNOTFOUND = 15; + /// Registry read error. + public const int MMSYSERR_READERROR = 16; + /// Registry write error. + public const int MMSYSERR_WRITEERROR = 17; + /// Registry delete error. + public const int MMSYSERR_DELETEERROR = 18; + /// Registry value not found. + public const int MMSYSERR_VALNOTFOUND = 19; + /// Driver does not call DriverCallback. + public const int MMSYSERR_NODRIVERCB = 20; + /// Last error. + public const int MMSYSERR_LASTERROR = 20; + + #endregion + + private int errorCode; + + /// + /// Calls the Device Exception error code. + /// + public DeviceException(int errorCode) + { + this.errorCode = errorCode; + } + + /// + /// Public integer for the error code. + /// + public int ErrorCode + { + get + { + return errorCode; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/ErrorEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/ErrorEventArgs.cs new file mode 100644 index 00000000..8a1f32cb --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/ErrorEventArgs.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sanford.Multimedia +{ + public class ErrorEventArgs : EventArgs + { + private Exception ex; + + public ErrorEventArgs(Exception ex) + { + this.ex = ex; + } + + public Exception Error + { + get + { + return ex; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Key.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Key.cs new file mode 100644 index 00000000..5e526296 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Key.cs @@ -0,0 +1,73 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +namespace Sanford.Multimedia +{ + /// + /// Defines constants for all major and minor keys. + /// + public enum Key + { + AFlatMinor, + EFlatMinor, + BFlatMinor, + FMinor, + CMinor, + GMinor, + DMinor, + AMinor, + EMinor, + BMinor, + FSharpMinor, + CSharpMinor, + GSharpMinor, + DSharpMinor, + ASharpMinor, + CFlatMajor, + GFlatMajor, + DFlatMajor, + AFlatMajor, + EFlatMajor, + BFlatMajor, + FMajor, + CMajor, + GMajor, + DMajor, + AMajor, + EMajor, + BMajor, + FSharpMajor, + CSharpMajor, + } +} \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Note.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Note.cs new file mode 100644 index 00000000..bfe32cfa --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Note.cs @@ -0,0 +1,127 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +namespace Sanford.Multimedia +{ + /// + /// Defines constants representing the 12 Note of the chromatic scale. + /// + public enum Note + { + /// + /// C natural. + /// + C, + + /// + /// C sharp. + /// + CSharp, + + /// + /// D flat. + /// + DFlat = CSharp, + + /// + /// D natural. + /// + D, + + /// + /// D sharp. + /// + DSharp, + + /// + /// E flat. + /// + EFlat = DSharp, + + /// + /// E natural. + /// + E, + + /// + /// F natural. + /// + F, + + /// + /// F sharp. + /// + FSharp, + + /// + /// G flat. + /// + GFlat = FSharp, + + /// + /// G natural. + /// + G, + + /// + /// G sharp. + /// + GSharp, + + /// + /// A flat. + /// + AFlat = GSharp, + + /// + /// A natural. + /// + A, + + /// + /// A sharp. + /// + ASharp, + + /// + /// B flat. + /// + BFlat = ASharp, + + /// + /// B natural. + /// + B + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/AsyncResult.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/AsyncResult.cs new file mode 100644 index 00000000..17e120df --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/AsyncResult.cs @@ -0,0 +1,177 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Threading; + +namespace Sanford.Threading +{ + /// + /// Provides basic implementation of the IAsyncResult interface. + /// + public class AsyncResult : IAsyncResult + { + #region AsyncResult Members + + #region Fields + + // The owner of this AsyncResult object. + private object owner; + + // The callback to be invoked when the operation completes. + private AsyncCallback callback; + + // User state information. + private object state; + + // For signaling when the operation has completed. + private ManualResetEvent waitHandle = new ManualResetEvent(false); + + // A value indicating whether the operation completed synchronously. + private bool completedSynchronously; + + // A value indicating whether the operation has completed. + private bool isCompleted = false; + + // The ID of the thread this AsyncResult object originated on. + private int threadId; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the AsyncResult object with the + /// specified owner of the AsyncResult object, the optional callback + /// delegate, and optional state object. + /// + /// + /// The owner of the AsyncResult object. + /// + /// + /// An optional asynchronous callback, to be called when the + /// operation is complete. + /// + /// + /// A user-provided object that distinguishes this particular + /// asynchronous request from other requests. + /// + public AsyncResult(object owner, AsyncCallback callback, object state) + { + this.owner = owner; + this.callback = callback; + this.state = state; + + // Get the current thread ID. This will be used later to determine + // if the operation completed synchronously. + threadId = Thread.CurrentThread.ManagedThreadId; + } + + #endregion + + #region Methods + + /// + /// Signals that the operation has completed. + /// + public void Signal() + { + isCompleted = true; + + completedSynchronously = threadId == Thread.CurrentThread.ManagedThreadId; + + waitHandle.Set(); + + if(callback != null) + { + callback(this); + } + } + + #endregion + + #region Properties + + /// + /// Gets the owner of this AsyncResult object. + /// + public object Owner + { + get + { + return owner; + } + } + + #endregion + + #endregion + + #region IAsyncResult Members + + public object AsyncState + { + get + { + return state; + } + } + + public WaitHandle AsyncWaitHandle + { + get + { + return waitHandle; + } + } + + public bool CompletedSynchronously + { + get + { + return completedSynchronously; + } + } + + public bool IsCompleted + { + get + { + return isCompleted; + } + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.AsyncResult.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.AsyncResult.cs new file mode 100644 index 00000000..7c0ca382 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.AsyncResult.cs @@ -0,0 +1,121 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace Sanford.Threading +{ + public partial class DelegateQueue + { + private enum NotificationType + { + None, + BeginInvokeCompleted, + PostCompleted + } + + /// + /// Implements the IAsyncResult interface for the DelegateQueue class. + /// + private class DelegateQueueAsyncResult : AsyncResult + { + // The delegate to be invoked. + private Delegate method; + + // Args to be passed to the delegate. + private object[] args; + + // The object returned from the delegate. + private object returnValue = null; + + // Represents a possible exception thrown by invoking the method. + private Exception error = null; + + private NotificationType notificationType; + + public DelegateQueueAsyncResult( + object owner, + Delegate method, + object[] args, + bool synchronously, + NotificationType notificationType) + : base(owner, null, null) + { + this.method = method; + this.args = args; + this.notificationType = notificationType; + } + + public DelegateQueueAsyncResult( + object owner, + AsyncCallback callback, + object state, + Delegate method, + object[] args, + bool synchronously, + NotificationType notificationType) + : base(owner, callback, state) + { + this.method = method; + this.args = args; + this.notificationType = notificationType; + } + + public void Invoke() + { + try + { + returnValue = method.DynamicInvoke(args); + } + catch(Exception ex) + { + error = ex; + } + finally + { + Signal(); + } + } + + public object[] GetArgs() + { + return args; + } + + public object ReturnValue + { + get + { + return returnValue; + } + } + + public Exception Error + { + get + { + return error; + } + set + { + error = value; + } + } + + public Delegate Method + { + get + { + return method; + } + } + + public NotificationType NotificationType + { + get + { + return notificationType; + } + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.cs new file mode 100644 index 00000000..8c51bf27 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.cs @@ -0,0 +1,843 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; +using System.Threading; +using Sanford.Collections.Generic; + +namespace Sanford.Threading +{ + /// + /// Represents an asynchronous queue of delegates. + /// + public partial class DelegateQueue : SynchronizationContext, IComponent, ISynchronizeInvoke + { + #region DelegateQueue Members + + #region Fields + + // The thread for processing delegates. + private Thread delegateThread; + + // The deque for holding delegates. + private Deque delegateDeque = new Deque(); + + // The object to use for locking. + private readonly object lockObject = new object(); + + // The synchronization context in which this DelegateQueue was created. + private SynchronizationContext context; + + // Inidicates whether the delegate queue has been disposed. + private volatile bool disposed = false; + + // Thread ID counter for all DelegateQueues. + private volatile static uint threadID = 0; + + private ISite site = null; + + #endregion + + #region Events + + /// + /// Occurs after a method has been invoked as a result of a call to + /// the BeginInvoke or BeginInvokePriority methods. + /// + public event EventHandler InvokeCompleted; + + /// + /// Occurs after a method has been invoked as a result of a call to + /// the Post and PostPriority methods. + /// + public event EventHandler PostCompleted; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the DelegateQueue class. + /// + public DelegateQueue() + { + InitializeDelegateQueue(); + + if(SynchronizationContext.Current == null) + { + context = new SynchronizationContext(); + } + else + { + context = SynchronizationContext.Current; + } + } + + /// + /// Initializes a new instance of the DelegateQueue class with the specified IContainer object. + /// + /// + /// The IContainer to which the DelegateQueue will add itself. + /// + public DelegateQueue(IContainer container) + { + /// + /// Required for Windows.Forms Class Composition Designer support + /// + container.Add(this); + + InitializeDelegateQueue(); + } + + ~DelegateQueue() + { + Dispose(false); + } + + // Initializes the DelegateQueue. + private void InitializeDelegateQueue() + { + // Create thread for processing delegates. + delegateThread = new Thread(DelegateProcedure); + + lock(lockObject) + { + // Increment to next thread ID. + threadID++; + + // Create name for thread. + delegateThread.Name = "Delegate Queue Thread: " + threadID.ToString(); + + // Start thread. + delegateThread.Start(); + + Debug.WriteLine(delegateThread.Name + " Started."); + + // Wait for signal from thread that it is running. + Monitor.Wait(lockObject); + } + } + + #endregion + + #region Methods + + protected virtual void Dispose(bool disposing) + { + if(disposing) + { + lock(lockObject) + { + disposed = true; + + Monitor.Pulse(lockObject); + + GC.SuppressFinalize(this); + } + } + } + + /// + /// Executes the delegate on the main thread that this object executes on. + /// + /// + /// A Delegate to a method that takes parameters of the same number and + /// type that are contained in args. + /// + /// + /// An array of type Object to pass as arguments to the given method. + /// + /// + /// An IAsyncResult interface that represents the asynchronous operation + /// started by calling this method. + /// + /// + /// The delegate is placed at the beginning of the queue. Its invocation + /// takes priority over delegates already in the queue. + /// + public IAsyncResult BeginInvokePriority(Delegate method, params object[] args) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateQueue"); + } + else if(method == null) + { + throw new ArgumentNullException(); + } + + #endregion + + DelegateQueueAsyncResult result; + + // If BeginInvokePriority was called from a different thread than the one + // in which the DelegateQueue is running. + if(InvokeRequired) + { + result = new DelegateQueueAsyncResult(this, method, args, false, NotificationType.BeginInvokeCompleted); + + lock(lockObject) + { + // Put the method at the front of the queue. + delegateDeque.PushFront(result); + + Monitor.Pulse(lockObject); + } + } + // Else BeginInvokePriority was called from the same thread in which the + // DelegateQueue is running. + else + { + result = new DelegateQueueAsyncResult(this, method, args, true, NotificationType.None); + + // The method is invoked here instead of placing it in the + // queue. The reason for this is that if EndInvoke is called + // from the same thread in which the DelegateQueue is running and + // the method has not been invoked, deadlock will occur. + result.Invoke(); + } + + return result; + } + + /// + /// Executes the delegate on the main thread that this object executes on. + /// + /// + /// A Delegate to a method that takes parameters of the same number and + /// type that are contained in args. + /// + /// + /// An array of type Object to pass as arguments to the given method. + /// + /// + /// An IAsyncResult interface that represents the asynchronous operation + /// started by calling this method. + /// + /// + /// + /// The delegate is placed at the beginning of the queue. Its invocation + /// takes priority over delegates already in the queue. + /// + /// + /// Unlike BeginInvoke, this method operates synchronously, that is, it + /// waits until the process completes before returning. Exceptions raised + /// during the call are propagated back to the caller. + /// + /// + public object InvokePriority(Delegate method, params object[] args) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateQueue"); + } + else if(method == null) + { + throw new ArgumentNullException(); + } + + #endregion + + object returnValue = null; + + // If InvokePriority was called from a different thread than the one + // in which the DelegateQueue is running. + if(InvokeRequired) + { + DelegateQueueAsyncResult result = new DelegateQueueAsyncResult(this, method, args, false, NotificationType.None); + + lock(lockObject) + { + // Put the method at the back of the queue. + delegateDeque.PushFront(result); + + Monitor.Pulse(lockObject); + } + + // Wait for the result of the method invocation. + returnValue = EndInvoke(result); + } + // Else InvokePriority was called from the same thread in which the + // DelegateQueue is running. + else + { + // Invoke the method here rather than placing it in the queue. + returnValue = method.DynamicInvoke(args); + } + + return returnValue; + } + + /// + /// Executes the delegate on the main thread that this object executes on. + /// + /// + /// An optional asynchronous callback, to be called when the method is invoked. + /// + /// + /// A user-provided object that distinguishes this particular asynchronous invoke request from other requests. + /// + /// + /// A Delegate to a method that takes parameters of the same number and + /// type that are contained in args. + /// + /// + /// An array of type Object to pass as arguments to the given method. + /// + /// + /// An IAsyncResult interface that represents the asynchronous operation + /// started by calling this method. + /// + public IAsyncResult BeginInvoke(AsyncCallback callback, object state, Delegate method, params object[] args) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateQueue"); + } + else if(method == null) + { + throw new ArgumentNullException(); + } + + #endregion + + DelegateQueueAsyncResult result; + + if(InvokeRequired) + { + result = new DelegateQueueAsyncResult(this, callback, state, method, args, false, NotificationType.BeginInvokeCompleted); + + lock(lockObject) + { + delegateDeque.PushBack(result); + + Monitor.Pulse(lockObject); + } + } + else + { + result = new DelegateQueueAsyncResult(this, callback, state, method, args, false, NotificationType.None); + + result.Invoke(); + } + + return result; + } + + /// + /// Dispatches an asynchronous message to this synchronization context. + /// + /// + /// The SendOrPostCallback delegate to call. + /// + /// + /// The object passed to the delegate. + /// + /// + /// The Post method starts an asynchronous request to post a message. + /// + public void PostPriority(SendOrPostCallback d, object state) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateQueue"); + } + else if(d == null) + { + throw new ArgumentNullException(); + } + + #endregion + + lock(lockObject) + { + DelegateQueueAsyncResult result = new DelegateQueueAsyncResult(this, d, new object[] { state }, false, NotificationType.PostCompleted); + + // Put the method at the front of the queue. + delegateDeque.PushFront(result); + + Monitor.Pulse(lockObject); + } + } + + /// + /// Dispatches an synchronous message to this synchronization context. + /// + /// + /// The SendOrPostCallback delegate to call. + /// + /// + /// The object passed to the delegate. + /// + public void SendPriority(SendOrPostCallback d, object state) + { + InvokePriority(d, state); + } + + // Processes and invokes delegates. + private void DelegateProcedure() + { + lock(lockObject) + { + // Signal the constructor that the thread is now running. + Monitor.Pulse(lockObject); + } + + // Set this DelegateQueue as the SynchronizationContext for this thread. + SynchronizationContext.SetSynchronizationContext(this); + + // Placeholder for DelegateQueueAsyncResult objects. + DelegateQueueAsyncResult result = null; + + // While the DelegateQueue has not been disposed. + while(true) + { + // Critical section. + lock(lockObject) + { + // If the DelegateQueue has been disposed, break out of loop; we're done. + if(disposed) + { + break; + } + + // If there are delegates waiting to be invoked. + if(delegateDeque.Count > 0) + { + result = delegateDeque.PopFront(); + } + // Else there are no delegates waiting to be invoked. + else + { + // Wait for next delegate. + Monitor.Wait(lockObject); + + // If the DelegateQueue has been disposed, break out of loop; we're done. + if(disposed) + { + break; + } + + Debug.Assert(delegateDeque.Count > 0); + + result = delegateDeque.PopFront(); + } + } + + Debug.Assert(result != null); + + // Invoke the delegate. + result.Invoke(); + + if(result.NotificationType == NotificationType.BeginInvokeCompleted) + { + InvokeCompletedEventArgs e = new InvokeCompletedEventArgs( + result.Method, + result.GetArgs(), + result.ReturnValue, + result.Error); + + OnInvokeCompleted(e); + } + else if(result.NotificationType == NotificationType.PostCompleted) + { + object[] args = result.GetArgs(); + + Debug.Assert(args.Length == 1); + Debug.Assert(result.Method is SendOrPostCallback); + + PostCompletedEventArgs e = new PostCompletedEventArgs( + (SendOrPostCallback)result.Method, + result.Error, + args[0]); + + OnPostCompleted(e); + } + else + { + Debug.Assert(result.NotificationType == NotificationType.None); + } + } + + Debug.WriteLine(delegateThread.Name + " Finished"); + } + + // Raises the InvokeCompleted event. + protected virtual void OnInvokeCompleted(InvokeCompletedEventArgs e) + { + EventHandler handler = InvokeCompleted; + + if(handler != null) + { + context.Post(delegate(object state) + { + handler(this, e); + }, null); + } + } + + // Raises the PostCompleted event. + protected virtual void OnPostCompleted(PostCompletedEventArgs e) + { + EventHandler handler = PostCompleted; + + if(handler != null) + { + context.Post(delegate(object state) + { + handler(this, e); + }, null); + } + } + + // Raises the Disposed event. + protected virtual void OnDisposed(EventArgs e) + { + EventHandler handler = Disposed; + + if(handler != null) + { + context.Post(delegate(object state) + { + handler(this, e); + }, null); + } + } + + #endregion + + #endregion + + #region SynchronizationContext Overrides + + /// + /// Dispatches a synchronous message to this synchronization context. + /// + /// + /// The SendOrPostCallback delegate to call. + /// + /// + /// The object passed to the delegate. + /// + /// + /// The Send method starts an synchronous request to send a message. + /// + public override void Send(SendOrPostCallback d, object state) + { + Invoke(d, state); + } + + /// + /// Dispatches an asynchronous message to this synchronization context. + /// + /// + /// The SendOrPostCallback delegate to call. + /// + /// + /// The object passed to the delegate. + /// + /// + /// The Post method starts an asynchronous request to post a message. + /// + public override void Post(SendOrPostCallback d, object state) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateQueue"); + } + else if(d == null) + { + throw new ArgumentNullException(); + } + + #endregion + + lock(lockObject) + { + delegateDeque.PushBack(new DelegateQueueAsyncResult(this, d, new object[] { state }, false, NotificationType.PostCompleted)); + + Monitor.Pulse(lockObject); + } + } + + #endregion + + #region IComponent Members + + /// + /// Represents the method that handles the Disposed delegate of a DelegateQueue. + /// + public event System.EventHandler Disposed; + + /// + /// Gets or sets the ISite associated with the DelegateQueue. + /// + public ISite Site + { + get + { + return site; + } + set + { + site = value; + } + } + + #endregion + + #region ISynchronizeInvoke Members + + /// + /// Executes the delegate on the main thread that this DelegateQueue executes on. + /// + /// + /// A Delegate to a method that takes parameters of the same number and type that + /// are contained in args. + /// + /// + /// An array of type Object to pass as arguments to the given method. This can be + /// a null reference (Nothing in Visual Basic) if no arguments are needed. + /// + /// + /// An IAsyncResult interface that represents the asynchronous operation started + /// by calling this method. + /// + /// + /// The delegate is called asynchronously, and this method returns immediately. + /// You can call this method from any thread. If you need the return value from a process + /// started with this method, call EndInvoke to get the value. + /// If you need to call the delegate synchronously, use the Invoke method instead. + /// + public IAsyncResult BeginInvoke(Delegate method, params object[] args) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateQueue"); + } + else if(method == null) + { + throw new ArgumentNullException(); + } + + #endregion + + DelegateQueueAsyncResult result; + + if(InvokeRequired) + { + result = new DelegateQueueAsyncResult(this, method, args, false, NotificationType.BeginInvokeCompleted); + + lock(lockObject) + { + delegateDeque.PushBack(result); + + Monitor.Pulse(lockObject); + } + } + else + { + result = new DelegateQueueAsyncResult(this, method, args, false, NotificationType.None); + + result.Invoke(); + } + + return result; + } + + /// + /// Waits until the process started by calling BeginInvoke completes, and then returns + /// the value generated by the process. + /// + /// + /// An IAsyncResult interface that represents the asynchronous operation started + /// by calling BeginInvoke. + /// + /// + /// An Object that represents the return value generated by the asynchronous operation. + /// + /// + /// This method gets the return value of the asynchronous operation represented by the + /// IAsyncResult passed by this interface. If the asynchronous operation has not completed, this method will wait until the result is available. + /// + public object EndInvoke(IAsyncResult result) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateQueue"); + } + else if(!(result is DelegateQueueAsyncResult)) + { + throw new ArgumentException(); + } + else if(((DelegateQueueAsyncResult)result).Owner != this) + { + throw new ArgumentException(); + } + + #endregion + + result.AsyncWaitHandle.WaitOne(); + + DelegateQueueAsyncResult r = (DelegateQueueAsyncResult)result; + + if(r.Error != null) + { + throw r.Error; + } + + return r.ReturnValue; + } + + /// + /// Executes the delegate on the main thread that this DelegateQueue executes on. + /// + /// + /// A Delegate that contains a method to call, in the context of the thread for the DelegateQueue. + /// + /// + /// An array of type Object that represents the arguments to pass to the given method. + /// + /// + /// An Object that represents the return value from the delegate being invoked, or a + /// null reference (Nothing in Visual Basic) if the delegate has no return value. + /// + /// + /// Unlike BeginInvoke, this method operates synchronously, that is, it waits until + /// the process completes before returning. Exceptions raised during the call are propagated + /// back to the caller. + /// Use this method when calling a method from a different thread to marshal the call + /// to the proper thread. + /// + public object Invoke(Delegate method, params object[] args) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateQueue"); + } + else if(method == null) + { + throw new ArgumentNullException(); + } + + #endregion + + object returnValue = null; + + if(InvokeRequired) + { + DelegateQueueAsyncResult result = new DelegateQueueAsyncResult(this, method, args, false, NotificationType.None); + + lock(lockObject) + { + delegateDeque.PushBack(result); + + Monitor.Pulse(lockObject); + } + + returnValue = EndInvoke(result); + } + else + { + // Invoke the method here rather than placing it in the queue. + returnValue = method.DynamicInvoke(args); + } + + return returnValue; + } + + /// + /// Gets a value indicating whether the caller must call Invoke. + /// + /// + /// true if the caller must call Invoke; otherwise, false. + /// + /// + /// This property determines whether the caller must call Invoke when making + /// method calls to this DelegateQueue. If you are calling a method from a different + /// thread, you must use the Invoke method to marshal the call to the proper thread. + /// + public bool InvokeRequired + { + get + { + return Thread.CurrentThread.ManagedThreadId != delegateThread.ManagedThreadId; + } + } + + #endregion + + #region IDisposable Members + + /// + /// Disposes of the DelegateQueue. + /// + public void Dispose() + { + #region Guards + + if(disposed) + { + return; + } + + #endregion + + Dispose(true); + + OnDisposed(EventArgs.Empty); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/PostCompletedEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/PostCompletedEventArgs.cs new file mode 100644 index 00000000..85504780 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/PostCompletedEventArgs.cs @@ -0,0 +1,59 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Threading; + +namespace Sanford.Threading +{ + public class PostCompletedEventArgs : AsyncCompletedEventArgs + { + private SendOrPostCallback callback; + + public PostCompletedEventArgs(SendOrPostCallback callback, Exception error, object state) + : base(error, false, state) + { + this.callback = callback; + } + + public SendOrPostCallback Callback + { + get + { + return callback; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/DelegateScheduler.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/DelegateScheduler.cs new file mode 100644 index 00000000..f2973a51 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/DelegateScheduler.cs @@ -0,0 +1,568 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Threading; +using System.Timers; +using Sanford.Collections; + +namespace Sanford.Threading +{ + /// + /// Provides functionality for timestamped delegate invocation. + /// + public partial class DelegateScheduler : IDisposable, IComponent + { + #region DelegateScheduler Members + + #region Fields + + /// + /// A constant value representing an unlimited number of delegate invocations. + /// + public const int Infinite = -1; + + // Default polling interval. + private const int DefaultPollingInterval = 10; + + // For queuing the delegates in priority order. + private PriorityQueue queue = new PriorityQueue(); + + // Used for timing events for polling the delegate queue. + private System.Timers.Timer timer = new System.Timers.Timer(DefaultPollingInterval); + + // For storing tasks when the scheduler isn't running. + private List tasks = new List(); + + // A value indicating whether the DelegateScheduler is running. + private bool running = false; + + // A value indicating whether the DelegateScheduler has been disposed. + private bool disposed = false; + + private ISite site = null; + + #endregion + + #region Events + + /// + /// Raised when a delegate is invoked. + /// + public event EventHandler InvokeCompleted; + + #endregion + + #region Construction + + /// + /// Initializes a new instance of the DelegateScheduler class. + /// + public DelegateScheduler() + { + Initialize(); + } + + /// + /// Initializes a new instance of the DelegateScheduler class with the + /// specified IContainer. + /// + public DelegateScheduler(IContainer container) + { + /// + /// Required for Windows.Forms Class Composition Designer support + /// + container.Add(this); + + Initialize(); + } + + // Initializes the DelegateScheduler. + private void Initialize() + { + timer.Elapsed += new ElapsedEventHandler(HandleElapsed); + } + + ~DelegateScheduler() + { + Dispose(false); + } + + #endregion + + #region Methods + + protected virtual void Dispose(bool disposing) + { + if(disposing) + { + Stop(); + + timer.Dispose(); + + Clear(); + + disposed = true; + + OnDisposed(EventArgs.Empty); + + GC.SuppressFinalize(this); + } + } + + /// + /// Adds a delegate to the DelegateScheduler. + /// + /// + /// The number of times the delegate should be invoked. + /// + /// + /// The time in milliseconds between delegate invocation. + /// + /// + /// + /// The delegate to invoke. + /// + /// The arguments to pass to the delegate when it is invoked. + /// + /// + /// A Task object representing the scheduled task. + /// + /// + /// If the DelegateScheduler has already been disposed. + /// + /// + /// If an unlimited count is desired, pass the DelegateScheduler.Infinity + /// constant as the count argument. + /// + public Task Add( + int count, + int millisecondsTimeout, + Delegate method, + params object[] args) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateScheduler"); + } + + #endregion + + Task t = new Task(count, millisecondsTimeout, method, args); + + lock(queue.SyncRoot) + { + // Only add the task to the DelegateScheduler if the count + // is greater than zero or set to Infinite. + if(count > 0 || count == DelegateScheduler.Infinite) + { + if(IsRunning) + { + queue.Enqueue(t); + } + else + { + tasks.Add(t); + } + } + } + + return t; + } + + /// + /// Removes the specified Task. + /// + /// + /// The Task to be removed. + /// + /// + /// If the DelegateScheduler has already been disposed. + /// + public void Remove(Task task) + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("DelegateScheduler"); + } + + #endregion + + #region Guard + + if(task == null) + { + return; + } + + #endregion + + lock(queue.SyncRoot) + { + if(IsRunning) + { + queue.Remove(task); + } + else + { + tasks.Remove(task); + } + } + } + + /// + /// Starts the DelegateScheduler. + /// + /// + /// If the DelegateScheduler has already been disposed. + /// + public void Start() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + #region Guard + + if(IsRunning) + { + return; + } + + #endregion + + lock(queue.SyncRoot) + { + Task t; + + while(tasks.Count > 0) + { + t = tasks[tasks.Count - 1]; + + tasks.RemoveAt(tasks.Count - 1); + + t.ResetNextTimeout(); + + queue.Enqueue(t); + } + + running = true; + + timer.Start(); + } + } + + /// + /// Stops the DelegateScheduler. + /// + /// + /// If the DelegateScheduler has already been disposed. + /// + public void Stop() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + #region Guard + + if(!IsRunning) + { + return; + } + + #endregion + + lock(queue.SyncRoot) + { + // While there are still tasks left in the queue. + while(queue.Count > 0) + { + // Remove task from queue and add it to the Task list + // to be used again next time the DelegateScheduler is run. + tasks.Add((Task)queue.Dequeue()); + } + + timer.Stop(); + + running = false; + } + } + + /// + /// Clears the DelegateScheduler of all tasks. + /// + /// + /// If the DelegateScheduler has already been disposed. + /// + public void Clear() + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + #endregion + + lock(queue.SyncRoot) + { + queue.Clear(); + tasks.Clear(); + } + } + + // Responds to the timer's Elapsed event by running any tasks that are due. + private void HandleElapsed(object sender, ElapsedEventArgs e) + { + Debug.WriteLine("Signal time: " + e.SignalTime.ToString()); + + lock(queue.SyncRoot) + { + #region Guard + + if(queue.Count == 0) + { + return; + } + + #endregion + + // Take a look at the first task in the queue to see if it's + // time to run it. + Task tk = (Task)queue.Peek(); + + // The return value from the delegate that will be invoked. + object returnValue; + + // While there are still tasks in the queue and it is time + // to run one or more of them. + while(queue.Count > 0 && tk.NextTimeout <= e.SignalTime) + { + // Remove task from queue. + queue.Dequeue(); + + // While it's time for the task to run. + while((tk.Count == Infinite || tk.Count > 0) && tk.NextTimeout <= e.SignalTime) + { + try + { + Debug.WriteLine("Invoking delegate."); + Debug.WriteLine("Next timeout: " + tk.NextTimeout.ToString()); + + // Invoke delegate. + returnValue = tk.Invoke(e.SignalTime); + + OnInvokeCompleted( + new InvokeCompletedEventArgs( + tk.Method, + tk.GetArgs(), + returnValue, + null)); + } + catch(Exception ex) + { + OnInvokeCompleted( + new InvokeCompletedEventArgs( + tk.Method, + tk.GetArgs(), + null, + ex)); + } + } + + // If this task should run again. + if(tk.Count == Infinite || tk.Count > 0) + { + // Enqueue task back into priority queue. + queue.Enqueue(tk); + } + + // If there are still tasks in the queue. + if(queue.Count > 0) + { + // Take a look at the next task to see if it is + // time to run. + tk = (Task)queue.Peek(); + } + } + } + } + + // Raises the Disposed event. + protected virtual void OnDisposed(EventArgs e) + { + EventHandler handler = Disposed; + + if(handler != null) + { + handler(this, e); + } + } + + // Raises the InvokeCompleted event. + protected virtual void OnInvokeCompleted(InvokeCompletedEventArgs e) + { + EventHandler handler = InvokeCompleted; + + if(handler != null) + { + handler(this, e); + } + } + + #endregion + + #region Properties + + /// + /// Gets or sets the interval in milliseconds in which the + /// DelegateScheduler polls its queue of delegates in order to + /// determine when they should run. + /// + public double PollingInterval + { + get + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("PriorityQueue"); + } + + #endregion + + return timer.Interval; + } + set + { + #region Require + + if(disposed) + { + throw new ObjectDisposedException("PriorityQueue"); + } + + #endregion + + timer.Interval = value; + } + } + + /// + /// Gets a value indicating whether the DelegateScheduler is running. + /// + public bool IsRunning + { + get + { + return running; + } + } + + /// + /// Gets or sets the object used to marshal event-handler calls and delegate invocations. + /// + public ISynchronizeInvoke SynchronizingObject + { + get + { + return timer.SynchronizingObject; + } + set + { + timer.SynchronizingObject = value; + } + } + + #endregion + + #endregion + + #region IComponent Members + + public event System.EventHandler Disposed; + + public ISite Site + { + get + { + return site; + } + set + { + site = value; + } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + #region Guard + + if(disposed) + { + return; + } + + #endregion + + Dispose(true); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/Task.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/Task.cs new file mode 100644 index 00000000..19bc54eb --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/Task.cs @@ -0,0 +1,176 @@ +#region License + +/* Copyright (c) 2007 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Sanford.Threading +{ + public class Task : IComparable + { + #region Task Members + + #region Fields + + // The number of times left to invoke the delegate associated with this Task. + private int count; + + // The interval between delegate invocation. + private int millisecondsTimeout; + + // The delegate to invoke. + private Delegate method; + + // The arguments to pass to the delegate when it is invoked. + private object[] args; + + // The time for the next timeout; + private DateTime nextTimeout; + + // For locking. + private readonly object lockObject = new object(); + + #endregion + + #region Construction + + internal Task( + int count, + int millisecondsTimeout, + Delegate method, + object[] args) + { + this.count = count; + this.millisecondsTimeout = millisecondsTimeout; + this.method = method; + this.args = args; + + ResetNextTimeout(); + } + + #endregion + + #region Methods + + internal void ResetNextTimeout() + { + nextTimeout = DateTime.Now.AddMilliseconds(millisecondsTimeout); + } + + internal object Invoke(DateTime signalTime) + { + Debug.Assert(count == DelegateScheduler.Infinite || count > 0); + + object returnValue = method.DynamicInvoke(args); + + if(count == DelegateScheduler.Infinite) + { + nextTimeout = nextTimeout.AddMilliseconds(millisecondsTimeout); + } + else + { + count--; + + if(count > 0) + { + nextTimeout = nextTimeout.AddMilliseconds(millisecondsTimeout); + } + } + + return returnValue; + } + + public object[] GetArgs() + { + return args; + } + + #endregion + + #region Properties + + public DateTime NextTimeout + { + get + { + return nextTimeout; + } + } + + public int Count + { + get + { + return count; + } + } + + public Delegate Method + { + get + { + return method; + } + } + + public int MillisecondsTimeout + { + get + { + return millisecondsTimeout; + } + } + + #endregion + + #endregion + + #region IComparable Members + + public int CompareTo(object obj) + { + Task t = obj as Task; + + if(t == null) + { + throw new ArgumentException("obj is not the same type as this instance."); + } + + return -nextTimeout.CompareTo(t.nextTimeout); + } + + #endregion + } +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/InvokeCompletedEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/InvokeCompletedEventArgs.cs new file mode 100644 index 00000000..8e69cf3e --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/InvokeCompletedEventArgs.cs @@ -0,0 +1,81 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +#region Contact + +/* + * Leslie Sanford + * Email: jabberdabber@hotmail.com + */ + +#endregion + +using System; +using System.ComponentModel; +using System.Reflection; + +namespace Sanford.Threading +{ + /// + /// Represents information about the InvokeCompleted event. + /// + public class InvokeCompletedEventArgs : AsyncCompletedEventArgs + { + private Delegate method; + + private object[] args; + + private object result; + + public InvokeCompletedEventArgs(Delegate method, object[] args, object result, Exception error) + : base(error, false, null) + { + this.method = method; + this.args = args; + this.result = result; + } + + public object[] GetArgs() + { + return args; + } + + public Delegate Method + { + get + { + return method; + } + } + + public object Result + { + get + { + return result; + } + } + } +} diff --git a/Sanford.Multimedia.Midi.Core/docs/Sanford.Multimedia.Midi.XML b/Sanford.Multimedia.Midi.Core/docs/Sanford.Multimedia.Midi.XML new file mode 100644 index 00000000..8a659719 --- /dev/null +++ b/Sanford.Multimedia.Midi.Core/docs/Sanford.Multimedia.Midi.XML @@ -0,0 +1,5378 @@ + + + + Sanford.Multimedia.Midi.Core + + + + + Represents a simple double-ended-queue collection of objects. + + + + + Initializes a new instance of the Deque class. + + + + + Initializes a new instance of the Deque class that contains + elements copied from the specified collection. + + + The ICollection to copy elements from. + + + + + Removes all objects from the Deque. + + + + + Determines whether or not an element is in the Deque. + + + The Object to locate in the Deque. + + + true if obj if found in the Deque; otherwise, + false. + + + + + Inserts an object at the front of the Deque. + + + The object to push onto the deque; + + + + + Inserts an object at the back of the Deque. + + + The object to push onto the deque; + + + + + Removes and returns the object at the front of the Deque. + + + The object at the front of the Deque. + + + The Deque is empty. + + + + + Removes and returns the object at the back of the Deque. + + + The object at the back of the Deque. + + + The Deque is empty. + + + + + Returns the object at the front of the Deque without removing it. + + + The object at the front of the Deque. + + + The Deque is empty. + + + + + Returns the object at the back of the Deque without removing it. + + + The object at the back of the Deque. + + + The Deque is empty. + + + + + Copies the Deque to a new array. + + + A new array containing copies of the elements of the Deque. + + + + + Returns a synchronized (thread-safe) wrapper for the Deque. + + + The Deque to synchronize. + + + A synchronized wrapper around the Deque. + + + + + Gets a value indicating whether access to the Deque is synchronized + (thread-safe). + + + + + Gets the number of elements contained in the Deque. + + + + + Copies the Deque elements to an existing one-dimensional Array, + starting at the specified array index. + + + The one-dimensional Array that is the destination of the elements + copied from Deque. The Array must have zero-based indexing. + + + The zero-based index in array at which copying begins. + + + + + Gets an object that can be used to synchronize access to the Deque. + + + + + Returns an enumerator that can iterate through the Deque. + + + An IEnumerator for the Deque. + + + + + Creates a shallow copy of the Deque. + + + A shallow copy of the Deque. + + + + + Represents a simple double-ended-queue collection of objects. + + + + + Initializes a new instance of the Deque class. + + + + + Initializes a new instance of the Deque class that contains + elements copied from the specified collection. + + + The collection whose elements are copied to the new Deque. + + + + + Removes all objects from the Deque. + + + + + Determines whether or not an element is in the Deque. + + + The Object to locate in the Deque. + + + true if obj if found in the Deque; otherwise, + false. + + + + + Inserts an object at the front of the Deque. + + + The object to push onto the deque; + + + + + Inserts an object at the back of the Deque. + + + The object to push onto the deque; + + + + + Removes and returns the object at the front of the Deque. + + + The object at the front of the Deque. + + + The Deque is empty. + + + + + Removes and returns the object at the back of the Deque. + + + The object at the back of the Deque. + + + The Deque is empty. + + + + + Returns the object at the front of the Deque without removing it. + + + The object at the front of the Deque. + + + The Deque is empty. + + + + + Returns the object at the back of the Deque without removing it. + + + The object at the back of the Deque. + + + The Deque is empty. + + + + + Copies the Deque to a new array. + + + A new array containing copies of the elements of the Deque. + + + + + Returns a synchronized (thread-safe) wrapper for the Deque. + + + The Deque to synchronize. + + + A synchronized wrapper around the Deque. + + + + + Gets a value indicating whether access to the Deque is synchronized + (thread-safe). + + + + + Gets the number of elements contained in the Deque. + + + + + Copies the Deque elements to an existing one-dimensional Array, + starting at the specified array index. + + + The one-dimensional Array that is the destination of the elements + copied from Deque. The Array must have zero-based indexing. + + + The zero-based index in array at which copying begins. + + + + + Gets an object that can be used to synchronize access to the Deque. + + + + + Returns an enumerator that can iterate through the Deque. + + + An IEnumerator for the Deque. + + + + + Creates a shallow copy of the Deque. + + + A shallow copy of the Deque. + + + + + Returns an enumerator that can iterate through the Deque. + + + An IEnumerator for the Deque. + + + + + Represents a list with undo/redo functionality. + + + The type of elements in the list. + + + + + Undoes the last operation. + + + true if the last operation was undone, false if there + are no more operations left to undo. + + + + + Redoes the last operation. + + + true if the last operation was redone, false if there + are no more operations left to redo. + + + + + Clears the undo/redo history. + + + + + The number of operations left to undo. + + + + + The number of operations left to redo. + + + + + Undoes the last operation. + + + true if the last operation was undone, false if there + are no more operations left to undo. + + + + + Redoes the last operation. + + + true if the last operation was redone, false if there + are no more operations left to redo. + + + + + Clears the undo/redo history. + + + + + The number of operations left to undo. + + + + + The number of operations left to redo. + + + + + Represents an array data structure. + + + + + Initialize an instance of the Array class with the specified array + length. + + + The length of the array. + + + + + Initializes a new instance of the Array class with the specified + head of the random access list and the length of the array. + + + The head of the random access list. + + + The length of the array. + + + + + Gets the value of the specified element in the current Array. + + + An integer that represents the position of the Array element to + get. + + + The value at the specified position in the Array. + + + index is outside the range of valid indexes for the current Array. + + + + + Sets the specified element in the current Array to the specified + value. + + + The new value for the specified element. + + + An integer that represents the position of the Array element to set. + + + A new array with the element at the specified position set to the + specified value. + + + index is outside the range of valid indexes for the current Array. + + + + + Gets an integer that represents the total number of elements in all + the dimensions of the Array. + + + + + Returns an IEnumerator for the Array. + + + An IEnumerator for the Array. + + + + + Represents a collection of elements accessible by index and supports + insertion and deletion. + + + + + Initializes the ArrayList class. + + + + + Initializes a new instance of the ArrayList class. + + + + + Initializes a new instance of the ArrayList class that contains + elements copied from the specified collection. + + + The ICollection whose elements are copied to the new list. + + + + + Initializes a new instance of the ArrayList class with the + specified root and count. + + + The root of the tree. + + + The number of items in the ArrayList. + + + + + Adds an object to the end of the ArrayList. + + + The Object to be added to the end of the ArrayList. + + + A new ArrayList object with the specified value added at the end. + + + + + Determines whether an element is in the ArrayList. + + + The Object to locate in the ArrayList. + + + true if item is found in the ArrayList; otherwise, + false. + + + + + Returns the zero-based index of the first occurrence of a value in + the ArrayList. + + + The Object to locate in the ArrayList. + + + The zero-based index of the first occurrence of value within the + ArrayList, if found; otherwise, -1. + + + + + Inserts an element into the ArrayList at the specified index. + + + The zero-based index at which value should be inserted. + + + The Object to insert. + + + A new ArrayList with the specified object inserted at the specified + index. + + + index is less than zero or index is greater than Count. + + + + + Removes the first occurrence of a specified object from the + ArrayList. + + + The Object to remove from the ArrayList. + + + A new ArrayList with the first occurrent of the specified object + removed. + + + + + Removes the element at the specified index of the ArrayList. + + + The zero-based index of the element to remove. + + + A new ArrayList with the element at the specified index removed. + + + index is less than zero or index is equal to or greater than Count. + + + + + Gets the value at the specified index. + + + The zero-based index of the element to get. + + + The value at the specified index. + + + index is less than zero or index is equal to or greater than Count. + + + + + Sets the value at the specified index. + + + The zero-based index of the element to set. + + + The value to set at the specified index. + + + A new ArrayList with the specified value set at the specified index. + + + index is less than zero or index is equal to or greater than Count. + + + + + Gets the number of elements contained in the ArrayList. + + + + + Returns an enumerator that can iterate through the ArrayList. + + + An IEnumerator that can be used to iterate through the ArrayList. + + + + + Provides functionality for iterating over an AVL tree. + + + + + Initializes a new instance of the AvlEnumerator class. + + + The root of the AVL tree to iterate over. + + + + + Initializes a new instance of the AvlEnumerator class. + + + The root of the AVL tree to iterate over. + + + The number of nodes in the tree. + + + + + Sets the enumerator to its initial position, which is before + the first element in the AVL tree. + + + + + Gets the current element in the AVL tree. + + + The enumerator is positioned before the first element in the AVL + tree or after the last element. + + + + + Advances the enumerator to the next element of the AVL tree. + + + true if the enumerator was successfully advanced to the + next element; false if the enumerator has passed the end + of the collection. + + + + + Represents a node in an AVL tree. + + + + + Initializes a new instance of the AvlNode class with the specified + data and left and right children. + + + The data for the node. + + + The left child. + + + The right child. + + + + + Removes the current node from the AVL tree. + + + The node to in the tree to replace the current node. + + + + + Balances the subtree represented by the node. + + + The root node of the balanced subtree. + + + + + Indicates whether or not the subtree the node represents is in + balance. + + + true if the subtree is in balance; otherwise, false. + + + + + Gets the balance factor of the subtree the node represents. + + + + + Gets the number of nodes in the subtree. + + + + + Gets the node's data. + + + + + Gets the height of the subtree the node represents. + + + + + Gets the node's left child. + + + + + Gets the node's right child. + + + + + Represents the functionality and properties of AVL nodes. + + + + + Removes the current node from the AVL tree. + + + The node to in the tree to replace the current node. + + + + + Balances the subtree represented by the node. + + + The root node of the balanced subtree. + + + + + Indicates whether or not the subtree the node represents is in + balance. + + + true if the subtree is in balance; otherwise, false. + + + + + Gets the balance factor of the subtree the node represents. + + + + + Gets the number of nodes in the subtree. + + + + + Gets the node's data. + + + + + Gets the height of the subtree the node represents. + + + + + Gets the node's left child. + + + + + Gets the node's right child. + + + + + Represents a null AVL node. + + + + + Removes the current node from the AVL tree. + + + The node to in the tree to replace the current node. + + + + + Balances the subtree represented by the node. + + + The root node of the balanced subtree. + + + + + Indicates whether or not the subtree the node represents is in + balance. + + + true if the subtree is in balance; otherwise, false. + + + + + Gets the balance factor of the subtree the node represents. + + + + + Gets the number of nodes in the subtree. + + + + + Gets the node's data. + + + + + Gets the height of the subtree the node represents. + + + + + Gets the node's left child. + + + + + Gets the node's right child. + + + + + Provides functionality for enumerating a RandomAccessList. + + + + + Initializes a new instance of the Enumerator with the specified + head of the list and the number of nodes in the list. + + + The head of the list. + + + The number of nodes in the list. + + + + + Sets the enumerator to its initial position, which is before + the first element in the random access list. + + + + + Gets the current element in the random access list. + + + The enumerator is positioned before the first element in the + random access list or after the last element. + + + + + Advances the enumerator to the next element in the random access + list. + + + true if the enumerator was successfully advanced to the + next element; false if the enumerator has passed the end + of the collection. + + + + + Represents the top nodes in a RandomAccessList. + + + + + Initializes a new instance of the RalTopNode with the specified + root of the tree this node represents and the next top node in the + list. + + + The root node of the tree this top node represents. + + + The next top node in the list. + + + + + Gets the value at the specified element in the random access list. + + + An integer that represents the position of the random access list + element to get. + + + The value at the specified position in the random access list. + + + + + Sets the specified element in the current random access list to the + specified value. + + + The new value for the specified element. + + + An integer that represents the position of the random access list + element to set. + + + A new random access list top node with the element at the specified + position set to the specified value. + + + + + Gets the root node represented by the top node. + + + + + Gets the next top node in the random access list. + + + + + Represents subtree nodes within random access lists. + + + + + Initializes an instance of the RandomAccessListNode with the + specified value, left child, and right child. + + + The value to store in the node. + + + The left child. + + + The right child. + + + + + Gets the value at the specified element in the random access list + subtree. + + + An integer that represents the position of the random access list + subtree element to get. + + + The value at the specified position in the random access list + subtree. + + + + + Sets the specified element in the current random access list + subtree to the specified value. + + + The new value for the specified element. + + + An integer that represents the position of the random access list + subtree element to set. + + + A new random access list tree node with the element at the specified + position set to the specified value. + + + + + Gets the number of nodes in the tree. + + + + + Gets the left child. + + + + + Gets the right child. + + + + + Gets the value represented by this node. + + + + + Implements Chris Okasaki's random access list. + + + + + Represents an empty random access list. + + + + + Initializes a new instance of the RandomAccessList class. + + + + + Initializes a new instance of the RandomAccessList class with the + specified first top node and the number of elements in the list. + + + The first top node in the list. + + + The number of nodes in the list. + + + + + Prepends a value to the random access list. + + + The value to prepend to the list. + + + A new random access list with the specified value prepended to the + list. + + + + + Gets the value at the specified position in the current + RandomAccessList. + + + An integer that represents the position of the RandomAccessList + element to get. + + + The value at the specified position in the RandomAccessList. + + + index is outside the range of valid indexes for the current + RandomAccessList. + + + + + Sets the specified element in the current RandomAccessList to the + specified value. + + + The new value for the specified element. + + + An integer that represents the position of the RandomAccessList + element to set. + + + A new RandomAccessList with the element at the specified position + set to the specified value. + + + index is outside the range of valid indexes for the current + RandomAccessList. + + + + + Gets the number of elements in the RandomAccessList. + + + + + Gets a RandomAccessList with first element of the current + RandomAccessList. + + + If the RandomAccessList is empty. + + + + + Gets a RandomAccessList with all but the first element of the + current RandomAccessList. + + + If the RandomAccessList is empty. + + + + + Returns an IEnumerator for the RandomAccessList. + + + An IEnumerator for the RandomAccessList. + + + + + Represents a collection of key-and-value pairs that are sorted by the + keys and are accessible by key. + + + + + An empty SortedList. + + + + + Initializes a new instance of the SortedList class that is empty + and is sorted according to the IComparable interface implemented by + each key added to the SortedList. + + + + + Initializes a new instance of the SortedList class that is empty + and is sorted according to the specified IComparer interface. + + + The IComparer implementation to use when comparing keys, or a null + reference to use the IComparable implementation of each key. + + + + + Initializes a new instance of the SortedList class with the + specified root node and the IComparer interface to use for sorting + keys. + + + The root of the AVL tree. + + + The IComparer implementation to use when comparing keys, or a null + reference to use the IComparable implementation of each key. + + + + + Adds an element with the specified key and value to the SortedList. + + + The key of the element to add. + + + The value of the element to add. The value can be a null reference. + + + A new SortedList with the specified key and value added to the + previous SortedList. + + + key is a null reference. + + + An element with the specified key already exists in the SortedList, + or The SortedList is set to use the IComparable interface, and key + does not implement the IComparable interface. + + + + + Determines whether the SortedList contains a specific key. + + + The key to locate in the SortedList. + + + true if the SortedList contains an element with the + specified key; otherwise, false. + + + + + Returns an IDictionaryEnumerator that can iterate through the + SortedList. + + + An IDictionaryEnumerator for the SortedList. + + + + + Removes the element with the specified key from SortedList. + + + + + The key of the element to remove. + + + key is a null reference. + + + The SortedList is set to use the IComparable interface, and key + does not implement the IComparable interface. + + + + + Gets the value associated with the specified key. + + + + + Gets the number of elements contained in the SortedList. + + + + + Provides functionality for iterating through a SortedList. + + + + + Initializes a new instance of the SortedListEnumerator class + with the specified root of the AVL tree to iterate over. + + + The root of the AVL tree the SortedList uses internally. + + + + + Represents a simple last-in-first-out collection of objects. + + + + + An empty Stack. + + + + + Initializes a new instance of the Stack class. + + + + + Initializes a new instance of the Stack class with the + specified top node and the number of elements in the stack. + + + The top node in the stack. + + + The number of elements in the stack. + + + + + Inserts an object at the top of the Stack. + + + The Object to push onto the Stack. + + + A new stack with the specified object on the top of the stack. + + + + + Removes the object at the top of the Stack. + + + A new stack with top of the previous stack removed. + + + The Stack is empty. + + + + + Gets the number of elements in the Stack. + + + + + Gets the top of the stack. + + + The Stack is empty. + + + + + Represents a node in the stack. + + + + + Provides functionality for iterating over the Stack class. + + + + + Initializes a new instance of the StackEnumerator class with + the specified stack to iterate over. + + + The Stack to iterate over. + + + + + Sets the enumerator to its initial position, which is before + the first element in the Stack. + + + + + Gets the current element in the Stack. + + + The enumerator is positioned before the first element of the + Stack or after the last element. + + + + + Advances the enumerator to the next element of the Stack. + + + + + + Returns an IEnumerator for the Stack. + + + An IEnumerator for the Stack. + + + + + Represents the priority queue data structure. + + + + + Initializes a new instance of the PriorityQueue class. + + + The PriorityQueue will cast its elements to the IComparable + interface when making comparisons. + + + + + Initializes a new instance of the PriorityQueue class with the + specified IComparer. + + + The IComparer to use for comparing and ordering elements. + + + If the specified IComparer is null, the PriorityQueue will cast its + elements to the IComparable interface when making comparisons. + + + + + Enqueues the specified element into the PriorityQueue. + + + The element to enqueue into the PriorityQueue. + + + If element is null. + + + + + Removes the element at the head of the PriorityQueue. + + + The element at the head of the PriorityQueue. + + + If Count is zero. + + + + + Removes the specified element from the PriorityQueue. + + + The element to remove. + + + If element is null + + + + + Returns a value indicating whether the specified element is in the + PriorityQueue. + + + The element to test. + + + true if the element is in the PriorityQueue; otherwise + false. + + + + + Returns the element at the head of the PriorityQueue without + removing it. + + + The element at the head of the PriorityQueue. + + + + + Removes all elements from the PriorityQueue. + + + + + Returns a synchronized wrapper of the specified PriorityQueue. + + + The PriorityQueue to synchronize. + + + A synchronized PriorityQueue. + + + If queue is null. + + + + + Represents a collection of key-and-value pairs. + + + The SkipList class is an implementation of the IDictionary interface. It + is based on the data structure created by William Pugh. + + + + + Initializes a new instance of the SkipList class that is empty and + is sorted according to the IComparable interface implemented by + each key added to the SkipList. + + + Each key must implement the IComparable interface to be capable of + comparisons with every other key in the SortedList. The elements + are sorted according to the IComparable implementation of each key + added to the SkipList. + + + + + Initializes a new instance of the SkipList class that is empty and + is sorted according to the specified IComparer interface. + + + The IComparer implementation to use when comparing keys. + + + The elements are sorted according to the specified IComparer + implementation. If comparer is a null reference, the IComparable + implementation of each key is used; therefore, each key must + implement the IComparable interface to be capable of comparisons + with every other key in the SkipList. + + + + + Destructor. + + + + + Initializes the SkipList. + + + + + Returns a level value for a new SkipList node. + + + The level value for a new SkipList node. + + + + + Searches for the specified key. + + + The key to search for. + + + Returns true if the specified key is in the SkipList. + + + + + Searches for the specified key. + + + The key to search for. + + + A SkipList node to hold the results of the search. + + + Returns true if the specified key is in the SkipList. + + + + + Searches for the specified key. + + + The key to search for. + + + An array of nodes holding references to the places in the SkipList + search in which the search dropped down one level. + + + Returns true if the specified key is in the SkipList. + + + + + Searches for the specified key. + + + The key to search for. + + + A SkipList node to hold the results of the search. + + + An array of nodes holding references to the places in the SkipList + search in which the search dropped down one level. + + + Returns true if the specified key is in the SkipList. + + + + + Search for the specified key using a comparer. + + + The key to search for. + + + A SkipList node to hold the results of the search. + + + An array of nodes holding references to the places in the SkipList + search in which the search dropped down one level. + + + Returns true if the specified key is in the SkipList. + + + + + Search for the specified key using the IComparable interface + implemented by each key. + + + The key to search for. + + + A SkipList node to hold the results of the search. + + + An array of nodes holding references to the places in the SkipList + search in which the search dropped down one level. + + + Returns true if the specified key is in the SkipList. + + + Assumes each key inserted into the SkipList implements the + IComparable interface. + + If the specified key is in the SkipList, the curr parameter will + reference the node with the key. If the specified key is not in the + SkipList, the curr paramater will either hold the node with the + first key value greater than the specified key or it will have the + same value as the header indicating that the search reached the end + of the SkipList. + + + + + Inserts a key/value pair into the SkipList. + + + The key to insert into the SkipList. + + + The value to insert into the SkipList. + + + An array of nodes holding references to places in the SkipList in + which the search for the place to insert the new key/value pair + dropped down one level. + + + + + Represents a node in the SkipList. + + + + + Initializes an instant of a Node with its node level. + + + The node level. + + + + + Initializes an instant of a Node with its node level and + key/value pair. + + + The node level. + + + The key for the node. + + + The value for the node. + + + + + Key property. + + + + + Value property. + + + + + Node dictionary Entry property - contains key/value pair. + + + + + Disposes the Node. + + + + + Enumerates the elements of a skip list. + + + + + Initializes an instance of a SkipListEnumerator. + + + + + + Gets both the key and the value of the current dictionary + entry. + + + + + Gets the key of the current dictionary entry. + + + + + Gets the value of the current dictionary entry. + + + + + Advances the enumerator to the next element of the skip list. + + + true if the enumerator was successfully advanced to the next + element; false if the enumerator has passed the end of the + skip list. + + + + + Sets the enumerator to its initial position, which is before + the first element in the skip list. + + + + + Gets the current element in the skip list. + + + + + Adds an element with the provided key and value to the SkipList. + + + The Object to use as the key of the element to add. + + + The Object to use as the value of the element to add. + + + + + Removes all elements from the SkipList. + + + + + Determines whether the SkipList contains an element with the + specified key. + + + The key to locate in the SkipList. + + + true if the SkipList contains an element with the key; otherwise, + false. + + + + + Returns an IDictionaryEnumerator for the SkipList. + + + An IDictionaryEnumerator for the SkipList. + + + + + Removes the element with the specified key from the SkipList. + + + The key of the element to remove. + + + + + Gets a value indicating whether the SkipList has a fixed size. + + + + + Gets a value indicating whether the IDictionary is read-only. + + + + + Gets or sets the element with the specified key. This is the + indexer for the SkipList. + + + + + Gets an ICollection containing the keys of the SkipList. + + + + + Gets an ICollection containing the values of the SkipList. + + + + + Copies the elements of the SkipList to an Array, starting at a + particular Array index. + + + The one-dimensional Array that is the destination of the elements + copied from SkipList. + + + The zero-based index in array at which copying begins. + + + + + Gets the number of elements contained in the SkipList. + + + + + Gets a value indicating whether access to the SkipList is + synchronized (thread-safe). + + + + + Gets an object that can be used to synchronize access to the + SkipList. + + + + + Returns an enumerator that can iterate through the SkipList. + + + An IEnumerator that can be used to iterate through the collection. + + + + + Represents functionality for generating events for driving Sequence playback. + + + + + Occurs when an IClock generates a tick. + + + + + Occurs when an IClock starts generating Ticks. + + + When an IClock is started, it resets itself and generates ticks to + drive playback from the beginning of the Sequence. + + + + + Occurs when an IClock continues generating Ticks. + + + When an IClock is continued, it generates ticks to drive playback + from the current position within the Sequence. + + + + + Occurs when an IClock is stopped. + + + + + Gets a value indicating whether the IClock is running. + + + + + Generates clock events internally. + + + + + Initializes a new instance of the MidiInternalClock class. + + + + + Initializes a new instance of the MidiInternalClock class with the + specified IContainer. + + + The IContainer to which the MidiInternalClock will add itself. + + + + + Starts the MidiInternalClock. + + + + + Resumes tick generation from the current position. + + + + + Stops the MidiInternalClock. + + + + + Gets or sets the tempo in microseconds per beat. + + + + + Provides basic functionality for generating tick events with pulses per + quarter note resolution. + + + + + The default tempo in microseconds: 120bpm. + + + + + The minimum pulses per quarter note value. + + + + + Represents a MIDI device capable of receiving MIDI events. + + + + + Initializes a new instance of the InputDevice class with the + specified device ID. + + + + + Gets or sets a value indicating whether the midi events should be posted on the same synchronization context as the device constructor was called. + Default is true. If set to false the events are fired on the driver callback or the thread of the driver callback delegate queue, depending on the PostDriverCallbackToDelegateQueue property. + + + true if midi events should be posted on the same synchronization context as the device constructor was called; otherwise, false. + + + + + Occurs when any message was received. The underlying type of the message is as specific as possible. + Channel, Common, Realtime or SysEx. + + + + + Gets or sets a value indicating whether the midi input driver callback should be posted on a delegate queue with its own thread. + Default is true. If set to false the driver callback directly calls the events for lowest possible latency. + + + true if the midi input driver callback should be posted on a delegate queue with its own thread; otherwise, false. + + + + + The exception that is thrown when a error occurs with the InputDevice + class. + + + + + Initializes a new instance of the InputDeviceException class with + the specified error code. + + + The error code. + + + + + Gets a message that describes the current exception. + + + + + Represents MIDI input device capabilities. + + + + + Manufacturer identifier of the device driver for the Midi output + device. + + + + + Product identifier of the Midi output device. + + + + + Version number of the device driver for the Midi output device. The + high-order byte is the major version number, and the low-order byte + is the minor version number. + + + + + Product name. + + + + + Optional functionality supported by the device. + + + + + The base class for all MIDI devices. + + + + + Connects a MIDI InputDevice to a MIDI thru or OutputDevice, or + connects a MIDI thru device to a MIDI OutputDevice. + + + Handle to a MIDI InputDevice or a MIDI thru device (for thru + devices, this handle must belong to a MIDI OutputDevice). + + + Handle to the MIDI OutputDevice or thru device. + + + If an error occurred while connecting the two devices. + + + + + Disconnects a MIDI InputDevice from a MIDI thru or OutputDevice, or + disconnects a MIDI thru device from a MIDI OutputDevice. + + + Handle to a MIDI InputDevice or a MIDI thru device. + + + Handle to the MIDI OutputDevice to be disconnected. + + + If an error occurred while disconnecting the two devices. + + + + + The base class for all MIDI device exception classes. + + + + + Initializes a new instance of the DeviceException class with the + specified error code. + + + The error code. + + + + + Represents the Windows Multimedia MIDIHDR structure. + + + + + Pointer to MIDI data. + + + + + Size of the buffer. + + + + + Actual amount of data in the buffer. This value should be less than + or equal to the value given in the dwBufferLength member. + + + + + Custom user data. + + + + + Flags giving information about the buffer. + + + + + Reserved; do not use. + + + + + Reserved; do not use. + + + + + Offset into the buffer when a callback is performed. (This + callback is generated because the MEVT_F_CALLBACK flag is + set in the dwEvent member of the MidiEventArgs structure.) + This offset enables an application to determine which + event caused the callback. + + + + + Reserved; do not use. + + + + + Builds a pointer to a MidiHeader structure. + + + + + Initializes a new instance of the MidiHeaderBuilder. + + + + + Builds the pointer to the MidiHeader structure. + + + + + Initializes the MidiHeaderBuilder with the specified SysExMessage. + + + The SysExMessage to use for initializing the MidiHeaderBuilder. + + + + + Releases the resources associated with the built MidiHeader pointer. + + + + + Releases the resources associated with the specified MidiHeader pointer. + + + The MidiHeader pointer. + + + + + The length of the system exclusive buffer. + + + + + Gets the pointer to the MidiHeader. + + + + + Represents MIDI output device capabilities. + + + + + Manufacturer identifier of the device driver for the Midi output + device. + + + + + Product identifier of the Midi output device. + + + + + Version number of the device driver for the Midi output device. The + high-order byte is the major version number, and the low-order byte + is the minor version number. + + + + + Product name. + + + + + Flags describing the type of the Midi output device. + + + + + Number of voices supported by an internal synthesizer device. If + the device is a port, this member is not meaningful and is set + to 0. + + + + + Maximum number of simultaneous notes that can be played by an + internal synthesizer device. If the device is a port, this member + is not meaningful and is set to 0. + + + + + Channels that an internal synthesizer device responds to, where the + least significant bit refers to channel 0 and the most significant + bit to channel 15. Port devices that transmit on all channels set + this member to 0xFFFF. + + + + + Optional functionality supported by the device. + + + + + + + + + + Represents a device capable of sending MIDI messages. + + + + + Initializes a new instance of the OutputDevice class. + + + + + Closes the OutputDevice. + + + If an error occurred while closing the OutputDevice. + + + + + Resets the OutputDevice. + + + + + Gets or sets a value indicating whether the OutputDevice uses + a running status. + + + + + The exception that is thrown when a error occurs with the OutputDevice + class. + + + + + Initializes a new instance of the OutputDeviceException class with + the specified error code. + + + The error code. + + + + + Gets a message that describes the current exception. + + + + + Defines constants representing the General MIDI instrument set. + + + + + Defines constants for ChannelMessage types. + + + + + Represents the note-off command type. + + + + + Represents the note-on command type. + + + + + Represents the poly pressure (aftertouch) command type. + + + + + Represents the controller command type. + + + + + Represents the program change command type. + + + + + Represents the channel pressure (aftertouch) command + type. + + + + + Represents the pitch wheel command type. + + + + + Defines constants for controller types. + + + + + The Bank Select coarse. + + + + + The Modulation Wheel coarse. + + + + + The Breath Control coarse. + + + + + The Foot Pedal coarse. + + + + + The Portamento Time coarse. + + + + + The Data Entry Slider coarse. + + + + + The Volume coarse. + + + + + The Balance coarse. + + + + + The Pan position coarse. + + + + + The Expression coarse. + + + + + The Effect Control 1 coarse. + + + + + The Effect Control 2 coarse. + + + + + The General Puprose Slider 1 + + + + + The General Puprose Slider 2 + + + + + The General Puprose Slider 3 + + + + + The General Puprose Slider 4 + + + + + The Bank Select fine. + + + + + The Modulation Wheel fine. + + + + + The Breath Control fine. + + + + + The Foot Pedal fine. + + + + + The Portamento Time fine. + + + + + The Data Entry Slider fine. + + + + + The Volume fine. + + + + + The Balance fine. + + + + + The Pan position fine. + + + + + The Expression fine. + + + + + The Effect Control 1 fine. + + + + + The Effect Control 2 fine. + + + + + The Hold Pedal 1. + + + + + The Portamento. + + + + + The Sustenuto Pedal. + + + + + The Soft Pedal. + + + + + The Legato Pedal. + + + + + The Hold Pedal 2. + + + + + The Sound Variation. + + + + + The Sound Timbre. + + + + + The Sound Release Time. + + + + + The Sound Attack Time. + + + + + The Sound Brightness. + + + + + The Sound Control 6. + + + + + The Sound Control 7. + + + + + The Sound Control 8. + + + + + The Sound Control 9. + + + + + The Sound Control 10. + + + + + The General Purpose Button 1. + + + + + The General Purpose Button 2. + + + + + The General Purpose Button 3. + + + + + The General Purpose Button 4. + + + + + The Effects Level. + + + + + The Tremolo Level. + + + + + The Chorus Level. + + + + + The Celeste Level. + + + + + The Phaser Level. + + + + + The Data Button Increment. + + + + + The Data Button Decrement. + + + + + The NonRegistered Parameter Fine. + + + + + The NonRegistered Parameter Coarse. + + + + + The Registered Parameter Fine. + + + + + The Registered Parameter Coarse. + + + + + The All Sound Off. + + + + + The All Controllers Off. + + + + + The Local Keyboard. + + + + + The All Notes Off. + + + + + The Omni Mode Off. + + + + + The Omni Mode On. + + + + + The Mono Operation. + + + + + The Poly Operation. + + + + + Represents MIDI channel messages. + + + + + Maximum value allowed for MIDI channels. + + + + + Initializes a new instance of the ChannelEventArgs class with the + specified command, MIDI channel, and data 1 values. + + + The command value. + + + The MIDI channel. + + + The data 1 value. + + + If midiChannel is less than zero or greater than 15. Or if + data1 is less than zero or greater than 127. + + + + + Initializes a new instance of the ChannelEventArgs class with the + specified command, MIDI channel, data 1, and data 2 values. + + + The command value. + + + The MIDI channel. + + + The data 1 value. + + + The data 2 value. + + + If midiChannel is less than zero or greater than 15. Or if + data1 or data 2 is less than zero or greater than 127. + + + + + Returns a value for the current ChannelEventArgs suitable for use in + hashing algorithms. + + + A hash code for the current ChannelEventArgs. + + + + + Determines whether two ChannelEventArgs instances are equal. + + + The ChannelMessageEventArgs to compare with the current ChannelEventArgs. + + + true if the specified object is equal to the current + ChannelMessageEventArgs; otherwise, false. + + + + + Returns a value indicating how many bytes are used for the + specified ChannelCommand. + + + The ChannelCommand value to test. + + + The number of bytes used for the specified ChannelCommand. + + + + + Unpacks the command value from the specified integer channel + message. + + + The message to unpack. + + + The command value for the packed message. + + + + + Unpacks the MIDI channel from the specified integer channel + message. + + + The message to unpack. + + + The MIDI channel for the pack message. + + + + + Packs the MIDI channel into the specified integer message. + + + The message into which the MIDI channel is packed. + + + The MIDI channel to pack into the message. + + + An integer message. + + + If midiChannel is less than zero or greater than 15. + + + + + Packs the command value into an integer message. + + + The message into which the command is packed. + + + The command value to pack into the message. + + + An integer message. + + + + + Gets the channel command value. + + + + + Gets the MIDI channel. + + + + + Gets the first data value. + + + + + Gets the second data value. + + + + + Gets the EventType. + + + + + Raw short message as int or byte array, useful when working with VST. + + + + + Defines constants representing MIDI message types. + + + + + Represents the basic functionality for all MIDI messages. + + + + + Gets a byte array representation of the MIDI message. + + + A byte array representation of the MIDI message. + + + + + Gets the MIDI message's status value. + + + + + Gets the MIDI event's type. + + + + + Delta samples when the event should be processed in the next audio buffer. + Leave at 0 for realtime input to play as fast as possible. + Set to the desired sample in the next buffer if you play a midi sequence synchronized to the audio callback + + + + + Provides functionality for building ChannelMessages. + + + + + Initializes a new instance of the ChannelMessageBuilder class. + + + + + Initializes a new instance of the ChannelMessageBuilder class with + the specified ChannelMessageEventArgs. + + + The ChannelMessageEventArgs to use for initializing the ChannelMessageBuilder. + + + The ChannelMessageBuilder uses the specified ChannelMessageEventArgs to + initialize its property values. + + + + + Initializes the ChannelMessageBuilder with the specified + ChannelMessageEventArgs. + + + The ChannelMessageEventArgs to use for initializing the ChannelMessageBuilder. + + + + + Clears the ChannelMessageEventArgs cache. + + + + + Gets the number of messages in the ChannelMessageEventArgs cache. + + + + + Gets the built ChannelMessageEventArgs. + + + + + Gets or sets the ChannelMessageEventArgs as a packed integer. + + + + + Gets or sets the Command value to use for building the + ChannelMessageEventArgs. + + + + + Gets or sets the MIDI channel to use for building the + ChannelMessageEventArgs. + + + MidiChannel is set to a value less than zero or greater than 15. + + + + + Gets or sets the first data value to use for building the + ChannelMessageEventArgs. + + + Data1 is set to a value less than zero or greater than 127. + + + + + Gets or sets the second data value to use for building the + ChannelMessageEventArgs. + + + Data2 is set to a value less than zero or greater than 127. + + + + + Builds a ChannelMessageEventArgs. + + + + + Represents functionality for building MIDI messages. + + + + + Builds the MIDI message. + + + + + Builds key signature MetaMessages. + + + + + Initializes a new instance of the KeySignatureBuilder class. + + + + + Initializes a new instance of the KeySignatureBuilder class with + the specified key signature MetaMessage. + + + The key signature MetaMessage to use for initializing the + KeySignatureBuilder class. + + + + + Initializes the KeySignatureBuilder with the specified MetaMessage. + + + The key signature MetaMessage to use for initializing the + KeySignatureBuilder. + + + + + Gets or sets the key. + + + + + The build key signature MetaMessage. + + + + + Builds the key signature MetaMessage. + + + + + Provides functionality for building meta text messages. + + + + + Initializes a new instance of the MetaMessageTextBuilder class. + + + + + Initializes a new instance of the MetaMessageTextBuilder class with the + specified type. + + + The type of MetaMessage. + + + If the MetaMessage type is not a text based type. + + + The MetaMessage type must be one of the following text based + types: + + + Copyright + + + Cuepoint + + + DeviceName + + + InstrumentName + + + Lyric + + + Marker + + + ProgramName + + + Text + + + TrackName + + + If the MetaMessage is not a text based type, an exception + will be thrown. + + + + + Initializes a new instance of the MetaMessageTextBuilder class with the + specified type. + + + The type of MetaMessage. + + + If the MetaMessage type is not a text based type. + + + The MetaMessage type must be one of the following text based + types: + + + Copyright + + + Cuepoint + + + DeviceName + + + InstrumentName + + + Lyric + + + Marker + + + ProgramName + + + Text + + + TrackName + + + If the MetaMessage is not a text based type, an exception + will be thrown. + + + + + Initializes a new instance of the MetaMessageTextBuilder class with the + specified MetaMessage. + + + The MetaMessage to use for initializing the MetaMessageTextBuilder. + + + If the MetaMessage is not a text based type. + + + The MetaMessage must be one of the following text based types: + + + Copyright + + + Cuepoint + + + DeviceName + + + InstrumentName + + + Lyric + + + Marker + + + ProgramName + + + Text + + + TrackName + + + If the MetaMessage is not a text based type, an exception will be + thrown. + + + + + Initializes the MetaMessageTextBuilder with the specified MetaMessage. + + + The MetaMessage to use for initializing the MetaMessageTextBuilder. + + + If the MetaMessage is not a text based type. + + + + + Indicates whether or not the specified MetaType is a text based + type. + + + The MetaType to test. + + + true if the MetaType is a text based type; + otherwise, false. + + + + + Gets or sets the text for the MetaMessage. + + + + + Gets or sets the MetaMessage type. + + + If the type is not a text based type. + + + + + Gets the built MetaMessage. + + + + + Builds the text MetaMessage. + + + + + Provides functionality for building song position pointer messages. + + + + + Initializes a new instance of the SongPositionPointerBuilder class. + + + + + Initializes a new instance of the SongPositionPointerBuilder class + with the specified song position pointer message. + + + The song position pointer message to use for initializing the + SongPositionPointerBuilder. + + + If message is not a song position pointer message. + + + + + Initializes the SongPositionPointerBuilder with the specified + SysCommonMessage. + + + The SysCommonMessage to use to initialize the + SongPositionPointerBuilder. + + + If the SysCommonMessage is not a song position pointer message. + + + + + Gets or sets the sequence position in ticks. + + + Value is set to less than zero. + + + Note: the position in ticks value is converted to the song position + pointer value. Since the song position pointer has a lower + resolution than the position in ticks, there is a probable loss of + resolution when setting the position in ticks value. + + + + + Gets or sets the PulsesPerQuarterNote object. + + + Value is not a multiple of 24. + + + + + Gets or sets the song position. + + + Value is set to less than zero. + + + + + Gets the built song position pointer message. + + + + + Builds a song position pointer message. + + + + + Provides functionality for building SysCommonMessages. + + + + + Initializes a new instance of the SysCommonMessageBuilder class. + + + + + Initializes a new instance of the SysCommonMessageBuilder class + with the specified SystemCommonMessage. + + + The SysCommonMessage to use for initializing the + SysCommonMessageBuilder. + + + The SysCommonMessageBuilder uses the specified SysCommonMessage to + initialize its property values. + + + + + Initializes the SysCommonMessageBuilder with the specified + SysCommonMessage. + + + The SysCommonMessage to use for initializing the + SysCommonMessageBuilder. + + + + + Clears the SysCommonMessageBuilder cache. + + + + + Gets the number of messages in the SysCommonMessageBuilder cache. + + + + + Gets the built SysCommonMessage. + + + + + Gets or sets the SysCommonMessage as a packed integer. + + + + + Gets or sets the type of SysCommonMessage. + + + + + Gets or sets the first data value to use for building the + SysCommonMessage. + + + Data1 is set to a value less than zero or greater than 127. + + + + + Gets or sets the second data value to use for building the + SysCommonMessage. + + + Data2 is set to a value less than zero or greater than 127. + + + + + Builds a SysCommonMessage. + + + + + Provides functionality for building tempo messages. + + + + + Initializes a new instance of the TempoChangeBuilder class. + + + + + Initialize a new instance of the TempoChangeBuilder class with the + specified MetaMessage. + + + The MetaMessage to use for initializing the TempoChangeBuilder class. + + + If the specified MetaMessage is not a tempo type. + + + The TempoChangeBuilder uses the specified MetaMessage to initialize + its property values. + + + + + Initializes the TempoChangeBuilder with the specified MetaMessage. + + + The MetaMessage to use for initializing the TempoChangeBuilder. + + + If the specified MetaMessage is not a tempo type. + + + + + Gets or sets the tempo. + + + Value is set to less than zero. + + + + + Gets the built message. + + + + + Builds the tempo change MetaMessage. + + + + + Provides easy to use functionality for time signature MetaMessages. + + + + + Initializes a new instance of the TimeSignatureBuilder class. + + + + + Initializes a new instance of the TimeSignatureBuilder class with the + specified MetaMessage. + + + The MetaMessage to use for initializing the TimeSignatureBuilder class. + + + If the specified MetaMessage is not a time signature type. + + + The TimeSignatureBuilder uses the specified MetaMessage to + initialize its property values. + + + + + Initializes the TimeSignatureBuilder with the specified MetaMessage. + + + The MetaMessage to use for initializing the TimeSignatureBuilder. + + + If the specified MetaMessage is not a time signature type. + + + + + Gets or sets the numerator. + + + Numerator is set to a value less than one. + + + + + Gets or sets the denominator. + + + Denominator is set to a value less than 2. + + + Denominator is set to a value that is not a power of 2. + + + + + Gets or sets the clocks per metronome click. + + + Clocks per metronome click determines how many MIDI clocks occur + for each metronome click. + + + + + Gets or sets how many thirty second notes there are for each + quarter note. + + + + + Gets the built message. + + + + + Builds the time signature MetaMessage. + + + + + Dispatches IMidiMessages to their corresponding sink. + + + + + Dispatches IMidiMessages to their corresponding sink. + + + The IMidiMessage to dispatch. + + + + + Represents MetaMessage types. + + + + + Represents sequencer number type. + + + + + Represents the text type. + + + + + Represents the copyright type. + + + + + Represents the track name type. + + + + + Represents the instrument name type. + + + + + Represents the lyric type. + + + + + Represents the marker type. + + + + + Represents the cue point type. + + + + + Represents the program name type. + + + + + Represents the device name type. + + + + + Represents then end of track type. + + + + + Represents the tempo type. + + + + + Represents the Smpte offset type. + + + + + Represents the time signature type. + + + + + Represents the key signature type. + + + + + Represents the proprietary event type. + + + + + Represents MIDI meta messages. + + + Meta messages are MIDI messages that are stored in MIDI files. These + messages are not sent or received via MIDI but are read and + interpretted from MIDI files. They provide information that describes + a MIDI file's properties. For example, tempo changes are implemented + using meta messages. + + + + + The amount to shift data bytes when calculating the hash code. + + + + + Length in bytes for tempo meta message data. + + + + + Length in bytes for SMPTE offset meta message data. + + + + + Length in bytes for time signature meta message data. + + + + + Length in bytes for key signature meta message data. + + + + + End of track meta message. + + + + + Initializes a new instance of the MetaMessage class. + + + The type of MetaMessage. + + + The MetaMessage data. + + + The length of the MetaMessage is not valid for the MetaMessage type. + + + Each MetaMessage has type and length properties. For certain + types, the length of the message data must be a specific value. For + example, tempo messages must have a data length of exactly three. + Some MetaMessage types can have any data length. Text messages are + an example of a MetaMessage that can have a variable data length. + When a MetaMessage is created, the length of the data is checked + to make sure that it is valid for the specified type. If it is not, + an exception is thrown. + + + + + Gets a copy of the data bytes for this meta message. + + + A copy of the data bytes for this meta message. + + + + + Returns a value for the current MetaMessage suitable for use in + hashing algorithms. + + + A hash code for the current MetaMessage. + + + + + Determines whether two MetaMessage instances are equal. + + + The MetaMessage to compare with the current MetaMessage. + + + true if the specified MetaMessage is equal to the current + MetaMessage; otherwise, false. + + + + + Validates data length. + + + The MetaMessage type. + + + The length of the MetaMessage data. + + + true if the data length is valid for this type of + MetaMessage; otherwise, false. + + + + + Gets the element at the specified index. + + + index is less than zero or greater than or equal to Length. + + + + + Gets the length of the meta message. + + + + + Gets the type of meta message. + + + + + Gets the status value. + + + + + Gets the MetaMessage's MessageType. + + + + + MidiSignal provides all midi events from an input device + + + + + Create Midisignal with an input device which fires the events + + + + + + All incoming midi messages in short format + + + + + All incoming midi messages in short format + + + + + Channel messages like, note, controller, program, ... + + + + + SysEx messages + + + + + Midi timecode, song position, song select, tune request + + + + + Timing events, midi clock, start, stop, reset, active sense, tick + + + + + Takes a number of MidiEvents and merges them into a new single MidiEvent source + + + + + An event source that combines all possible midi events + + + + + Gets the device identifier of the input devive. + Set it to any negative value for custom event sources. + + + + + Occurs when any message was received. The underlying type of the message should be as specific as possible. + Channel, Common, Realtime or SysEx. + + + + + All incoming midi short messages + + + + + Channel messages like, note, controller, program, ... + + + + + SysEx messages + + + + + Midi timecode, song position, song select, tune request + + + + + Timing events, midi clock, start, stop, reset, active sense, tick + + + + + Event sink that sends midi messages to an output device + + + + + Disposes the underying output device and removes the events from the source + + + + + Delta samples when the event should be processed in the next audio buffer. + Leave at 0 for realtime input to play as fast as possible. + Set to the desired sample in the next buffer if you play a midi sequence synchronized to the audio callback + + + + + Represents the basic class for all MIDI short messages. + + + MIDI short messages represent all MIDI messages except meta messages + and system exclusive messages. This includes channel messages, system + realtime messages, and system common messages. + + + + + Gets the timestamp of the midi input driver in milliseconds since the midi input driver was started. + + + The timestamp in milliseconds since the midi input driver was started. + + + + + Gets the short message as a packed integer. + + + The message is packed into an integer value with the low-order byte + of the low-word representing the status value. The high-order byte + of the low-word represents the first data value, and the low-order + byte of the high-word represents the second data value. + + + + + Gets the messages's status value. + + + + + Defines constants representing the various system common message types. + + + + + Represents the MTC system common message type. + + + + + Represents the song position pointer type. + + + + + Represents the song select type. + + + + + Represents the tune request type. + + + + + Represents MIDI system common messages. + + + + + Initializes a new instance of the SysCommonMessage class with the + specified type. + + + The type of SysCommonMessage. + + + + + Initializes a new instance of the SysCommonMessage class with the + specified type and the first data value. + + + The type of SysCommonMessage. + + + The first data value. + + + If data1 is less than zero or greater than 127. + + + + + Initializes a new instance of the SysCommonMessage class with the + specified type, first data value, and second data value. + + + The type of SysCommonMessage. + + + The first data value. + + + The second data value. + + + If data1 or data2 is less than zero or greater than 127. + + + + + Returns a value for the current SysCommonMessage suitable for use + in hashing algorithms. + + + A hash code for the current SysCommonMessage. + + + + + Determines whether two SysCommonMessage instances are equal. + + + The SysCommonMessage to compare with the current SysCommonMessage. + + + true if the specified SysCommonMessage is equal to the + current SysCommonMessage; otherwise, false. + + + + + Gets the SysCommonType. + + + + + Gets the first data value. + + + + + Gets the second data value. + + + + + Gets the MessageType. + + + + + Defines constants representing various system exclusive message types. + + + + + Represents the start of system exclusive message type. + + + + + Represents the continuation of a system exclusive message. + + + + + Represents MIDI system exclusive messages. + + + + + Maximum value for system exclusive channels. + + + + + Initializes a new instance of the SysExMessageEventArgs class with the + specified system exclusive data. + + + The system exclusive data. + + + The system exclusive data's status byte, the first byte in the + data, must have a value of 0xF0 or 0xF7. + + + + + Gets the timestamp of the midi input driver in milliseconds since the midi input driver was started. + + + The timestamp in milliseconds since the midi input driver was started. + + + + + Gets the element at the specified index. + + + If index is less than zero or greater than or equal to the length + of the message. + + + + + Gets the length of the system exclusive data. + + + + + Gets the system exclusive type. + + + + + Gets the status value. + + + + + Gets the MessageType. + + + + + Defines constants representing the various system realtime message types. + + + + + Represents the clock system realtime type. + + + + + Represents the tick system realtime type. + + + + + Represents the start system realtime type. + + + + + Represents the continue system realtime type. + + + + + Represents the stop system realtime type. + + + + + Represents the active sense system realtime type. + + + + + Represents the reset system realtime type. + + + + + Represents MIDI system realtime messages. + + + System realtime messages are MIDI messages that are primarily concerned + with controlling and synchronizing MIDI devices. + + + + + The instance of the system realtime start message. + + + + + The instance of the system realtime continue message. + + + + + The instance of the system realtime stop message. + + + + + The instance of the system realtime clock message. + + + + + The instance of the system realtime tick message. + + + + + The instance of the system realtime active sense message. + + + + + The instance of the system realtime reset message. + + + + + Returns a value for the current SysRealtimeMessage suitable for use in + hashing algorithms. + + + A hash code for the current SysRealtimeMessage. + + + + + Determines whether two SysRealtimeMessage instances are equal. + + + The SysRealtimeMessage to compare with the current SysRealtimeMessage. + + + true if the specified SysRealtimeMessage is equal to the current + SysRealtimeMessage; otherwise, false. + + + + + Gets the SysRealtimeType. + + + + + Gets the MessageType. + + + + + Converts a MIDI note number to its corresponding frequency. + + + + + The minimum value a note ID can have. + + + + + The maximum value a note ID can have. + + + + + Converts the specified note to a frequency. + + + The ID of the note to convert. + + + The frequency of the specified note. + + + + + Converts the specified frequency to a note. + + + The frequency to convert. + + + The ID of the note closest to the specified frequency. + + + + + Defintes constants representing SMPTE frame rates. + + + + + The different types of sequences. + + + + + Represents MIDI file properties. + + + + + Represents a collection of Tracks. + + + + + Initializes a new instance of the Sequence class. + + + + + Initializes a new instance of the Sequence class with the specified division. + + + The Sequence's division value. + + + + + Initializes a new instance of the Sequence class with the specified + file name of the MIDI file to load. + + + The name of the MIDI file to load. + + + + + Initializes a new instance of the Sequence class with the specified + file stream of the MIDI file to load. + + + The stream of the MIDI file to load. + + + + + Loads a MIDI file into the Sequence. + + + The MIDI file's name. + + + + + Loads a MIDI stream into the Sequence. + + + The MIDI file's stream. + + + + + Saves the Sequence as a MIDI file. + + + The name to use for saving the MIDI file. + + + + + Saves the Sequence as a Stream. + + + The stream to use for saving the sequence. + + + + + Gets the length in ticks of the Sequence. + + + The length in ticks of the Sequence. + + + The length in ticks of the Sequence is represented by the Track + with the longest length. + + + + + Gets the Track at the specified index. + + + The index of the Track to get. + + + The Track at the specified index. + + + + + Gets the Sequence's division value. + + + + + Gets or sets the Sequence's format value. + + + + + Gets the Sequence's type. + + + + + Represents a collection of MidiEvents and a MIDI track within a + Sequence. + + + + + Inserts an IMidiMessage at the specified position in absolute ticks. + + + The position in the Track in absolute ticks in which to insert the + IMidiMessage. + + + The IMidiMessage to insert. + + + + + Clears all of the MidiEvents, with the exception of the end of track + message, from the Track. + + + + + Merges the specified Track with the current Track. + + + The Track to merge with. + + + + + Removes the MidiEvent at the specified index. + + + The index into the Track at which to remove the MidiEvent. + + + + + Gets the MidiEvent at the specified index. + + + The index of the MidiEvent to get. + + + The MidiEvent at the specified index. + + + + + Gets the number of MidiEvents in the Track. + + + + + Gets the length of the Track in ticks. + + + + + Gets or sets the end of track meta message position offset. + + + + + Gets an object that can be used to synchronize access to the Track. + + + + + Reads a track from a stream. + + + + + Writes a Track to a Stream. + + + + + Gets or sets the Track to write to the Stream. + + + + + Gets a value indicating whether the Timer is running. + + + + + Gets the timer mode. + + + If the timer has already been disposed. + + + + + Period between timer events in milliseconds. + + + + + Resolution of the timer in milliseconds. + + + + + Gets or sets the object used to marshal event-handler calls. + + + + + Occurs when the Timer has started; + + + + + Occurs when the Timer has stopped; + + + + + Occurs when the time period has elapsed. + + + + + Starts the timer. + + + The timer has already been disposed. + + + The timer failed to start. + + + + + Stops timer. + + + If the timer has already been disposed. + + + + + Replacement for the Windows multimedia timer that also runs on Mono + + + + + Gets or sets the object used to marshal event-handler calls. + + + + + Queues and executes timer events in an internal worker thread. + + + + + The thread to execute the timer events + + + + + Defines constants representing the timing format used by the Time struct. + + + + + Represents the Windows Multimedia MMTIME structure. + + + + + Defines constants for the multimedia Timer's event types. + + + + + Timer event occurs once. + + + + + Timer event occurs periodically. + + + + + Represents information about the multimedia Timer's capabilities. + + + + + Minimum supported period in milliseconds. + + + + + Maximum supported period in milliseconds. + + + + + Represents the Windows multimedia timer. + + + + + Occurs when the Timer has started; + + + + + Occurs when the Timer has stopped; + + + + + Occurs when the time period has elapsed. + + + + + Initialize class. + + + + + Initializes a new instance of the Timer class with the specified IContainer. + + + The IContainer to which the Timer will add itself. + + + + + Initializes a new instance of the Timer class. + + + + + Starts the timer. + + + The timer has already been disposed. + + + The timer failed to start. + + + + + Stops timer. + + + If the timer has already been disposed. + + + + + Gets or sets the object used to marshal event-handler calls. + + + + + Gets or sets the time between Tick events. + + + If the timer has already been disposed. + + + + + Gets or sets the timer resolution. + + + If the timer has already been disposed. + + + The resolution is in milliseconds. The resolution increases + with smaller values; a resolution of 0 indicates periodic events + should occur with the greatest possible accuracy. To reduce system + overhead, however, you should use the maximum value appropriate + for your application. + + + + + Gets the timer mode. + + + If the timer has already been disposed. + + + + + Gets a value indicating whether the Timer is running. + + + + + Gets the timer capabilities. + + + + + Frees timer resources. + + + + + The exception that is thrown when a timer fails to start. + + + + + Initializes a new instance of the TimerStartException class. + + + The error message that explains the reason for the exception. + + + + + Use this factory to create ITimer instances. + + Caller is responsible for Dispose. + + + + Creates an instance of ITimer + + Newly created instance of ITimer + + + + Defines the public abstract 'Device' class to interface System.IDisposable. + + + + + This protected construct, uses a Callback Function integer if it's equal to value 0x30000. + + + + + This protected construct, uses a Callback Event integer if it's equal to value 0x50000. + + + + + Synchronizes the context. + + + + + Outputs an error via ErrorEventArgs, if the EventHandler encounters an issue. + + + + + This public function utilises the Device ID integer with SynchronizationContext. + + + + + Utilises system garbage collector (System.GC) to dispose memory when the boolean value is set to true. + + + + + Error handling function. + + + + + Closes the MIDI device. + + + + + Resets the device. + + + + + Gets the device handle. + + + + + Calls the DeviceID public integer. + + + + + Declares the device as disposed. + + + + + Disposes of the device. + + + + + Refers the System.ApplicationException as DeviceException. + + + + No error. + + + Unspecified error. + + + Device ID out of range. + + + Driver failed enable. + + + Device already allocated. + + + Device handle is invalid. + + + No device driver present. + + + Memory allocation error. + + + Function isn't supported. + + + Error value out of range. + + + Invalid flag passed. + + + Invalid parameter passed. + + + + Handle being used.

+ Simultaneously on another.

+ Thread (eg callback).

+
+
+ + Specified alias not found. + + + Bad registry database. + + + Registry key not found. + + + Registry read error. + + + Registry write error. + + + Registry delete error. + + + Registry value not found. + + + Driver does not call DriverCallback. + + + Last error. + + + + Calls the Device Exception error code. + + + + + Public integer for the error code. + + + + + Defines constants for all major and minor keys. + + + + + Defines constants representing the 12 Note of the chromatic scale. + + + + + C natural. + + + + + C sharp. + + + + + D flat. + + + + + D natural. + + + + + D sharp. + + + + + E flat. + + + + + E natural. + + + + + F natural. + + + + + F sharp. + + + + + G flat. + + + + + G natural. + + + + + G sharp. + + + + + A flat. + + + + + A natural. + + + + + A sharp. + + + + + B flat. + + + + + B natural. + + + + + Provides basic implementation of the IAsyncResult interface. + + + + + Initializes a new instance of the AsyncResult object with the + specified owner of the AsyncResult object, the optional callback + delegate, and optional state object. + + + The owner of the AsyncResult object. + + + An optional asynchronous callback, to be called when the + operation is complete. + + + A user-provided object that distinguishes this particular + asynchronous request from other requests. + + + + + Signals that the operation has completed. + + + + + Gets the owner of this AsyncResult object. + + + + + Represents an asynchronous queue of delegates. + + + + + Implements the IAsyncResult interface for the DelegateQueue class. + + + + + Occurs after a method has been invoked as a result of a call to + the BeginInvoke or BeginInvokePriority methods. + + + + + Occurs after a method has been invoked as a result of a call to + the Post and PostPriority methods. + + + + + Initializes a new instance of the DelegateQueue class. + + + + + Initializes a new instance of the DelegateQueue class with the specified IContainer object. + + + The IContainer to which the DelegateQueue will add itself. + + + + + Executes the delegate on the main thread that this object executes on. + + + A Delegate to a method that takes parameters of the same number and + type that are contained in args. + + + An array of type Object to pass as arguments to the given method. + + + An IAsyncResult interface that represents the asynchronous operation + started by calling this method. + + + The delegate is placed at the beginning of the queue. Its invocation + takes priority over delegates already in the queue. + + + + + Executes the delegate on the main thread that this object executes on. + + + A Delegate to a method that takes parameters of the same number and + type that are contained in args. + + + An array of type Object to pass as arguments to the given method. + + + An IAsyncResult interface that represents the asynchronous operation + started by calling this method. + + + + The delegate is placed at the beginning of the queue. Its invocation + takes priority over delegates already in the queue. + + + Unlike BeginInvoke, this method operates synchronously, that is, it + waits until the process completes before returning. Exceptions raised + during the call are propagated back to the caller. + + + + + + Executes the delegate on the main thread that this object executes on. + + + An optional asynchronous callback, to be called when the method is invoked. + + + A user-provided object that distinguishes this particular asynchronous invoke request from other requests. + + + A Delegate to a method that takes parameters of the same number and + type that are contained in args. + + + An array of type Object to pass as arguments to the given method. + + + An IAsyncResult interface that represents the asynchronous operation + started by calling this method. + + + + + Dispatches an asynchronous message to this synchronization context. + + + The SendOrPostCallback delegate to call. + + + The object passed to the delegate. + + + The Post method starts an asynchronous request to post a message. + + + + + Dispatches an synchronous message to this synchronization context. + + + The SendOrPostCallback delegate to call. + + + The object passed to the delegate. + + + + + Dispatches a synchronous message to this synchronization context. + + + The SendOrPostCallback delegate to call. + + + The object passed to the delegate. + + + The Send method starts an synchronous request to send a message. + + + + + Dispatches an asynchronous message to this synchronization context. + + + The SendOrPostCallback delegate to call. + + + The object passed to the delegate. + + + The Post method starts an asynchronous request to post a message. + + + + + Represents the method that handles the Disposed delegate of a DelegateQueue. + + + + + Gets or sets the ISite associated with the DelegateQueue. + + + + + Executes the delegate on the main thread that this DelegateQueue executes on. + + + A Delegate to a method that takes parameters of the same number and type that + are contained in args. + + + An array of type Object to pass as arguments to the given method. This can be + a null reference (Nothing in Visual Basic) if no arguments are needed. + + + An IAsyncResult interface that represents the asynchronous operation started + by calling this method. + + + The delegate is called asynchronously, and this method returns immediately. + You can call this method from any thread. If you need the return value from a process + started with this method, call EndInvoke to get the value. + If you need to call the delegate synchronously, use the Invoke method instead. + + + + + Waits until the process started by calling BeginInvoke completes, and then returns + the value generated by the process. + + + An IAsyncResult interface that represents the asynchronous operation started + by calling BeginInvoke. + + + An Object that represents the return value generated by the asynchronous operation. + + + This method gets the return value of the asynchronous operation represented by the + IAsyncResult passed by this interface. If the asynchronous operation has not completed, this method will wait until the result is available. + + + + + Executes the delegate on the main thread that this DelegateQueue executes on. + + + A Delegate that contains a method to call, in the context of the thread for the DelegateQueue. + + + An array of type Object that represents the arguments to pass to the given method. + + + An Object that represents the return value from the delegate being invoked, or a + null reference (Nothing in Visual Basic) if the delegate has no return value. + + + Unlike BeginInvoke, this method operates synchronously, that is, it waits until + the process completes before returning. Exceptions raised during the call are propagated + back to the caller. + Use this method when calling a method from a different thread to marshal the call + to the proper thread. + + + + + Gets a value indicating whether the caller must call Invoke. + + + true if the caller must call Invoke; otherwise, false. + + + This property determines whether the caller must call Invoke when making + method calls to this DelegateQueue. If you are calling a method from a different + thread, you must use the Invoke method to marshal the call to the proper thread. + + + + + Disposes of the DelegateQueue. + + + + + Provides functionality for timestamped delegate invocation. + + + + + A constant value representing an unlimited number of delegate invocations. + + + + + Raised when a delegate is invoked. + + + + + Initializes a new instance of the DelegateScheduler class. + + + + + Initializes a new instance of the DelegateScheduler class with the + specified IContainer. + + + + + Adds a delegate to the DelegateScheduler. + + + The number of times the delegate should be invoked. + + + The time in milliseconds between delegate invocation. + + + + The delegate to invoke. + + The arguments to pass to the delegate when it is invoked. + + + A Task object representing the scheduled task. + + + If the DelegateScheduler has already been disposed. + + + If an unlimited count is desired, pass the DelegateScheduler.Infinity + constant as the count argument. + + + + + Removes the specified Task. + + + The Task to be removed. + + + If the DelegateScheduler has already been disposed. + + + + + Starts the DelegateScheduler. + + + If the DelegateScheduler has already been disposed. + + + + + Stops the DelegateScheduler. + + + If the DelegateScheduler has already been disposed. + + + + + Clears the DelegateScheduler of all tasks. + + + If the DelegateScheduler has already been disposed. + + + + + Gets or sets the interval in milliseconds in which the + DelegateScheduler polls its queue of delegates in order to + determine when they should run. + + + + + Gets a value indicating whether the DelegateScheduler is running. + + + + + Gets or sets the object used to marshal event-handler calls and delegate invocations. + + + + + Represents information about the InvokeCompleted event. + + +
+
diff --git a/SoundFont2/SF2.cs b/SoundFont2/SF2.cs new file mode 100644 index 00000000..3bc550c9 --- /dev/null +++ b/SoundFont2/SF2.cs @@ -0,0 +1,150 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.SoundFont2 +{ + public sealed class SF2 + { + private uint _size; + public InfoListChunk InfoChunk { get; } + public SdtaListChunk SoundChunk { get; } + public PdtaListChunk HydraChunk { get; } + + /// For creating + public SF2() + { + InfoChunk = new InfoListChunk(this); + SoundChunk = new SdtaListChunk(this); + HydraChunk = new PdtaListChunk(this); + } + + /// For reading + public SF2(string path) + { + var reader = new EndianBinaryReader(File.Open(path, FileMode.Open)); + string str = reader.ReadString(4, false); + if (str != "RIFF") + { + throw new InvalidDataException("RIFF header was not found at the start of the file."); + } + _size = reader.ReadUInt32(); + str = reader.ReadString(4, false); + if (str != "sfbk") + { + throw new InvalidDataException("sfbk header was not found at the expected offset."); + } + InfoChunk = new InfoListChunk(this, reader); + SoundChunk = new SdtaListChunk(this, reader); + HydraChunk = new PdtaListChunk(this, reader); + } + + public void Save(string path) + { + var writer = new EndianBinaryWriter(File.Open(path, FileMode.Create)); + { + AddTerminals(); + + writer.Write("RIFF", 4); + writer.Write(_size); + writer.Write("sfbk", 4); + + InfoChunk.Write(writer); + SoundChunk.Write(writer); + HydraChunk.Write(writer); + } + } + + + /// Returns sample index + public uint AddSample(short[] pcm16, string name, bool bLoop, uint loopPos, uint sampleRate, byte originalKey, sbyte pitchCorrection) + { + uint start = SoundChunk.SMPLSubChunk.AddSample(pcm16, bLoop, loopPos); + // If the sample is looped the standard requires us to add the 8 bytes from the start of the loop to the end + uint end, loopEnd, loopStart; + + uint len = (uint)pcm16.Length; + if (bLoop) + { + end = start + len + 8; + loopStart = start + loopPos; loopEnd = start + len; + } + else + { + end = start + len; + loopStart = 0; loopEnd = 0; + } + + return AddSampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection); + } + /// Returns instrument index + public uint AddInstrument(string name) + { + return HydraChunk.INSTSubChunk.AddInstrument(new SF2Instrument(name, (ushort)HydraChunk.IBAGSubChunk.Count)); + } + public void AddInstrumentBag() + { + HydraChunk.IBAGSubChunk.AddBag(new SF2Bag(this, false)); + } + public void AddInstrumentModulator() + { + HydraChunk.IMODSubChunk.AddModulator(new SF2ModulatorList()); + } + public void AddInstrumentGenerator() + { + HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList()); + } + public void AddInstrumentGenerator(SF2Generator generator, SF2GeneratorAmount amount) + { + HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); + } + public void AddPreset(string name, ushort preset, ushort bank) + { + HydraChunk.PHDRSubChunk.AddPreset(new SF2PresetHeader(name, preset, bank, (ushort)HydraChunk.PBAGSubChunk.Count)); + } + public void AddPresetBag() + { + HydraChunk.PBAGSubChunk.AddBag(new SF2Bag(this, true)); + } + public void AddPresetModulator() + { + HydraChunk.PMODSubChunk.AddModulator(new SF2ModulatorList()); + } + public void AddPresetGenerator() + { + HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList()); + } + public void AddPresetGenerator(SF2Generator generator, SF2GeneratorAmount amount) + { + HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); + } + + private uint AddSampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) + { + return HydraChunk.SHDRSubChunk.AddSample(new SF2SampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection)); + } + private void AddTerminals() + { + AddSampleHeader("EOS", 0, 0, 0, 0, 0, 0, 0); + AddInstrument("EOI"); + AddInstrumentBag(); + AddInstrumentGenerator(); + AddInstrumentModulator(); + AddPreset("EOP", 0xFF, 0xFF); + AddPresetBag(); + AddPresetGenerator(); + AddPresetModulator(); + } + + internal void UpdateSize() + { + if (InfoChunk == null || SoundChunk == null || HydraChunk == null) + { + return; + } + _size = 4 + + InfoChunk.UpdateSize() + 8 + + SoundChunk.UpdateSize() + 8 + + HydraChunk.UpdateSize() + 8; + } + } +} diff --git a/SoundFont2/SF2Chunks.cs b/SoundFont2/SF2Chunks.cs new file mode 100644 index 00000000..96013042 --- /dev/null +++ b/SoundFont2/SF2Chunks.cs @@ -0,0 +1,1105 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; + +namespace Kermalis.SoundFont2 +{ + public class SF2Chunk + { + protected readonly SF2 _sf2; + + /// Length 4 + public string ChunkName { get; } + /// Size in bytes + public uint Size { get; protected set; } + + protected SF2Chunk(SF2 inSf2, string name) + { + _sf2 = inSf2; + ChunkName = name; + } + protected SF2Chunk(SF2 inSf2, EndianBinaryReader reader) + { + _sf2 = inSf2; + ChunkName = reader.ReadString(4, false); + Size = reader.ReadUInt32(); + } + + internal virtual void Write(EndianBinaryWriter writer) + { + writer.Write(ChunkName, 4); + writer.Write(Size); + } + } + + public abstract class SF2ListChunk : SF2Chunk + { + ///Length 4 + public string ListChunkName { get; } + + protected SF2ListChunk(SF2 inSf2, string name) : base(inSf2, "LIST") + { + ListChunkName = name; + Size = 4; + } + protected SF2ListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + ListChunkName = reader.ReadString(4, false); + } + + internal abstract uint UpdateSize(); + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(ListChunkName, 4); + } + } + + public sealed class SF2PresetHeader + { + public const uint Size = 38; + + /// Length 20 + public string PresetName { get; set; } + public ushort Preset { get; set; } + public ushort Bank { get; set; } + public ushort PresetBagIndex { get; set; } + // Reserved for future implementations + private readonly uint _library; + private readonly uint _genre; + private readonly uint _morphology; + + internal SF2PresetHeader(string name, ushort preset, ushort bank, ushort index) + { + PresetName = name; + Preset = preset; + Bank = bank; + PresetBagIndex = index; + } + internal SF2PresetHeader(EndianBinaryReader reader) + { + PresetName = reader.ReadString(20, true); + Preset = reader.ReadUInt16(); + Bank = reader.ReadUInt16(); + PresetBagIndex = reader.ReadUInt16(); + _library = reader.ReadUInt32(); + _genre = reader.ReadUInt32(); + _morphology = reader.ReadUInt32(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(PresetName, 20); + writer.Write(Preset); + writer.Write(Bank); + writer.Write(PresetBagIndex); + writer.Write(_library); + writer.Write(_genre); + writer.Write(_morphology); + } + + public override string ToString() + { + return $"Preset Header - Bank = {Bank}" + + $",\nPreset = {Preset}" + + $",\nName = \"{PresetName}\""; + } + } + + /// Covers sfPresetBag and sfInstBag + public sealed class SF2Bag + { + public const uint Size = 4; + + /// Index in list of generators + public ushort GeneratorIndex { get; set; } + /// Index in list of modulators + public ushort ModulatorIndex { get; set; } + + internal SF2Bag(SF2 inSf2, bool isPresetBag) + { + if (isPresetBag) + { + GeneratorIndex = (ushort)inSf2.HydraChunk.PGENSubChunk.Count; + ModulatorIndex = (ushort)inSf2.HydraChunk.PMODSubChunk.Count; + } + else + { + GeneratorIndex = (ushort)inSf2.HydraChunk.IGENSubChunk.Count; + ModulatorIndex = (ushort)inSf2.HydraChunk.IMODSubChunk.Count; + } + } + internal SF2Bag(EndianBinaryReader reader) + { + GeneratorIndex = reader.ReadUInt16(); + ModulatorIndex = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(GeneratorIndex); + writer.Write(ModulatorIndex); + } + + public override string ToString() + { + return $"Bag - Generator index = {GeneratorIndex}" + + $",\nModulator index = {ModulatorIndex}"; + } + } + + /// Covers sfModList and sfInstModList + public sealed class SF2ModulatorList + { + public const uint Size = 10; + + public SF2Modulator ModulatorSource { get; set; } + public SF2Generator ModulatorDestination { get; set; } + public short ModulatorAmount { get; set; } + public SF2Modulator ModulatorAmountSource { get; set; } + public SF2Transform ModulatorTransform { get; set; } + + internal SF2ModulatorList() { } + internal SF2ModulatorList(EndianBinaryReader reader) + { + ModulatorSource = reader.ReadEnum(); + ModulatorDestination = reader.ReadEnum(); + ModulatorAmount = reader.ReadInt16(); + ModulatorAmountSource = reader.ReadEnum(); + ModulatorTransform = reader.ReadEnum(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(ModulatorSource); + writer.Write(ModulatorDestination); + writer.Write(ModulatorAmount); + writer.Write(ModulatorAmountSource); + writer.Write(ModulatorTransform); + } + + public override string ToString() + { + return $"Modulator List - Modulator source = {ModulatorSource}" + + $",\nModulator destination = {ModulatorDestination}" + + $",\nModulator amount = {ModulatorAmount}" + + $",\nModulator amount source = {ModulatorAmountSource}" + + $",\nModulator transform = {ModulatorTransform}"; + } + } + + public sealed class SF2GeneratorList + { + public const uint Size = 4; + + public SF2Generator Generator { get; set; } + public SF2GeneratorAmount GeneratorAmount { get; set; } + + internal SF2GeneratorList() { } + internal SF2GeneratorList(SF2Generator generator, SF2GeneratorAmount amount) + { + Generator = generator; + GeneratorAmount = amount; + } + internal SF2GeneratorList(EndianBinaryReader reader) + { + Generator = reader.ReadEnum(); + GeneratorAmount = new SF2GeneratorAmount { Amount = reader.ReadInt16() }; + } + + public void Write(EndianBinaryWriter writer) + { + writer.Write(Generator); + writer.Write(GeneratorAmount.Amount); + } + + public override string ToString() + { + return $"Generator List - Generator = {Generator}" + + $",\nGenerator amount = \"{GeneratorAmount}\""; + } + } + + public sealed class SF2Instrument + { + public const uint Size = 22; + + /// Length 20 + public string InstrumentName { get; set; } + public ushort InstrumentBagIndex { get; set; } + + internal SF2Instrument(string name, ushort index) + { + InstrumentName = name; + InstrumentBagIndex = index; + } + internal SF2Instrument(EndianBinaryReader reader) + { + InstrumentName = reader.ReadString(20, true); + InstrumentBagIndex = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(InstrumentName, 20); + writer.Write(InstrumentBagIndex); + } + + public override string ToString() + { + return $"Instrument - Name = \"{InstrumentName}\""; + } + } + + public sealed class SF2SampleHeader + { + public const uint Size = 46; + + /// Length 20 + public string SampleName { get; set; } + public uint Start { get; set; } + public uint End { get; set; } + public uint LoopStart { get; set; } + public uint LoopEnd { get; set; } + public uint SampleRate { get; set; } + public byte OriginalKey { get; set; } + public sbyte PitchCorrection { get; set; } + public ushort SampleLink { get; set; } + public SF2SampleLink SampleType { get; set; } + + internal SF2SampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) + { + SampleName = name; + Start = start; + End = end; + LoopStart = loopStart; + LoopEnd = loopEnd; + SampleRate = sampleRate; + OriginalKey = originalKey; + PitchCorrection = pitchCorrection; + SampleType = SF2SampleLink.MonoSample; + } + internal SF2SampleHeader(EndianBinaryReader reader) + { + SampleName = reader.ReadString(20, true); + Start = reader.ReadUInt32(); + End = reader.ReadUInt32(); + LoopStart = reader.ReadUInt32(); + LoopEnd = reader.ReadUInt32(); + SampleRate = reader.ReadUInt32(); + OriginalKey = reader.ReadByte(); + PitchCorrection = reader.ReadSByte(); + SampleLink = reader.ReadUInt16(); + SampleType = reader.ReadEnum(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(SampleName, 20); + writer.Write(Start); + writer.Write(End); + writer.Write(LoopStart); + writer.Write(LoopEnd); + writer.Write(SampleRate); + writer.Write(OriginalKey); + writer.Write(PitchCorrection); + writer.Write(SampleLink); + writer.Write(SampleType); + } + + public override string ToString() + { + return $"Sample - Name = \"{SampleName}\"" + + $",\nType = {SampleType}"; + } + } + + #region Sub-Chunks + + public sealed class VersionSubChunk : SF2Chunk + { + public SF2VersionTag Version { get; set; } + + internal VersionSubChunk(SF2 inSf2, string subChunkName) : base(inSf2, subChunkName) + { + Size = SF2VersionTag.Size; + inSf2.UpdateSize(); + } + internal VersionSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + Version = new SF2VersionTag(reader); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + Version.Write(writer); + } + + public override string ToString() + { + return $"Version Chunk - Revision = {Version}"; + } + } + + public sealed class HeaderSubChunk : SF2Chunk + { + public int MaxSize { get; } + private int _fieldTargetLength; + private string _field; + /// Length + public string Field + { + get => _field; + set + { + if (value.Length >= MaxSize) // Input too long; cut it down + { + _fieldTargetLength = MaxSize; + } + else if (value.Length % 2 == 0) // Even amount of characters + { + _fieldTargetLength = value.Length + 2; // Add two null-terminators to keep the byte count even + } + else // Odd amount of characters + { + _fieldTargetLength = value.Length + 1; // Add one null-terminator since that would make byte the count even + } + _field = value; + Size = (uint)_fieldTargetLength; + _sf2.UpdateSize(); + } + } + + internal HeaderSubChunk(SF2 inSf2, string subChunkName, int maxSize = 0x100) : base(inSf2, subChunkName) + { + MaxSize = maxSize; + } + internal HeaderSubChunk(SF2 inSf2, EndianBinaryReader reader, int maxSize = 0x100) : base(inSf2, reader) + { + MaxSize = maxSize; + _field = reader.ReadString((int)Size, true); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + writer.Write(_field, _fieldTargetLength); + } + + public override string ToString() + { + return $"Header Chunk - Name = \"{ChunkName}\"" + + $",\nField Max Size = {MaxSize}" + + $",\nField = \"{Field}\""; + } + } + + public sealed class SMPLSubChunk : SF2Chunk + { + private readonly List _samples = new List(); // Block of sample data + + internal SMPLSubChunk(SF2 inSf2) : base(inSf2, "smpl") { } + internal SMPLSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / sizeof(short); i++) + { + _samples.Add(reader.ReadInt16()); + } + } + + // Returns index of the start of the sample + internal uint AddSample(short[] pcm16, bool bLoop, uint loopPos) + { + uint start = (uint)_samples.Count; + + // Write wave + _samples.AddRange(pcm16); + + // If looping is enabled, write 8 samples from the loop point + if (bLoop) + { + // In case (loopPos + i) is greater than the sample length + uint max = (uint)pcm16.Length - loopPos; + for (uint i = 0; i < 8; i++) + { + _samples.Add(pcm16[loopPos + (i % max)]); + } + } + + // Write 46 empty samples + _samples.AddRange(new short[46]); + + Size = (uint)_samples.Count * sizeof(short); + _sf2.UpdateSize(); + return start; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + foreach (short s in _samples) + { + writer.Write(s); + } + } + + public override string ToString() + { + return $"Sample Data Chunk"; + } + } + + public sealed class PHDRSubChunk : SF2Chunk + { + private readonly List _presets = new List(); + public uint Count => (uint)_presets.Count; + + internal PHDRSubChunk(SF2 inSf2) : base(inSf2, "phdr") { } + internal PHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2PresetHeader.Size; i++) + { + _presets.Add(new SF2PresetHeader(reader)); + } + } + + internal void AddPreset(SF2PresetHeader preset) + { + _presets.Add(preset); + Size = Count * SF2PresetHeader.Size; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _presets[i].Write(writer); + } + } + + public override string ToString() + { + return $"Preset Header Chunk - Preset count = {Count}"; + } + } + + public sealed class INSTSubChunk : SF2Chunk + { + private readonly List _instruments = new List(); + public uint Count => (uint)_instruments.Count; + + internal INSTSubChunk(SF2 inSf2) : base(inSf2, "inst") { } + internal INSTSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2Instrument.Size; i++) + { + _instruments.Add(new SF2Instrument(reader)); + } + } + + internal uint AddInstrument(SF2Instrument instrument) + { + _instruments.Add(instrument); + Size = Count * SF2Instrument.Size; + _sf2.UpdateSize(); + return Count - 1; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _instruments[i].Write(writer); + } + } + + public override string ToString() + { + return $"Instrument Chunk - Instrument count = {Count}"; + } + } + + public sealed class BAGSubChunk : SF2Chunk + { + private readonly List _bags = new List(); + public uint Count => (uint)_bags.Count; + + internal BAGSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pbag" : "ibag") { } + internal BAGSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2Bag.Size; i++) + { + _bags.Add(new SF2Bag(reader)); + } + } + + internal void AddBag(SF2Bag bag) + { + _bags.Add(bag); + Size = Count * SF2Bag.Size; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _bags[i].Write(writer); + } + } + + public override string ToString() + { + return $"Bag Chunk - Name = \"{ChunkName}\"" + + $",\nBag count = {Count}"; + } + } + + public sealed class MODSubChunk : SF2Chunk + { + private readonly List _modulators = new List(); + public uint Count => (uint)_modulators.Count; + + internal MODSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pmod" : "imod") { } + internal MODSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2ModulatorList.Size; i++) + { + _modulators.Add(new SF2ModulatorList(reader)); + } + } + + internal void AddModulator(SF2ModulatorList modulator) + { + _modulators.Add(modulator); + Size = Count * SF2ModulatorList.Size; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _modulators[i].Write(writer); + } + } + + public override string ToString() + { + return $"Modulator Chunk - Name = \"{ChunkName}\"" + + $",\nModulator count = {Count}"; + } + } + + public sealed class GENSubChunk : SF2Chunk + { + private readonly List _generators = new List(); + public uint Count => (uint)_generators.Count; + + internal GENSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pgen" : "igen") { } + internal GENSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2GeneratorList.Size; i++) + { + _generators.Add(new SF2GeneratorList(reader)); + } + } + + internal void AddGenerator(SF2GeneratorList generator) + { + _generators.Add(generator); + Size = Count * SF2GeneratorList.Size; + _sf2.UpdateSize(); + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _generators[i].Write(writer); + } + } + + public override string ToString() + { + return $"Generator Chunk - Name = \"{ChunkName}\"" + + $",\nGenerator count = {Count}"; + } + } + + public sealed class SHDRSubChunk : SF2Chunk + { + private readonly List _samples = new List(); + public uint Count => (uint)_samples.Count; + + internal SHDRSubChunk(SF2 inSf2) : base(inSf2, "shdr") { } + internal SHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + for (int i = 0; i < Size / SF2SampleHeader.Size; i++) + { + _samples.Add(new SF2SampleHeader(reader)); + } + } + + internal uint AddSample(SF2SampleHeader sample) + { + _samples.Add(sample); + Size = Count * SF2SampleHeader.Size; + _sf2.UpdateSize(); + return Count - 1; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + for (int i = 0; i < Count; i++) + { + _samples[i].Write(writer); + } + } + + public override string ToString() + { + return $"Sample Header Chunk - Sample header count = {Count}"; + } + } + + #endregion + + #region Main Chunks + + public sealed class InfoListChunk : SF2ListChunk + { + private readonly List _subChunks = new List(); + private const string DefaultEngine = "EMU8000"; + public string Engine + { + get + { + if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "isng") { Field = DefaultEngine }); + return DefaultEngine; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "isng") { Field = value }); + } + } + } + + private const string DefaultBank = "General MIDI"; + public string Bank + { + get + { + if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "INAM") { Field = DefaultBank }); + return DefaultBank; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "INAM") { Field = value }); + } + } + } + public string ROM + { + get + { + if (_subChunks.Find(s => s.ChunkName == "irom") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "irom") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "irom") { Field = value }); + } + } + } + public SF2VersionTag ROMVersion + { + get + { + if (_subChunks.Find(s => s.ChunkName == "iver") is VersionSubChunk chunk) + { + return chunk.Version; + } + else + { + return null; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "iver") is VersionSubChunk chunk) + { + chunk.Version = value; + } + else + { + _subChunks.Add(new VersionSubChunk(_sf2, "iver") { Version = value }); + } + } + } + public string Date + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ICRD") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "ICRD") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ICRD") { Field = value }); + } + } + } + public string Designer + { + get + { + if (_subChunks.Find(s => s.ChunkName == "IENG") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "IENG") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "IENG") { Field = value }); + } + } + } + public string Products + { + get + { + if (_subChunks.Find(s => s.ChunkName == "IPRD") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "IPRD") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "IPRD") { Field = value }); + } + } + } + public string Copyright + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ICOP") is HeaderSubChunk icop) + { + return icop.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "ICOP") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ICOP") { Field = value }); + } + } + } + + private const int CommentMaxSize = 0x10000; + public string Comment + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ICMT") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "ICMT") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ICMT", maxSize: CommentMaxSize) { Field = value }); + } + } + } + public string Tools + { + get + { + if (_subChunks.Find(s => s.ChunkName == "ISFT") is HeaderSubChunk chunk) + { + return chunk.Field; + } + else + { + return string.Empty; + } + } + set + { + if (_subChunks.Find(s => s.ChunkName == "ISFT") is HeaderSubChunk chunk) + { + chunk.Field = value; + } + else + { + _subChunks.Add(new HeaderSubChunk(_sf2, "ISFT") { Field = value }); + } + } + } + + internal InfoListChunk(SF2 inSf2) : base(inSf2, "INFO") + { + // Mandatory sub-chunks + _subChunks.Add(new VersionSubChunk(inSf2, "ifil") { Version = new SF2VersionTag(2, 1) }); + _subChunks.Add(new HeaderSubChunk(inSf2, "isng") { Field = DefaultEngine }); + _subChunks.Add(new HeaderSubChunk(inSf2, "INAM") { Field = DefaultBank }); + inSf2.UpdateSize(); + } + internal InfoListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + long startOffset = reader.BaseStream.Position; + while (reader.BaseStream.Position < startOffset + Size - 4) // The 4 represents the INFO that was already read + { + // Peek 4 chars for the chunk name + string name = reader.ReadString(4, false); + reader.BaseStream.Position -= 4; + switch (name) + { + case "ICMT": _subChunks.Add(new HeaderSubChunk(inSf2, reader, maxSize: CommentMaxSize)); break; + case "ifil": + case "iver": _subChunks.Add(new VersionSubChunk(inSf2, reader)); break; + case "isng": + case "INAM": + case "ICRD": + case "IENG": + case "IPRD": + case "ICOP": + case "ISFT": + case "irom": _subChunks.Add(new HeaderSubChunk(inSf2, reader)); break; + default: throw new NotSupportedException($"Unsupported chunk name at 0x{reader.BaseStream.Position:X}: \"{name}\""); + } + } + } + + internal override uint UpdateSize() + { + Size = 4; + foreach (SF2Chunk sub in _subChunks) + { + Size += sub.Size + 8; + } + + return Size; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + foreach (SF2Chunk sub in _subChunks) + { + sub.Write(writer); + } + } + + public override string ToString() + { + return $"Info List Chunk - Sub-chunk count = {_subChunks.Count}"; + } + } + + public sealed class SdtaListChunk : SF2ListChunk + { + public SMPLSubChunk SMPLSubChunk { get; } + + internal SdtaListChunk(SF2 inSf2) : base(inSf2, "sdta") + { + SMPLSubChunk = new SMPLSubChunk(inSf2); + inSf2.UpdateSize(); + } + internal SdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + SMPLSubChunk = new SMPLSubChunk(inSf2, reader); + } + + internal override uint UpdateSize() + { + return Size = 4 + + SMPLSubChunk.Size + 8; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + SMPLSubChunk.Write(writer); + } + + public override string ToString() + { + return $"Sample Data List Chunk"; + } + } + + public sealed class PdtaListChunk : SF2ListChunk + { + public PHDRSubChunk PHDRSubChunk { get; } + public BAGSubChunk PBAGSubChunk { get; } + public MODSubChunk PMODSubChunk { get; } + public GENSubChunk PGENSubChunk { get; } + public INSTSubChunk INSTSubChunk { get; } + public BAGSubChunk IBAGSubChunk { get; } + public MODSubChunk IMODSubChunk { get; } + public GENSubChunk IGENSubChunk { get; } + public SHDRSubChunk SHDRSubChunk { get; } + + internal PdtaListChunk(SF2 inSf2) : base(inSf2, "pdta") + { + PHDRSubChunk = new PHDRSubChunk(inSf2); + PBAGSubChunk = new BAGSubChunk(inSf2, true); + PMODSubChunk = new MODSubChunk(inSf2, true); + PGENSubChunk = new GENSubChunk(inSf2, true); + INSTSubChunk = new INSTSubChunk(inSf2); + IBAGSubChunk = new BAGSubChunk(inSf2, false); + IMODSubChunk = new MODSubChunk(inSf2, false); + IGENSubChunk = new GENSubChunk(inSf2, false); + SHDRSubChunk = new SHDRSubChunk(inSf2); + inSf2.UpdateSize(); + } + internal PdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) + { + PHDRSubChunk = new PHDRSubChunk(inSf2, reader); + PBAGSubChunk = new BAGSubChunk(inSf2, reader); + PMODSubChunk = new MODSubChunk(inSf2, reader); + PGENSubChunk = new GENSubChunk(inSf2, reader); + INSTSubChunk = new INSTSubChunk(inSf2, reader); + IBAGSubChunk = new BAGSubChunk(inSf2, reader); + IMODSubChunk = new MODSubChunk(inSf2, reader); + IGENSubChunk = new GENSubChunk(inSf2, reader); + SHDRSubChunk = new SHDRSubChunk(inSf2, reader); + } + + internal override uint UpdateSize() + { + return Size = 4 + + PHDRSubChunk.Size + 8 + + PBAGSubChunk.Size + 8 + + PMODSubChunk.Size + 8 + + PGENSubChunk.Size + 8 + + INSTSubChunk.Size + 8 + + IBAGSubChunk.Size + 8 + + IMODSubChunk.Size + 8 + + IGENSubChunk.Size + 8 + + SHDRSubChunk.Size + 8; + } + + internal override void Write(EndianBinaryWriter writer) + { + base.Write(writer); + PHDRSubChunk.Write(writer); + PBAGSubChunk.Write(writer); + PMODSubChunk.Write(writer); + PGENSubChunk.Write(writer); + INSTSubChunk.Write(writer); + IBAGSubChunk.Write(writer); + IMODSubChunk.Write(writer); + IGENSubChunk.Write(writer); + SHDRSubChunk.Write(writer); + } + + public override string ToString() + { + return $"Hydra List Chunk"; + } + } + + #endregion +} diff --git a/SoundFont2/SF2Types.cs b/SoundFont2/SF2Types.cs new file mode 100644 index 00000000..b297d802 --- /dev/null +++ b/SoundFont2/SF2Types.cs @@ -0,0 +1,142 @@ +using Kermalis.EndianBinaryIO; +using System.Runtime.InteropServices; + +namespace Kermalis.SoundFont2 +{ + /// SF2 v2.1 spec page 16 + public sealed class SF2VersionTag + { + public const uint Size = 4; + + public ushort Major { get; } + public ushort Minor { get; } + + public SF2VersionTag(ushort major, ushort minor) + { + Major = major; + Minor = minor; + } + internal SF2VersionTag(EndianBinaryReader reader) + { + Major = reader.ReadUInt16(); + Minor = reader.ReadUInt16(); + } + + internal void Write(EndianBinaryWriter writer) + { + writer.Write(Major); + writer.Write(Minor); + } + + public override string ToString() + { + return $"v{Major}.{Minor}"; + } + } + + /// SF2 spec v2.1 page 19 - Two bytes that can handle either two 8-bit values or a single 16-bit value + [StructLayout(LayoutKind.Explicit)] + public struct SF2GeneratorAmount + { + [FieldOffset(0)] public byte LowByte; + [FieldOffset(1)] public byte HighByte; + [FieldOffset(0)] public short Amount; + [FieldOffset(0)] public ushort UAmount; + + public override string ToString() + { + return $"BLo = {LowByte}, BHi = {HighByte}, Sh = {Amount}, U = {UAmount}"; + } + } + + /// SF2 v2.1 spec page 20 + public enum SF2SampleLink : ushort + { + MonoSample = 1, + RightSample = 2, + LeftSample = 4, + LinkedSample = 8, + RomMonoSample = 0x8001, + RomRightSample = 0x8002, + RomLeftSample = 0x8004, + RomLinkedSample = 0x8008 + } + + /// SF2 v2.1 spec page 38 + public enum SF2Generator : ushort + { + StartAddrsOffset = 0, + EndAddrsOffset = 1, + StartloopAddrsOffset = 2, + EndloopAddrsOffset = 3, + StartAddrsCoarseOffset = 4, + ModLfoToPitch = 5, + VibLfoToPitch = 6, + ModEnvToPitch = 7, + InitialFilterFc = 8, + InitialFilterQ = 9, + ModLfoToFilterFc = 10, + ModEnvToFilterFc = 11, + EndAddrsCoarseOffset = 12, + ModLfoToVolume = 13, + ChorusEffectsSend = 15, + ReverbEffectsSend = 16, + Pan = 17, + DelayModLFO = 21, + FreqModLFO = 22, + DelayVibLFO = 23, + FreqVibLFO = 24, + DelayModEnv = 25, + AttackModEnv = 26, + HoldModEnv = 27, + DecayModEnv = 28, + SustainModEnv = 29, + ReleaseModEnv = 30, + KeynumToModEnvHold = 31, + KeynumToModEnvDecay = 32, + DelayVolEnv = 33, + AttackVolEnv = 34, + HoldVolEnv = 35, + DecayVolEnv = 36, + SustainVolEnv = 37, + ReleaseVolEnv = 38, + KeynumToVolEnvHold = 39, + KeynumToVolEnvDecay = 40, + Instrument = 41, + KeyRange = 43, + VelRange = 44, + StartloopAddrsCoarseOffset = 45, + Keynum = 46, + Velocity = 47, + InitialAttenuation = 48, + EndloopAddrsCoarseOffset = 50, + CoarseTune = 51, + FineTune = 52, + SampleID = 53, + SampleModes = 54, + ScaleTuning = 56, + ExclusiveClass = 57, + OverridingRootKey = 58, + EndOper = 60 + } + + /// SF2 v2.1 spec page 50 + public enum SF2Modulator : ushort + { + None = 0, + NoteOnVelocity = 1, + NoteOnKey = 2, + PolyPressure = 10, + ChnPressure = 13, + PitchWheel = 14, + PitchWheelSensivity = 16 + } + + /// SF2 v2.1 spec page 52 + public enum SF2Transform : ushort + { + Linear = 0, + Concave = 1, + Convex = 2 + } +} diff --git a/SoundFont2/SoundFont2.csproj b/SoundFont2/SoundFont2.csproj new file mode 100644 index 00000000..2674a3ec --- /dev/null +++ b/SoundFont2/SoundFont2.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + Release + Kermalis + + SoundFont2 + SoundFont2 + SoundFont2 + Kermalis.SoundFont2 + 1.0.0.0 + + + + ..\Build + Auto + none + false + + + + + + + diff --git a/VG Music Studio.backup/AlphaDream.yaml b/VG Music Studio.backup/AlphaDream.yaml new file mode 100644 index 00000000..c6709d4d --- /dev/null +++ b/VG Music Studio.backup/AlphaDream.yaml @@ -0,0 +1,201 @@ +A88E_00: + Name: "Mario & Luigi - Superstar Saga (USA)" + AudioEngineVersion: "MLSS" + SongTableOffsets: 0x21CB70 + SongTableSizes: 407 + VoiceTableOffset: 0x21D1CC + SampleTableOffset: 0xA806B8 + SampleTableSize: 236 + Remap: "MLSS" + Playlists: + Music: + 1: "1" + 2: "2" + 3: "Mini Game" + 4: "Border Jump" + 5: "Star 'Stache Smash" + 6: "6" + 7: "7" + 8: "Stardust Fields" + 9: "Hoohoo Mountain" + 10: "Battle" + 11: "Victory" + 12: "Beanbean Fields" + 13: "Chucklehuck Woods" + 14: "Seabed" + 15: "Beanbean Castle" + 16: "Boss Battle" + 17: "Woohoo Hooniversity" + 18: "Teehee Valley" + 19: "Joke's End" + 20: "Little Fungitown" + 21: "Gwarhar Lagoon" + 22: "Underground" + 23: "Toad Town Square" + 24: "Bowser's Castle" + 25: "Warp Pipe" + 26: "Special Item" + 27: "Royal Welcome" + 28: "Prince Peasley's Theme" + 29: "Cackletta's Theme" + 30: "File Select" + 31: "Hoohoo Village" + 32: "Devastation" + 33: "Panic!" + 34: "Koopa Cruiser" + 35: "Danger!" + 36: "Popple Battle" + 37: "Cackletta Battle" + 38: "Bowletta Battle" + 39: "Ending" + 40: "Credits" + 41: "Title" + 42: "Chateau de Chucklehuck" + 43: "43" + 44: "Final Cackletta Battle" + 45: "45" + 46: "46" + 47: "47" + 48: "Professor E Gadd" + 49: "Ghostly Encounter" + 50: "Bean Time!" +A88J_00: + Name: "Mario & Luigi - Superstar Saga (Japan)" + SongTableOffsets: 0x205060 + SongTableSizes: 418 + VoiceTableOffset: 0x2056E8 + SampleTableOffset: 0x9721A8 + SampleTableSize: 239 + Copy: "A88E_00" +A88P_00: + Name: "Mario & Luigi - Superstar Saga (Europe)" + SongTableOffsets: 0x21DC50 + VoiceTableOffset: 0x21E2AC + SampleTableOffset: 0xA82088 + Copy: "A88E_00" +A84P_00: + Name: "Hamtaro - Rainbow Rescue (Europe)" + AudioEngineVersion: "Hamtaro" + SongTableOffsets: 0x694300 + SongTableSizes: 150 + VoiceTableOffset: 0x694558 + SampleTableOffset: 0xBCD510 + SampleTableSize: 149 + Playlists: + Music: + 1: "Title" + 2: "Menu" + 3: "Stickers" + 4: "Coloring" + 5: "The Clubhouse" + 6: "Ham-Ham Lawn" + 7: "Final Credits" + 8: "Bo" + 9: "Sunny Peak" + 10: "Flower Ranch" + 11: "Clover Elementary" + 12: "Ticky-Ticky Park" + 13: "Tip-Top Fair" + 14: "Taiko Drumming" + 15: "Sparkle Coast" + 16: "Aquarium" + 17: "Hamstarr Manor" + 18: "Puntaros" + 19: "Acorn Trail / Sea Spray Park" + 20: "Rainbow Theater" + 21: "Ham Game #1" + 22: "Ham Game #2" + 23: "Ham Game #3" + 24: "Ham Game #4" + 25: "Credits" + 26: "Ham-Ham Lawn (again)" + 27: "Ham-Ham March" + 28: "That's Not Good..." + 29: "Uh-Oh..." + 30: "Success!" + 31: "Oh No!" + 32: "Headband-Ham" + 33: "Bittersweet" + 34: "We did it!" + 35: "Pigeon Ride" + 36: "Boss" + 37: "Hamstarr" + 38: "Jingle" + 39: "Carrobo" +A84J_00: + Name: "Hamtaro - Rainbow Rescue (Japan)" + SongTableOffsets: 0x4DDE20 + VoiceTableOffset: 0x4DE078 + SampleTableOffset: 0x742FFC + Copy: "A84P_00" +B85A_00: + Name: "Hamtaro - Ham-Ham Games (Japan/USA)" + AudioEngineVersion: "Hamtaro" + SongTableOffsets: 0xDED20 + SongTableSizes: 212 + VoiceTableOffset: 0xDF10C + SampleTableOffset: 0x100000 + SampleTableSize: 368 + Playlists: + Music: + 1: "Title" + 2: "Menu" + 3: "The Clubhouse" + 4: "Cards / Crystal" + 5: "Introduction" + 6: "Credits" + 7: "Hamigo" + 8: "Event Preparation / Results" + 9: "Synchronized Swimming" + 10: "Other Events" + 11: "Marathon #1" + 12: "Marathon #2" + 13: "Marathon #3" + 14: "Marathon #4" + 15: "Marathon #5" + 16: "Results Ceremony" + 17: "Ham Shopping Network" + 18: "Ham Studio News" + 19: "Ham Fortune Telling" + 20: "Ham Studio Shows Introduction" + 21: "BGM #1" + 22: "BGM #2" + 23: "BGM #3" + 24: "BGM #4" + 25: "BGM #5" + 26: "BGM #6" + 27: "BGM #7" + 28: "BGM #8" + 29: "BGM #9" + 30: "BGM #10" + 31: "BGM #11" + 32: "Stadium Events" + 33: "BGM #12" + 34: "BGM #13" + 35: "BGM #14" + 36: "BGM #15" + 37: "BGM #16" + 38: "BGM #17" + 39: "BGM #18" + 40: "BGM #19" + 41: "BGM #20" + 42: "42" + 43: "BGM #21" + 44: "BGM #22" + 45: "BGM #23" + 46: "BGM #24" + 47: "BGM #25" + 48: "BGM #26" + 49: "BGM #27" + 50: "BGM #28" + 51: "BGM #29" + 52: "BGM #30" + 53: "Minigame A" + 54: "Minigame B" + 55: "Carrobo" +B85P_00: + Name: "Hamtaro - Ham-Ham Games (Europe)" + SongTableOffsets: 0xDED20 + VoiceTableOffset: 0xDF10C + SampleTableOffset: 0x100000 + Copy: "B85A_00" diff --git a/VG Music Studio.backup/Config.yaml b/VG Music Studio.backup/Config.yaml new file mode 100644 index 00000000..d4bdf7b8 --- /dev/null +++ b/VG Music Studio.backup/Config.yaml @@ -0,0 +1,137 @@ +TaskbarProgress: True # True or False # Whether the taskbar will show the song progress +RefreshRate: 30 # RefreshRate >= 1 and RefreshRate <= 1000 # How many times a second the visual updates +CenterIndicators: False # True or False # Whether lines should be drawn for the center of panpot in the visual +PanpotIndicators: False # True or False # Whether lines should be drawn for the track's panpot in the visual +PlaylistMode: "Random" # "Random" or "Sequential" # The way the playlist will behave +PlaylistSongLoops: 0 # Loops >= 0 and Loops <= 9223372036854775807 # How many times a song should loop before fading out +PlaylistFadeOutMilliseconds: 10000 # Milliseconds >= 0 and Milliseconds <= 9223372036854775807 # How many milliseconds it should take to fade out of a song +MiddleCOctave: 4 # Octave >= --128 and Octave <= 127 # The octave that holds middle C. Used in the visual and track viewer +Colors: + 0: {H: 185, S: 240, L: 180} + 1: {H: 183, S: 240, L: 170} + 2: {H: 180, S: 240, L: 157} + 3: {H: 184, S: 240, L: 85} + 4: {H: 171, S: 240, L: 134} + 5: {H: 168, S: 240, L: 159} + 6: {H: 36, S: 240, L: 170} + 7: {H: 15, S: 240, L: 134} + 8: {H: 175, S: 240, L: 200} + 9: {H: 120, S: 240, L: 150} + 10: {H: 114, S: 240, L: 138} + 11: {H: 99, S: 240, L: 171} + 12: {H: 68, S: 240, L: 171} + 13: {H: 83, S: 240, L: 200} + 14: {H: 215, S: 240, L: 104} + 15: {H: 25, S: 240, L: 200} + 16: {H: 224, S: 240, L: 150} + 17: {H: 195, S: 240, L: 120} + 18: {H: 206, S: 240, L: 95} + 19: {H: 218, S: 240, L: 129} + 20: {H: 203, S: 240, L: 180} + 21: {H: 145, S: 240, L: 100} + 22: {H: 140, S: 240, L: 111} + 23: {H: 151, S: 240, L: 120} + 24: {H: 5, S: 240, L: 169} + 25: {H: 6, S: 240, L: 156} + 26: {H: 14, S: 240, L: 164} + 27: {H: 12, S: 240, L: 137} + 28: {H: 8, S: 240, L: 140} + 29: {H: 0, S: 240, L: 123} + 30: {H: 229, S: 240, L: 70} + 31: {H: 239, S: 240, L: 89} + 32: {H: 25, S: 180, L: 160} + 33: {H: 20, S: 180, L: 145} + 34: {H: 17, S: 180, L: 140} + 35: {H: 36, S: 240, L: 163} + 36: {H: 25, S: 180, L: 140} + 37: {H: 25, S: 210, L: 95} + 38: {H: 160, S: 0, L: 180} + 39: {H: 200, S: 240, L: 90} + 40: {H: 195, S: 240, L: 100} + 41: {H: 190, S: 240, L: 93} + 42: {H: 180, S: 240, L: 90} + 43: {H: 170, S: 240, L: 150} + 44: {H: 166, S: 240, L: 89} + 45: {H: 210, S: 240, L: 170} + 46: {H: 214, S: 240, L: 185} + 47: {H: 15, S: 135, L: 135} + 48: {H: 148, S: 240, L: 130} + 49: {H: 173, S: 240, L: 80} + 50: {H: 170, S: 240, L: 95} + 51: {H: 176, S: 240, L: 100} + 52: {H: 26, S: 240, L: 215} + 53: {H: 20, S: 240, L: 210} + 54: {H: 5, S: 240, L: 220} + 55: {H: 6, S: 240, L: 150} + 56: {H: 22, S: 240, L: 134} + 57: {H: 25, S: 240, L: 130} + 58: {H: 40, S: 240, L: 120} + 59: {H: 28, S: 240, L: 122} + 60: {H: 16, S: 240, L: 124} + 61: {H: 11, S: 240, L: 118} + 62: {H: 53, S: 240, L: 158} + 63: {H: 57, S: 240, L: 133} + 64: {H: 30, S: 240, L: 195} + 65: {H: 23, S: 240, L: 182} + 66: {H: 32, S: 240, L: 160} + 67: {H: 32, S: 240, L: 130} + 68: {H: 37, S: 240, L: 135} + 69: {H: 13, S: 240, L: 143} + 70: {H: 134, S: 240, L: 85} + 71: {H: 130, S: 240, L: 95} + 72: {H: 120, S: 240, L: 165} + 73: {H: 126, S: 240, L: 120} + 74: {H: 126, S: 240, L: 100} + 75: {H: 135, S: 240, L: 160} + 76: {H: 118, S: 240, L: 186} + 77: {H: 135, S: 240, L: 102} + 78: {H: 113, S: 240, L: 100} + 79: {H: 70, S: 240, L: 160} + 80: {H: 82, S: 240, L: 132} + 81: {H: 227, S: 240, L: 188} + 82: {H: 103, S: 240, L: 140} + 83: {H: 60, S: 240, L: 165} + 84: {H: 239, S: 240, L: 165} + 85: {H: 123, S: 240, L: 175} + 86: {H: 210, S: 240, L: 145} + 87: {H: 53, S: 240, L: 120} + 88: {H: 110, S: 240, L: 155} + 89: {H: 122, S: 240, L: 205} + 90: {H: 217, S: 240, L: 95} + 91: {H: 142, S: 240, L: 50} + 92: {H: 100, S: 240, L: 90} + 93: {H: 137, S: 240, L: 90} + 94: {H: 188, S: 240, L: 117} + 95: {H: 160, S: 240, L: 210} + 96: {H: 130, S: 240, L: 200} + 97: {H: 202, S: 240, L: 80} + 98: {H: 0, S: 240, L: 160} + 99: {H: 30, S: 240, L: 110} + 100: {H: 130, S: 240, L: 210} + 101: {H: 75, S: 240, L: 75} + 102: {H: 180, S: 240, L: 205} + 103: {H: 27, S: 200, L: 105} + 104: {H: 33, S: 200, L: 145} + 105: {H: 37, S: 220, L: 130} + 106: {H: 45, S: 240, L: 135} + 107: {H: 55, S: 240, L: 175} + 108: {H: 95, S: 240, L: 185} + 109: {H: 53, S: 240, L: 190} + 110: {H: 135, S: 240, L: 120} + 111: {H: 38, S: 240, L: 110} + 112: {H: 220, S: 240, L: 170} + 113: {H: 120, S: 80, L: 150} + 114: {H: 130, S: 120, L: 190} + 115: {H: 0, S: 80, L: 90} + 116: {H: 18, S: 125, L: 130} + 117: {H: 15, S: 70, L: 120} + 118: {H: 200, S: 80, L: 110} + 119: {H: 140, S: 60, L: 180} + 120: {H: 10, S: 240, L: 90} + 121: {H: 123, S: 156, L: 100} + 122: {H: 128, S: 240, L: 100} + 123: {H: 40, S: 240, L: 180} + 124: {H: 239, S: 200, L: 90} + 125: {H: 145, S: 10, L: 155} + 126: {H: 15, S: 80, L: 160} + 127: {H: 160, S: 80, L: 150} \ No newline at end of file diff --git a/VG Music Studio.backup/Core/ADPCMDecoder.cs b/VG Music Studio.backup/Core/ADPCMDecoder.cs new file mode 100644 index 00000000..9c8a4e94 --- /dev/null +++ b/VG Music Studio.backup/Core/ADPCMDecoder.cs @@ -0,0 +1,90 @@ +namespace Kermalis.VGMusicStudio.Core +{ + internal class ADPCMDecoder + { + private static readonly short[] _indexTable = new short[8] + { + -1, -1, -1, -1, 2, 4, 6, 8 + }; + private static readonly short[] _stepTable = new short[89] + { + 00007, 00008, 00009, 00010, 00011, 00012, 00013, 00014, + 00016, 00017, 00019, 00021, 00023, 00025, 00028, 00031, + 00034, 00037, 00041, 00045, 00050, 00055, 00060, 00066, + 00073, 00080, 00088, 00097, 00107, 00118, 00130, 00143, + 00157, 00173, 00190, 00209, 00230, 00253, 00279, 00307, + 00337, 00371, 00408, 00449, 00494, 00544, 00598, 00658, + 00724, 00796, 00876, 00963, 01060, 01166, 01282, 01411, + 01552, 01707, 01878, 02066, 02272, 02499, 02749, 03024, + 03327, 03660, 04026, 04428, 04871, 05358, 05894, 06484, + 07132, 07845, 08630, 09493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; + + private readonly byte[] _data; + public short LastSample; + public short StepIndex; + public int DataOffset; + public bool OnSecondNibble; + + public ADPCMDecoder(byte[] data) + { + LastSample = (short)(data[0] | (data[1] << 8)); + StepIndex = (short)((data[2] | (data[3] << 8)) & 0x7F); + DataOffset = 4; + _data = data; + } + + public static short[] ADPCMToPCM16(byte[] data) + { + var decoder = new ADPCMDecoder(data); + short[] buffer = new short[(data.Length - 4) * 2]; + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = decoder.GetSample(); + } + return buffer; + } + + public short GetSample() + { + int val = (_data[DataOffset] >> (OnSecondNibble ? 4 : 0)) & 0xF; + short step = _stepTable[StepIndex]; + int diff = + (step / 8) + + (step / 4 * (val & 1)) + + (step / 2 * ((val >> 1) & 1)) + + (step * ((val >> 2) & 1)); + + int a = (diff * ((((val >> 3) & 1) == 1) ? -1 : 1)) + LastSample; + if (a < short.MinValue) + { + a = short.MinValue; + } + else if (a > short.MaxValue) + { + a = short.MaxValue; + } + LastSample = (short)a; + + a = StepIndex + _indexTable[val & 7]; + if (a < 0) + { + a = 0; + } + else if (a > 88) + { + a = 88; + } + StepIndex = (short)a; + + if (OnSecondNibble) + { + DataOffset++; + } + OnSecondNibble = !OnSecondNibble; + return LastSample; + } + } +} diff --git a/VG Music Studio.backup/Core/Assembler.cs b/VG Music Studio.backup/Core/Assembler.cs new file mode 100644 index 00000000..8da670d3 --- /dev/null +++ b/VG Music Studio.backup/Core/Assembler.cs @@ -0,0 +1,351 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core +{ + internal sealed class Assembler + { + private class Pair + { + public bool Global; + public int Offset; + } + private class Pointer + { + public string Label; + public int BinaryOffset; + } + private const string _fileErrorFormat = "{0}{3}{3}Error reading file included in line {1}:{3}{2}"; + private const string _mathErrorFormat = "{0}{3}{3}Error parsing value in line {1} (Are you missing a definition?):{3}{2}"; + private const string _cmdErrorFormat = "{0}{3}{3}Unknown command in line {1}:{3}\"{2}\""; + + public int BaseOffset { get; private set; } + private readonly List _loaded = new List(); + private readonly Dictionary _defines; + + private readonly Dictionary _labels = new Dictionary(); + private readonly List _lPointers = new List(); + private readonly List _bytes = new List(); + + public string FileName { get; } + public Endianness Endianness { get; } + public int this[string Label] => _labels[FixLabel(Label)].Offset; + public byte[] Binary => _bytes.ToArray(); + public int BinaryLength => _bytes.Count; + + public Assembler(string fileName, int baseOffset, Endianness endianness, Dictionary initialDefines = null) + { + FileName = fileName; + Endianness = endianness; + _defines = initialDefines ?? new Dictionary(); + Debug.WriteLine(Read(fileName)); + SetBaseOffset(baseOffset); + } + + public void SetBaseOffset(int baseOffset) + { + foreach (Pointer p in _lPointers) + { + // Our example label is SEQ_STUFF at the binary offset 0x1000, curBaseOffset is 0x500, baseOffset is 0x1800 + // There is a pointer (p) to SEQ_STUFF at the binary offset 0x1DFC + int oldPointer = EndianBitConverter.BytesToInt32(Binary, p.BinaryOffset, Endianness); // If there was a pointer to "SEQ_STUFF+4", the pointer would be 0x1504, at binary offset 0x1DFC + int labelOffset = oldPointer - BaseOffset; // Then labelOffset is 0x1004 (SEQ_STUFF+4) + byte[] newPointerBytes = EndianBitConverter.Int32ToBytes(baseOffset + labelOffset, Endianness); // b will contain {0x04, 0x28, 0x00, 0x00} [0x2804] (SEQ_STUFF+4 + baseOffset) + for (int i = 0; i < 4; i++) + { + _bytes[p.BinaryOffset + i] = newPointerBytes[i]; // Copy the new pointer to binary offset 0x1DF4 + } + } + BaseOffset = baseOffset; + } + + public static string FixLabel(string label) + { + string ret = ""; + for (int i = 0; i < label.Length; i++) + { + char c = label[i]; + if ((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9' && i > 0)) + { + ret += c; + } + else + { + ret += '_'; + } + } + return ret; + } + + // Returns a status + private string Read(string fileName) + { + if (_loaded.Contains(fileName)) + { + return $"{fileName} was already loaded"; + } + + string[] file = File.ReadAllLines(fileName); + _loaded.Add(fileName); + + for (int i = 0; i < file.Length; i++) + { + string line = file[i]; + if (string.IsNullOrWhiteSpace(line)) + { + continue; // Skip empty lines + } + + bool readingCMD = false; // If it's reading the command + string cmd = null; + var args = new List(); + string str = string.Empty; + foreach (char c in line) + { + if (c == '@') // Ignore comments from this point + { + break; + } + else if (c == '.' && cmd == null) + { + readingCMD = true; + } + else if (c == ':') // Labels + { + if (!_labels.ContainsKey(str)) + { + _labels.Add(str, new Pair()); + } + _labels[str].Offset = _bytes.Count; + str = string.Empty; + } + else if (char.IsWhiteSpace(c)) + { + if (readingCMD) // If reading the command, otherwise do nothing + { + cmd = str; + readingCMD = false; + str = string.Empty; + } + } + else if (c == ',') + { + args.Add(str); + str = string.Empty; + } + else + { + str += c; + } + } + if (cmd == null) + { + continue; // Commented line + } + + args.Add(str); // Add last string before the newline + + switch (cmd.ToLower()) + { + case "include": + { + try + { + Read(args[0].Replace("\"", string.Empty)); + } + catch + { + throw new IOException(string.Format(_fileErrorFormat, fileName, i, args[0], Environment.NewLine)); + } + break; + } + case "equ": + { + try + { + _defines.Add(args[0], ParseInt(args[1])); + } + catch + { + throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); + } + break; + } + case "global": + { + if (!_labels.ContainsKey(args[0])) + { + _labels.Add(args[0], new Pair()); + } + _labels[args[0]].Global = true; + break; + } + case "align": + { + int align = ParseInt(args[0]); + for (int a = BinaryLength % align; a < align; a++) + { + _bytes.Add(0); + } + break; + } + case "byte": + { + try + { + foreach (string a in args) + { + _bytes.Add((byte)ParseInt(a)); + } + } + catch + { + throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); + } + break; + } + case "hword": + { + try + { + foreach (string a in args) + { + _bytes.AddRange(EndianBitConverter.Int16ToBytes((short)ParseInt(a), Endianness)); + } + } + catch + { + throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); + } + break; + } + case "int": + case "word": + { + try + { + foreach (string a in args) + { + _bytes.AddRange(EndianBitConverter.Int32ToBytes(ParseInt(a), Endianness)); + } + } + catch + { + throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); + } + break; + } + case "end": + { + goto end; + } + case "section": // Ignore + { + break; + } + default: throw new NotSupportedException(string.Format(_cmdErrorFormat, fileName, i, cmd, Environment.NewLine)); + } + } + end: + return $"{fileName} loaded with no issues"; + } + + private static readonly CultureInfo _enUS = new CultureInfo("en-US"); + private int ParseInt(string value) + { + // First try regular values like "40" and "0x20" + if (value.StartsWith("0x") && int.TryParse(value.Substring(2), NumberStyles.HexNumber, _enUS, out int hex)) + { + return hex; + } + if (int.TryParse(value, NumberStyles.Integer, _enUS, out int dec)) + { + return dec; + } + // Then check if it's defined + if (_defines.TryGetValue(value, out int def)) + { + return def; + } + if (_labels.TryGetValue(value, out Pair pair)) + { + _lPointers.Add(new Pointer { Label = value, BinaryOffset = _bytes.Count }); + return pair.Offset; + } + + // Then check if it's math + bool foundMath = false; + string str = string.Empty; + int ret = 0; + bool add = true, // Add first, so the initial value is set + sub = false, + mul = false, + div = false; + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + + if (char.IsWhiteSpace(c)) // White space does nothing here + { + continue; + } + if (c == '+' || c == '-' || c == '*' || c == '/') + { + if (add) + { + ret += ParseInt(str); + } + else if (sub) + { + ret -= ParseInt(str); + } + else if (mul) + { + ret *= ParseInt(str); + } + else if (div) + { + ret /= ParseInt(str); + } + add = c == '+'; + sub = c == '-'; + mul = c == '*'; + div = c == '/'; + str = string.Empty; + foundMath = true; + } + else + { + str += c; + } + } + if (foundMath) + { + if (add) // Handle last + { + ret += ParseInt(str); + } + else if (sub) + { + ret -= ParseInt(str); + } + else if (mul) + { + ret *= ParseInt(str); + } + else if (div) + { + ret /= ParseInt(str); + } + return ret; + } + throw new ArgumentOutOfRangeException(nameof(value)); + } + } +} diff --git a/VG Music Studio.backup/Core/Config.cs b/VG Music Studio.backup/Core/Config.cs new file mode 100644 index 00000000..81bfbf62 --- /dev/null +++ b/VG Music Studio.backup/Core/Config.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core +{ + internal abstract class Config : IDisposable + { + public class Song + { + public long Index; + public string Name; + + public Song(long index, string name) + { + Index = index; Name = name; + } + + public override bool Equals(object obj) + { + return obj is Song other && other.Index == Index; + } + public override int GetHashCode() + { + return Index.GetHashCode(); + } + public override string ToString() + { + return Name; + } + } + public class Playlist + { + public string Name; + public List Songs; + + public Playlist(string name, IEnumerable songs) + { + Name = name; Songs = songs.ToList(); + } + + public override string ToString() + { + int songCount = Songs.Count; + CultureInfo cul = System.Threading.Thread.CurrentThread.CurrentUICulture; + if (cul.TwoLetterISOLanguageName == "it") // Italian + { + // PlaylistName - (1 Canzone) + // PlaylistName - (2 Canzoni) + return $"{Name} - ({songCount} {(songCount == 1 ? "Canzone" : "Canzoni")})"; + } + else if (cul.TwoLetterISOLanguageName == "es") // Spanish + { + // PlaylistName - (1 Canción) + // PlaylistName - (2 Canciones) + return $"{Name} - ({songCount} {(songCount == 1 ? "Canción" : "Canciones")})"; + } + else // Fallback to en-US + { + // PlaylistName - (1 Song) + // PlaylistName - (2 Songs) + return $"{Name} - ({songCount} {(songCount == 1 ? "Song" : "Songs")})"; + } + } + } + + public List Playlists = new List(); + + public Song GetFirstSong(long index) + { + foreach (Playlist p in Playlists) + { + foreach (Song s in p.Songs) + { + if (s.Index == index) + { + return s; + } + } + } + return null; + } + + public abstract string GetGameName(); + public abstract string GetSongName(long index); + + public virtual void Dispose() { } + } +} diff --git a/VG Music Studio.backup/Core/Engine.cs b/VG Music Studio.backup/Core/Engine.cs new file mode 100644 index 00000000..57200db2 --- /dev/null +++ b/VG Music Studio.backup/Core/Engine.cs @@ -0,0 +1,92 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core +{ + internal class Engine : IDisposable + { + public enum EngineType : byte + { + None, + GBA_AlphaDream, + GBA_MP2K, + NDS_DSE, + NDS_SDAT + } + + public static Engine Instance { get; private set; } + + public EngineType Type { get; } + public Config Config { get; private set; } + public Mixer Mixer { get; private set; } + public IPlayer Player { get; private set; } + + public Engine(EngineType type, object playerArg) + { + switch (type) + { + case EngineType.GBA_AlphaDream: + { + byte[] rom = (byte[])playerArg; + if (rom.Length > GBA.Utils.CartridgeCapacity) + { + throw new Exception($"The ROM is too large. Maximum size is 0x{GBA.Utils.CartridgeCapacity:X7} bytes."); + } + var config = new GBA.AlphaDream.Config(rom); + Config = config; + var mixer = new GBA.AlphaDream.Mixer(config); + Mixer = mixer; + Player = new GBA.AlphaDream.Player(mixer, config); + break; + } + case EngineType.GBA_MP2K: + { + byte[] rom = (byte[])playerArg; + if (rom.Length > GBA.Utils.CartridgeCapacity) + { + throw new Exception($"The ROM is too large. Maximum size is 0x{GBA.Utils.CartridgeCapacity:X7} bytes."); + } + var config = new GBA.MP2K.Config(rom); + Config = config; + var mixer = new GBA.MP2K.Mixer(config); + Mixer = mixer; + Player = new GBA.MP2K.Player(mixer, config); + break; + } + case EngineType.NDS_DSE: + { + string bgmPath = (string)playerArg; + var config = new NDS.DSE.Config(bgmPath); + Config = config; + var mixer = new NDS.DSE.Mixer(); + Mixer = mixer; + Player = new NDS.DSE.Player(mixer, config); + break; + } + case EngineType.NDS_SDAT: + { + var sdat = (NDS.SDAT.SDAT)playerArg; + var config = new NDS.SDAT.Config(sdat); + Config = config; + var mixer = new NDS.SDAT.Mixer(); + Mixer = mixer; + Player = new NDS.SDAT.Player(mixer, config); + break; + } + default: throw new ArgumentOutOfRangeException(nameof(type)); + } + Type = type; + Instance = this; + } + + public void Dispose() + { + Config.Dispose(); + Config = null; + Mixer.Dispose(); + Mixer = null; + Player.Dispose(); + Player = null; + Instance = null; + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Channel.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Channel.cs new file mode 100644 index 00000000..4eba3ac1 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/Channel.cs @@ -0,0 +1,237 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal abstract class Channel + { + protected readonly Mixer _mixer; + public EnvelopeState State; + public byte Key; + public bool Stopped; + + protected ADSR _adsr; + + protected byte _velocity; + protected int _pos; + protected float _interPos; + protected float _frequency; + protected byte _leftVol; + protected byte _rightVol; + + protected Channel(Mixer mixer) + { + _mixer = mixer; + } + + public ChannelVolume GetVolume() + { + const float max = 0x10000; + return new ChannelVolume + { + LeftVol = _leftVol * _velocity / max, + RightVol = _rightVol * _velocity / max + }; + } + public void SetVolume(byte vol, sbyte pan) + { + _leftVol = (byte)((vol * (-pan + 0x80)) >> 8); + _rightVol = (byte)((vol * (pan + 0x80)) >> 8); + } + public abstract void SetPitch(int pitch); + + public abstract void Process(float[] buffer); + } + internal class PCMChannel : Channel + { + private SampleHeader _sampleHeader; + private int _sampleOffset; + private bool _bFixed; + + public PCMChannel(Mixer mixer) : base(mixer) { } + public void Init(byte key, ADSR adsr, int sampleOffset, bool bFixed) + { + _velocity = adsr.A; + State = EnvelopeState.Attack; + _pos = 0; _interPos = 0; + Key = key; + _adsr = adsr; + _sampleHeader = _mixer.Config.Reader.ReadObject(sampleOffset); + _sampleOffset = sampleOffset + 0x10; + _bFixed = bFixed; + Stopped = false; + } + + public override void SetPitch(int pitch) + { + if (_sampleHeader != null) + { + _frequency = (_sampleHeader.SampleRate >> 10) * (float)Math.Pow(2, ((Key - 60) / 12f) + (pitch / 768f)); + } + } + + private void StepEnvelope() + { + switch (State) + { + case EnvelopeState.Attack: + { + int nextVel = _velocity + _adsr.A; + if (nextVel >= 0xFF) + { + State = EnvelopeState.Decay; + _velocity = 0xFF; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Decay: + { + int nextVel = (_velocity * _adsr.D) >> 8; + if (nextVel <= _adsr.S) + { + State = EnvelopeState.Sustain; + _velocity = _adsr.S; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Release: + { + int next = (_velocity * _adsr.R) >> 8; + if (next < 0) + { + next = 0; + } + _velocity = (byte)next; + break; + } + } + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + + ChannelVolume vol = GetVolume(); + float interStep = (_bFixed ? _sampleHeader.SampleRate >> 10 : _frequency) * _mixer.SampleRateReciprocal; + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = (_mixer.Config.ROM[_pos + _sampleOffset] - 0x80) / (float)0x80; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _sampleHeader.Length) + { + if (_sampleHeader.DoesLoop == 0x40000000) + { + _pos = _sampleHeader.LoopOffset; + } + else + { + Stopped = true; + break; + } + } + } while (--samplesPerBuffer > 0); + } + } + internal class SquareChannel : Channel + { + private float[] _pat; + + public SquareChannel(Mixer mixer) : base(mixer) { } + public void Init(byte key, ADSR env, byte vol, sbyte pan, int pitch) + { + _pat = MP2K.Utils.SquareD50; // TODO + Key = key; + _adsr = env; + SetVolume(vol, pan); + SetPitch(pitch); + State = EnvelopeState.Attack; + } + + public override void SetPitch(int pitch) + { + _frequency = 3520 * (float)Math.Pow(2, ((Key - 69) / 12f) + (pitch / 768f)); + } + + private void StepEnvelope() + { + switch (State) + { + case EnvelopeState.Attack: + { + int next = _velocity + _adsr.A; + if (next >= 0xF) + { + State = EnvelopeState.Decay; + _velocity = 0xF; + } + else + { + _velocity = (byte)next; + } + break; + } + case EnvelopeState.Decay: + { + int next = (_velocity * _adsr.D) >> 3; + if (next <= _adsr.S) + { + State = EnvelopeState.Sustain; + _velocity = _adsr.S; + } + else + { + _velocity = (byte)next; + } + break; + } + case EnvelopeState.Release: + { + int next = (_velocity * _adsr.R) >> 3; + if (next < 0) + { + next = 0; + } + _velocity = (byte)next; + break; + } + } + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + + ChannelVolume vol = GetVolume(); + float interStep = _frequency * _mixer.SampleRateReciprocal; + + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _pat[_pos]; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos = (_pos + posDelta) & 0x7; + } while (--samplesPerBuffer > 0); + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Commands.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Commands.cs new file mode 100644 index 00000000..50991f73 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/Commands.cs @@ -0,0 +1,113 @@ +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal class FinishCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Finish"; + public string Arguments => string.Empty; + } + internal class FreeNoteHamtaroCommand : ICommand // TODO: When optimization comes, get rid of free note vs note and just have the label differ + { + public Color Color => Color.SkyBlue; + public string Label => "Free Note"; + public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Volume} {Duration}"; + + public byte Key { get; set; } + public byte Volume { get; set; } + public byte Duration { get; set; } + } + internal class FreeNoteMLSSCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Free Note"; + public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Duration}"; + + public byte Key { get; set; } + public byte Duration { get; set; } + } + internal class JumpCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Jump"; + public string Arguments => $"0x{Offset:X7}"; + + public int Offset { get; set; } + } + internal class NoteHamtaroCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Volume} {Duration}"; + + public byte Key { get; set; } + public byte Volume { get; set; } + public byte Duration { get; set; } + } + internal class NoteMLSSCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Duration}"; + + public byte Key { get; set; } + public byte Duration { get; set; } + } + internal class PanpotCommand : ICommand + { + public Color Color => Color.GreenYellow; + public string Label => "Panpot"; + public string Arguments => Panpot.ToString(); + + public sbyte Panpot { get; set; } + } + internal class PitchBendCommand : ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend"; + public string Arguments => Bend.ToString(); + + public sbyte Bend { get; set; } + } + internal class PitchBendRangeCommand : ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend Range"; + public string Arguments => Range.ToString(); + + public byte Range { get; set; } + } + internal class RestCommand : ICommand + { + public Color Color => Color.PaleVioletRed; + public string Label => "Rest"; + public string Arguments => Rest.ToString(); + + public byte Rest { get; set; } + } + internal class TrackTempoCommand : ICommand + { + public Color Color => Color.DeepSkyBlue; + public string Label => "Track Tempo"; + public string Arguments => Tempo.ToString(); + + public byte Tempo { get; set; } + } + internal class VoiceCommand : ICommand + { + public Color Color => Color.DarkSalmon; + public string Label => "Voice"; + public string Arguments => Voice.ToString(); + + public byte Voice { get; set; } + } + internal class VolumeCommand : ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Volume"; + public string Arguments => Volume.ToString(); + + public byte Volume { get; set; } + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Config.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Config.cs new file mode 100644 index 00000000..c1689303 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/Config.cs @@ -0,0 +1,220 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using YamlDotNet.RepresentationModel; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal class Config : Core.Config + { + public readonly byte[] ROM; + public readonly EndianBinaryReader Reader; + public readonly string GameCode; + public readonly byte Version; + + public readonly string Name; + public readonly AudioEngineVersion AudioEngineVersion; + public readonly int[] SongTableOffsets; + public readonly long[] SongTableSizes; + public readonly int VoiceTableOffset; + public readonly int SampleTableOffset; + public readonly long SampleTableSize; + + public Config(byte[] rom) + { + const string configFile = "AlphaDream.yaml"; + using (StreamReader fileStream = File.OpenText(Util.Utils.CombineWithBaseDirectory(configFile))) + { + string gcv = string.Empty; + try + { + ROM = rom; + Reader = new EndianBinaryReader(new MemoryStream(rom)); + GameCode = Reader.ReadString(4, false, 0xAC); + Version = Reader.ReadByte(0xBC); + gcv = $"{GameCode}_{Version:X2}"; + var yaml = new YamlStream(); + yaml.Load(fileStream); + + var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; + YamlMappingNode game; + try + { + game = (YamlMappingNode)mapping.Children.GetValue(gcv); + } + catch (BetterKeyNotFoundException) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); + } + + YamlNode nameNode = null, + audioEngineVersionNode = null, + songTableOffsetsNode = null, + voiceTableOffsetNode = null, + sampleTableOffsetNode = null, + songTableSizesNode = null, + sampleTableSizeNode = null; + void Load(YamlMappingNode gameToLoad) + { + if (gameToLoad.Children.TryGetValue("Copy", out YamlNode node)) + { + YamlMappingNode copyGame; + try + { + copyGame = (YamlMappingNode)mapping.Children.GetValue(node); + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); + } + Load(copyGame); + } + if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) + { + nameNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(AudioEngineVersion), out node)) + { + audioEngineVersionNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) + { + songTableOffsetsNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) + { + songTableSizesNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(VoiceTableOffset), out node)) + { + voiceTableOffsetNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SampleTableOffset), out node)) + { + sampleTableOffsetNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SampleTableSize), out node)) + { + sampleTableSizeNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) + { + var playlists = (YamlMappingNode)node; + foreach (KeyValuePair kvp in playlists) + { + string name = kvp.Key.ToString(); + var songs = new List(); + foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) + { + long songIndex = Util.Utils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, long.MaxValue); + if (songs.Any(s => s.Index == songIndex)) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); + } + songs.Add(new Song(songIndex, song.Value.ToString())); + } + Playlists.Add(new Playlist(name, songs)); + } + } + } + + Load(game); + + if (nameNode == null) + { + throw new BetterKeyNotFoundException(nameof(Name), null); + } + Name = nameNode.ToString(); + if (audioEngineVersionNode == null) + { + throw new BetterKeyNotFoundException(nameof(AudioEngineVersion), null); + } + AudioEngineVersion = Util.Utils.ParseEnum(nameof(AudioEngineVersion), audioEngineVersionNode.ToString()); + if (songTableOffsetsNode == null) + { + throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); + } + string[] songTables = songTableOffsetsNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); + int numSongTables = songTables.Length; + if (numSongTables == 0) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); + } + if (songTableSizesNode == null) + { + throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); + } + string[] songTableSizes = songTableSizesNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); + if (songTableSizes.Length != numSongTables) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); + } + SongTableOffsets = new int[numSongTables]; + SongTableSizes = new long[numSongTables]; + int maxOffset = rom.Length - 1; + for (int i = 0; i < numSongTables; i++) + { + SongTableOffsets[i] = (int)Util.Utils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); + SongTableSizes[i] = Util.Utils.ParseValue(nameof(SongTableSizes), songTableSizes[i], 1, maxOffset); + } + if (voiceTableOffsetNode == null) + { + throw new BetterKeyNotFoundException(nameof(VoiceTableOffset), null); + } + VoiceTableOffset = (int)Util.Utils.ParseValue(nameof(VoiceTableOffset), voiceTableOffsetNode.ToString(), 0, maxOffset); + if (sampleTableOffsetNode == null) + { + throw new BetterKeyNotFoundException(nameof(SampleTableOffset), null); + } + SampleTableOffset = (int)Util.Utils.ParseValue(nameof(SampleTableOffset), sampleTableOffsetNode.ToString(), 0, maxOffset); + if (sampleTableSizeNode == null) + { + throw new BetterKeyNotFoundException(nameof(SampleTableSize), null); + } + SampleTableSize = Util.Utils.ParseValue(nameof(SampleTableSize), sampleTableSizeNode.ToString(), 0, maxOffset); + + // The complete playlist + if (!Playlists.Any(p => p.Name == "Music")) + { + Playlists.Insert(0, new Playlist(Strings.PlaylistMusic, Playlists.SelectMany(p => p.Songs).Distinct().OrderBy(s => s.Index))); + } + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); + } + catch (InvalidValueException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + ex.Message)); + } + catch (YamlDotNet.Core.YamlException ex) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + ex.Message)); + } + } + } + + public override string GetGameName() + { + return Name; + } + public override string GetSongName(long index) + { + Song s = GetFirstSong(index); + if (s != null) + { + return s.Name; + } + return index.ToString(); + } + + public override void Dispose() + { + Reader.Dispose(); + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Enums.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Enums.cs new file mode 100644 index 00000000..7c1a9e14 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/Enums.cs @@ -0,0 +1,16 @@ +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal enum AudioEngineVersion : byte + { + Hamtaro, + MLSS + } + + internal enum EnvelopeState : byte + { + Attack, + Decay, + Sustain, + Release + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Mixer.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Mixer.cs new file mode 100644 index 00000000..de980045 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/Mixer.cs @@ -0,0 +1,137 @@ +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal class Mixer : Core.Mixer + { + public readonly float SampleRateReciprocal; + private readonly float _samplesReciprocal; + public readonly int SamplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + public readonly Config Config; + private readonly WaveBuffer _audio; + private readonly float[][] _trackBuffers = new float[Player.NumTracks][]; + private readonly BufferedWaveProvider _buffer; + + public Mixer(Config config) + { + Config = config; + const int sampleRate = 13379; // TODO: Actual value unknown + SamplesPerBuffer = 224; // TODO + SampleRateReciprocal = 1f / sampleRate; + _samplesReciprocal = 1f / SamplesPerBuffer; + + int amt = SamplesPerBuffer * 2; + _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; + for (int i = 0; i < Player.NumTracks; i++) + { + _trackBuffers[i] = new float[amt]; + } + _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, 2)) // TODO + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64 + }; + Init(_buffer); + } + public override void Dispose() + { + base.Dispose(); + CloseWaveWriter(); + } + + public void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * Utils.AGB_FPS); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + public void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * Utils.AGB_FPS); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + public bool IsFading() + { + return _isFading; + } + public bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + public void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + private WaveFileWriter _waveWriter; + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter?.Dispose(); + } + public void Process(Track[] tracks, bool output, bool recording) + { + _audio.Clear(); + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < Player.NumTracks; i++) + { + Track track = tracks[i]; + if (track.Enabled && track.NoteDuration != 0 && !track.Channel.Stopped && !Mutes[i]) + { + float level = masterLevel; + float[] buf = _trackBuffers[i]; + Array.Clear(buf, 0, buf.Length); + track.Channel.Process(buf); + for (int j = 0; j < SamplesPerBuffer; j++) + { + _audio.FloatBuffer[j * 2] += buf[j * 2] * level; + _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + level += masterStep; + } + } + } + if (output) + { + _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + if (recording) + { + _waveWriter.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Player.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Player.cs new file mode 100644 index 00000000..d075b8cc --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/Player.cs @@ -0,0 +1,696 @@ +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal class Player : IPlayer + { + public const int NumTracks = 12; // 8 PCM, 4 PSG + private readonly Track[] _tracks = new Track[NumTracks]; + private readonly Mixer _mixer; + private readonly Config _config; + private readonly TimeBarrier _time; + private Thread _thread; + private byte _tempo; + private int _tempoStack; + private long _elapsedLoops; + + public List[] Events { get; private set; } + public long MaxTicks { get; private set; } + public long ElapsedTicks { get; private set; } + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + private int _longestTrack; + + public PlayerState State { get; private set; } + public event SongEndedEvent SongEnded; + + public Player(Mixer mixer, Config config) + { + for (byte i = 0; i < NumTracks; i++) + { + _tracks[i] = new Track(i, mixer); + } + _mixer = mixer; + _config = config; + + _time = new TimeBarrier(Utils.AGB_FPS); + } + private void CreateThread() + { + _thread = new Thread(Tick) { Name = "AlphaDream Player Tick" }; + _thread.Start(); + } + private void WaitThread() + { + if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) + { + _thread.Join(); + } + } + + private void InitEmulation() + { + _tempo = 120; // Player tempo is set to 75 on init, but I did not separate player and track tempo yet + _tempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + _mixer.ResetFade(); + for (int i = 0; i < NumTracks; i++) + { + _tracks[i].Init(); + } + } + private void SetTicks() + { + MaxTicks = 0; + bool u = false; + for (int trackIndex = 0; trackIndex < NumTracks; trackIndex++) + { + if (Events[trackIndex] == null) + { + continue; + } + + Events[trackIndex] = Events[trackIndex].OrderBy(e => e.Offset).ToList(); + List evs = Events[trackIndex]; + Track track = _tracks[trackIndex]; + track.Init(); + ElapsedTicks = 0; + while (true) + { + SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); + if (e.Ticks.Count > 0) + { + break; + } + + e.Ticks.Add(ElapsedTicks); + ExecuteNext(track, ref u); + if (track.Stopped) + { + break; + } + + ElapsedTicks += track.Rest; + track.Rest = 0; + } + if (ElapsedTicks > MaxTicks) + { + _longestTrack = trackIndex; + MaxTicks = ElapsedTicks; + } + track.NoteDuration = 0; + } + } + public void LoadSong(long index) + { + int songOffset = _config.Reader.ReadInt32(_config.SongTableOffsets[0] + (index * 4)); + if (songOffset == 0) + { + Events = null; + } + else + { + Events = new List[NumTracks]; + songOffset -= Utils.CartridgeOffset; + ushort trackBits = _config.Reader.ReadUInt16(songOffset); + for (int i = 0, usedTracks = 0; i < NumTracks; i++) + { + Track track = _tracks[i]; + if ((trackBits & (1 << i)) == 0) + { + track.Enabled = false; + track.StartOffset = 0; + } + else + { + track.Enabled = true; + Events[i] = new List(); + bool EventExists(long offset) + { + return Events[i].Any(e => e.Offset == offset); + } + + AddEvents(track.StartOffset = songOffset + _config.Reader.ReadInt16(songOffset + 2 + (2 * usedTracks++))); + void AddEvents(int startOffset) + { + _config.Reader.BaseStream.Position = startOffset; + bool cont = true; + while (cont) + { + long offset = _config.Reader.BaseStream.Position; + void AddEvent(ICommand command) + { + Events[i].Add(new SongEvent(offset, command)); + } + byte cmd = _config.Reader.ReadByte(); + switch (cmd) + { + case 0x00: + { + byte keyArg = _config.Reader.ReadByte(); + switch (_config.AudioEngineVersion) + { + case AudioEngineVersion.Hamtaro: + { + byte volume = _config.Reader.ReadByte(); + byte duration = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new FreeNoteHamtaroCommand { Key = (byte)(keyArg - 0x80), Volume = volume, Duration = duration }); + } + break; + } + case AudioEngineVersion.MLSS: + { + byte duration = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new FreeNoteMLSSCommand { Key = (byte)(keyArg - 0x80), Duration = duration }); + } + break; + } + } + break; + } + case 0xF0: + { + byte voice = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new VoiceCommand { Voice = voice }); + } + break; + } + case 0xF1: + { + byte volume = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new VolumeCommand { Volume = volume }); + } + break; + } + case 0xF2: + { + byte panArg = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new PanpotCommand { Panpot = (sbyte)(panArg - 0x80) }); + } + break; + } + case 0xF4: + { + byte range = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new PitchBendRangeCommand { Range = range }); + } + break; + } + case 0xF5: + { + sbyte bend = _config.Reader.ReadSByte(); + if (!EventExists(offset)) + { + AddEvent(new PitchBendCommand { Bend = bend }); + } + break; + } + case 0xF6: + { + byte rest = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = rest }); + } + break; + } + case 0xF8: + { + short jumpOffset = _config.Reader.ReadInt16(); + if (!EventExists(offset)) + { + int off = (int)(_config.Reader.BaseStream.Position + jumpOffset); + AddEvent(new JumpCommand { Offset = off }); + if (!EventExists(off)) + { + AddEvents(off); + } + } + cont = false; + break; + } + case 0xF9: + { + byte tempoArg = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new TrackTempoCommand { Tempo = tempoArg }); + } + break; + } + case 0xFF: + { + if (!EventExists(offset)) + { + AddEvent(new FinishCommand()); + } + cont = false; + break; + } + default: + { + if (cmd <= 0xDF) + { + byte key = _config.Reader.ReadByte(); + switch (_config.AudioEngineVersion) + { + case AudioEngineVersion.Hamtaro: + { + byte volume = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new NoteHamtaroCommand { Key = key, Volume = volume, Duration = cmd }); + } + break; + } + case AudioEngineVersion.MLSS: + { + if (!EventExists(offset)) + { + AddEvent(new NoteMLSSCommand { Key = key, Duration = cmd }); + } + break; + } + } + } + else + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, i, offset, cmd)); + } + break; + } + } + } + } + } + } + SetTicks(); + } + } + public void SetCurrentPosition(long ticks) + { + if (Events == null) + { + SongEnded?.Invoke(); + } + else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + if (State == PlayerState.Playing) + { + Pause(); + } + InitEmulation(); + bool u = false; + while (true) + { + if (ElapsedTicks == ticks) + { + goto finish; + } + + while (_tempoStack >= 75) + { + _tempoStack -= 75; + for (int trackIndex = 0; trackIndex < NumTracks; trackIndex++) + { + Track track = _tracks[trackIndex]; + if (track.Enabled && !track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track, ref u); + } + } + } + ElapsedTicks++; + if (ElapsedTicks == ticks) + { + goto finish; + } + } + _tempoStack += _tempo; + } + finish: + for (int i = 0; i < NumTracks; i++) + { + _tracks[i].NoteDuration = 0; + } + Pause(); + } + } + public void Play() + { + if (Events == null) + { + SongEnded?.Invoke(); + } + else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + Stop(); + InitEmulation(); + State = PlayerState.Playing; + CreateThread(); + } + } + public void Pause() + { + if (State == PlayerState.Playing) + { + State = PlayerState.Paused; + WaitThread(); + } + else if (State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.Playing; + CreateThread(); + } + } + public void Stop() + { + if (State == PlayerState.Playing || State == PlayerState.Paused) + { + State = PlayerState.Stopped; + WaitThread(); + } + } + public void Record(string fileName) + { + _mixer.CreateWaveWriter(fileName); + InitEmulation(); + State = PlayerState.Recording; + CreateThread(); + WaitThread(); + _mixer.CloseWaveWriter(); + } + public void Dispose() + { + if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.ShutDown; + WaitThread(); + } + } + public void GetSongState(UI.SongInfoControl.SongInfo info) + { + info.Tempo = _tempo; + for (int i = 0; i < NumTracks; i++) + { + Track track = _tracks[i]; + if (track.Enabled) + { + UI.SongInfoControl.SongInfo.Track tin = info.Tracks[i]; + tin.Position = track.DataOffset; + tin.Rest = track.Rest; + tin.Voice = track.Voice; + tin.Type = track.Type; + tin.Volume = track.Volume; + tin.PitchBend = track.GetPitch(); + tin.Panpot = track.Panpot; + if (track.NoteDuration != 0 && !track.Channel.Stopped) + { + tin.Keys[0] = track.Channel.Key; + ChannelVolume vol = track.Channel.GetVolume(); + tin.LeftVolume = vol.LeftVol; + tin.RightVolume = vol.RightVol; + } + else + { + tin.Keys[0] = byte.MaxValue; + tin.LeftVolume = 0f; + tin.RightVolume = 0f; + } + } + } + } + + private VoiceEntry GetVoiceEntry(byte voice, byte key) + { + int vto = _config.VoiceTableOffset; + short voiceOffset = _config.Reader.ReadInt16(vto + (voice * 2)); + short nextVoiceOffset = _config.Reader.ReadInt16(vto + ((voice + 1) * 2)); + if (voiceOffset == nextVoiceOffset) + { + return null; + } + else + { + long pos = vto + voiceOffset; // Prevent object creation in the last iteration + VoiceEntry e = _config.Reader.ReadObject(pos); + while (e.MinKey > key || e.MaxKey < key) + { + pos += 8; + if (pos == nextVoiceOffset) + { + return null; + } + e = _config.Reader.ReadObject(); + } + return e; + } + } + private void PlayNote(Track track, byte key, byte duration) + { + VoiceEntry entry = GetVoiceEntry(track.Voice, key); + if (entry != null) + { + track.NoteDuration = duration; + if (track.Index >= 8) + { + // TODO: "Sample" byte in VoiceEntry + ((SquareChannel)track.Channel).Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, track.Volume, track.Panpot, track.GetPitch()); + } + else + { + int sto = _config.SampleTableOffset; + int sampleOffset = _config.Reader.ReadInt32(sto + (entry.Sample * 4)); // Some entries are 0. If you play them, are they silent, or does it not care if they are 0? + ((PCMChannel)track.Channel).Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, sto + sampleOffset, entry.IsFixedFrequency == 0x80); + track.Channel.SetVolume(track.Volume, track.Panpot); + track.Channel.SetPitch(track.GetPitch()); + } + } + } + private void ExecuteNext(Track track, ref bool update) + { + byte cmd = _config.ROM[track.DataOffset++]; + switch (cmd) + { + case 0x00: // Free Note + { + byte key = (byte)(_config.ROM[track.DataOffset++] - 0x80); + if (_config.AudioEngineVersion == AudioEngineVersion.Hamtaro) + { + track.Volume = _config.ROM[track.DataOffset++]; + update = true; + } + byte duration = _config.ROM[track.DataOffset++]; + track.Rest += duration; + if (track.PrevCommand == 0 && track.Channel.Key == key) + { + track.NoteDuration += duration; + } + else + { + PlayNote(track, key, duration); + } + break; + } + case 0xF0: // Voice + { + track.Voice = _config.ROM[track.DataOffset++]; + break; + } + case 0xF1: // Volume + { + track.Volume = _config.ROM[track.DataOffset++]; + update = true; + break; + } + case 0xF2: // Panpot + { + track.Panpot = (sbyte)(_config.ROM[track.DataOffset++] - 0x80); + update = true; + break; + } + case 0xF4: // Pitch Bend Range + { + track.PitchBendRange = _config.ROM[track.DataOffset++]; + update = true; + break; + } + case 0xF5: // Pitch Bend + { + track.PitchBend = (sbyte)_config.ROM[track.DataOffset++]; + update = true; + break; + } + case 0xF6: // Rest + { + track.Rest = _config.ROM[track.DataOffset++]; + break; + } + case 0xF8: // Jump + { + short ofs = (short)(_config.ROM[track.DataOffset++] | (_config.ROM[track.DataOffset++] << 8)); // Cast to short is necessary + track.DataOffset += ofs; + break; + } + case 0xF9: // Track Tempo + { + _tempo = _config.ROM[track.DataOffset++]; + break; + } + case 0xFF: // Finish + { + track.Stopped = true; + break; + } + default: + { + if (cmd <= 0xDF) // Note + { + byte key = _config.ROM[track.DataOffset++]; + if (_config.AudioEngineVersion == AudioEngineVersion.Hamtaro) + { + track.Volume = _config.ROM[track.DataOffset++]; + update = true; + } + track.Rest += cmd; + if (track.PrevCommand == 0 && track.Channel.Key == key) + { + track.NoteDuration += cmd; + } + else + { + PlayNote(track, key, cmd); + } + } + break; + } + } + + track.PrevCommand = cmd; + } + + private void Tick() + { + _time.Start(); + while (true) + { + PlayerState state = State; + bool playing = state == PlayerState.Playing; + bool recording = state == PlayerState.Recording; + if (!playing && !recording) + { + goto stop; + } + + void MixerProcess() + { + _mixer.Process(_tracks, playing, recording); + } + + while (_tempoStack >= 75) + { + _tempoStack -= 75; + bool allDone = true; + for (int trackIndex = 0; trackIndex < NumTracks; trackIndex++) + { + Track track = _tracks[trackIndex]; + if (track.Enabled) + { + byte prevDuration = track.NoteDuration; + track.Tick(); + bool update = false; + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track, ref update); + } + if (trackIndex == _longestTrack) + { + if (ElapsedTicks == MaxTicks) + { + if (!track.Stopped) + { + List evs = Events[trackIndex]; + for (int i = 0; i < evs.Count; i++) + { + SongEvent ev = evs[i]; + if (ev.Offset == track.DataOffset) + { + ElapsedTicks = ev.Ticks[0] - track.Rest; + break; + } + } + _elapsedLoops++; + if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) + { + _mixer.BeginFadeOut(); + } + } + } + else + { + ElapsedTicks++; + } + } + if (prevDuration == 1 && track.NoteDuration == 0) // Note was not renewed + { + track.Channel.State = EnvelopeState.Release; + } + if (!track.Stopped) + { + allDone = false; + } + if (track.NoteDuration != 0) + { + allDone = false; + if (update) + { + track.Channel.SetVolume(track.Volume, track.Panpot); + track.Channel.SetPitch(track.GetPitch()); + } + } + } + } + if (_mixer.IsFadeDone()) + { + allDone = true; + } + if (allDone) + { + MixerProcess(); + State = PlayerState.Stopped; + SongEnded?.Invoke(); + } + } + _tempoStack += _tempo; + MixerProcess(); + if (playing) + { + _time.Wait(); + } + } + stop: + _time.Stop(); + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs new file mode 100644 index 00000000..b281c3d9 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs @@ -0,0 +1,180 @@ +using Kermalis.DLS2; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal sealed class SoundFontSaver_DLS + { + // Since every key will use the same articulation data, just store one instance + private static readonly Level2ArticulatorChunk _art2 = new Level2ArticulatorChunk() + { + new Level2ArticulatorConnectionBlock() { Destination = Level2ArticulatorDestination.LFOFrequency, Scale = 2786 }, + new Level2ArticulatorConnectionBlock() { Destination = Level2ArticulatorDestination.VIBFrequency, Scale = 2786 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.KeyNumber, Destination = Level2ArticulatorDestination.Pitch }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.Modulation_CC1, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.ChannelPressure, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Pan_CC10, Destination = Level2ArticulatorDestination.Pan, BipolarSource = true, Scale = 0xFE0000 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.ChorusSend_CC91, Destination = Level2ArticulatorDestination.Reverb, Scale = 0xC80000 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Reverb_SendCC93, Destination = Level2ArticulatorDestination.Chorus, Scale = 0xC80000 } + }; + + public static void Save(Config config, string path) + { + var dls = new DLS(); + AddInfo(config, dls); + Dictionary sampleDict = AddSamples(config, dls); + AddInstruments(config, dls, sampleDict); + dls.Save(path); + } + + private static void AddInfo(Config config, DLS dls) + { + var info = new ListChunk("INFO"); + dls.Add(info); + info.Add(new InfoSubChunk("INAM", config.Name)); + //info.Add(new InfoSubChunk("ICOP", config.Creator)); + info.Add(new InfoSubChunk("IENG", "Kermalis")); + info.Add(new InfoSubChunk("ISFT", Util.Utils.ProgramName)); + } + + private static Dictionary AddSamples(Config config, DLS dls) + { + ListChunk waves = dls.WavePool; + var sampleDict = new Dictionary((int)config.SampleTableSize); + for (int i = 0; i < config.SampleTableSize; i++) + { + int ofs = config.Reader.ReadInt32(config.SampleTableOffset + (i * 4)); + if (ofs == 0) + { + continue; // Skip null samples + } + + ofs += config.SampleTableOffset; + SampleHeader sh = config.Reader.ReadObject(ofs); + + // Create format chunk + var fmt = new FormatChunk(WaveFormat.PCM); + fmt.WaveInfo.Channels = 1; + fmt.WaveInfo.SamplesPerSec = (uint)(sh.SampleRate >> 10); + fmt.WaveInfo.AvgBytesPerSec = fmt.WaveInfo.SamplesPerSec; + fmt.WaveInfo.BlockAlign = 1; + fmt.FormatInfo.BitsPerSample = 8; + // Create wave sample chunk and add loop if there is one + var wsmp = new WaveSampleChunk() + { + UnityNote = 60, + Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression + }; + if (sh.DoesLoop == 0x40000000) + { + wsmp.Loop = new WaveSampleLoop + { + LoopStart = (uint)sh.LoopOffset, + LoopLength = (uint)(sh.Length - sh.LoopOffset), + LoopType = LoopType.Forward + }; + } + // Get PCM sample + byte[] pcm = new byte[sh.Length]; + System.Array.Copy(config.ROM, ofs + 0x10, pcm, 0, sh.Length); + + // Add + int dlsIndex = waves.Count; + waves.Add(new ListChunk("wave") + { + fmt, + wsmp, + new DataChunk(pcm), + new ListChunk("INFO") + { + new InfoSubChunk("INAM", $"Sample {i}") + } + }); + sampleDict.Add(i, (wsmp, dlsIndex)); + } + return sampleDict; + } + + private static void AddInstruments(Config config, DLS dls, Dictionary sampleDict) + { + ListChunk lins = dls.InstrumentList; + for (int v = 0; v < 256; v++) + { + short off = config.Reader.ReadInt16(config.VoiceTableOffset + (v * 2)); + short nextOff = config.Reader.ReadInt16(config.VoiceTableOffset + ((v + 1) * 2)); + int numEntries = (nextOff - off) / 8; // Each entry is 8 bytes + if (numEntries == 0) + { + continue; // Skip empty entries + } + + var ins = new ListChunk("ins "); + ins.Add(new InstrumentHeaderChunk + { + NumRegions = (uint)numEntries, + Locale = new MIDILocale(0, (byte)(v / 128), false, (byte)(v % 128)) + }); + var lrgn = new ListChunk("lrgn"); + ins.Add(lrgn); + ins.Add(new ListChunk("INFO") + { + new InfoSubChunk("INAM", $"Instrument {v}") + }); + lins.Add(ins); + for (int e = 0; e < numEntries; e++) + { + VoiceEntry entry = config.Reader.ReadObject(config.VoiceTableOffset + off + (e * 8)); + // Sample + if (entry.Sample >= config.SampleTableSize) + { + Debug.WriteLine(string.Format("Voice {0} uses an invalid sample id ({1})", v, entry.Sample)); + continue; + } + if (!sampleDict.TryGetValue(entry.Sample, out (WaveSampleChunk, int) value)) + { + Debug.WriteLine(string.Format("Voice {0} uses a null sample id ({1})", v, entry.Sample)); + continue; + } + void Add(ushort low, ushort high, ushort baseKey) + { + var rgnh = new RegionHeaderChunk(); + rgnh.KeyRange.Low = low; + rgnh.KeyRange.High = high; + lrgn.Add(new ListChunk("rgn2") + { + rgnh, + new WaveSampleChunk() + { + UnityNote = baseKey, + Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression, + Loop = value.Item1.Loop + }, + new WaveLinkChunk() + { + Channels = WaveLinkChannels.Left, + TableIndex = (uint)value.Item2 + }, + new ListChunk("lar2") + { + _art2 + } + }); + } + // Fixed frequency - Since DLS does not support it, we need to manually add every key with its own base note + if (entry.IsFixedFrequency == 0x80) + { + for (ushort i = entry.MinKey; i <= entry.MaxKey; i++) + { + Add(i, i, i); + } + } + else + { + Add(entry.MinKey, entry.MaxKey, 60); + } + } + } + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs new file mode 100644 index 00000000..6e0aa028 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs @@ -0,0 +1,97 @@ +using Kermalis.SoundFont2; +using Kermalis.VGMusicStudio.Util; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal sealed class SoundFontSaver_SF2 + { + public static void Save(Config config, string path) + { + var sf2 = new SF2(); + AddInfo(config, sf2); + Dictionary sampleDict = AddSamples(config, sf2); + AddInstruments(config, sf2, sampleDict); + sf2.Save(path); + } + + private static void AddInfo(Config config, SF2 sf2) + { + sf2.InfoChunk.Bank = config.Name; + //sf2.InfoChunk.Copyright = config.Creator; + sf2.InfoChunk.Tools = Util.Utils.ProgramName + " by Kermalis"; + } + + private static Dictionary AddSamples(Config config, SF2 sf2) + { + var sampleDict = new Dictionary((int)config.SampleTableSize); + for (int i = 0; i < config.SampleTableSize; i++) + { + int ofs = config.Reader.ReadInt32(config.SampleTableOffset + (i * 4)); + if (ofs == 0) + { + continue; + } + + ofs += config.SampleTableOffset; + SampleHeader sh = config.Reader.ReadObject(ofs); + + short[] pcm16 = SampleUtils.PCMU8ToPCM16(config.ROM, ofs + 0x10, sh.Length); + int sf2Index = (int)sf2.AddSample(pcm16, $"Sample {i}", sh.DoesLoop == 0x40000000, (uint)sh.LoopOffset, (uint)(sh.SampleRate >> 10), 60, 0); + sampleDict.Add(i, (sh, sf2Index)); + } + return sampleDict; + } + private static void AddInstruments(Config config, SF2 sf2, Dictionary sampleDict) + { + for (int v = 0; v < 256; v++) + { + short off = config.Reader.ReadInt16(config.VoiceTableOffset + (v * 2)); + short nextOff = config.Reader.ReadInt16(config.VoiceTableOffset + ((v + 1) * 2)); + int numEntries = (nextOff - off) / 8; // Each entry is 8 bytes + if (numEntries == 0) + { + continue; + } + + string name = "Instrument " + v; + sf2.AddPreset(name, (ushort)v, 0); + sf2.AddPresetBag(); + sf2.AddPresetGenerator(SF2Generator.Instrument, new SF2GeneratorAmount { Amount = (short)sf2.AddInstrument(name) }); + for (int e = 0; e < numEntries; e++) + { + VoiceEntry entry = config.Reader.ReadObject(config.VoiceTableOffset + off + (e * 8)); + sf2.AddInstrumentBag(); + // Key range + if (!(entry.MinKey == 0 && entry.MaxKey == 0x7F)) + { + sf2.AddInstrumentGenerator(SF2Generator.KeyRange, new SF2GeneratorAmount { LowByte = entry.MinKey, HighByte = entry.MaxKey }); + } + // Fixed frequency + if (entry.IsFixedFrequency == 0x80) + { + sf2.AddInstrumentGenerator(SF2Generator.ScaleTuning, new SF2GeneratorAmount { Amount = 0 }); + } + // Sample + if (entry.Sample < config.SampleTableSize) + { + if (!sampleDict.TryGetValue(entry.Sample, out (SampleHeader, int) value)) + { + Debug.WriteLine(string.Format("Voice {0} uses a null sample id ({1})", v, entry.Sample)); + } + else + { + sf2.AddInstrumentGenerator(SF2Generator.SampleModes, new SF2GeneratorAmount { Amount = (short)(value.Item1.DoesLoop == 0x40000000 ? 1 : 0) }); + sf2.AddInstrumentGenerator(SF2Generator.SampleID, new SF2GeneratorAmount { UAmount = (ushort)value.Item2 }); + } + } + else + { + Debug.WriteLine(string.Format("Voice {0} uses an invalid sample id ({1})", v, entry.Sample)); + } + } + } + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Structs.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Structs.cs new file mode 100644 index 00000000..ac3b2bb3 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/Structs.cs @@ -0,0 +1,33 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal class SampleHeader + { + /// 0x40000000 if True + public int DoesLoop { get; set; } + /// Right shift 10 for value + public int SampleRate { get; set; } + public int LoopOffset { get; set; } + public int Length { get; set; } + } + internal class VoiceEntry + { + public byte MinKey { get; set; } + public byte MaxKey { get; set; } + public byte Sample { get; set; } + /// 0x80 if True + public byte IsFixedFrequency { get; set; } + [BinaryArrayFixedLength(4)] + public byte[] Unknown { get; set; } + } + + internal struct ChannelVolume + { + public float LeftVol, RightVol; + } + internal class ADSR // TODO + { + public byte A, D, S, R; + } +} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Track.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Track.cs new file mode 100644 index 00000000..296a2840 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/AlphaDream/Track.cs @@ -0,0 +1,69 @@ +namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream +{ + internal class Track + { + public readonly byte Index; + public readonly string Type; + public readonly Channel Channel; + + public byte Voice; + public byte PitchBendRange; + public byte Volume; + public byte Rest; + public byte NoteDuration; + public sbyte PitchBend; + public sbyte Panpot; + public bool Enabled; + public bool Stopped; + public int StartOffset; + public int DataOffset; + public byte PrevCommand; + + public int GetPitch() + { + return PitchBend * (PitchBendRange / 2); + } + + public Track(byte i, Mixer mixer) + { + Index = i; + if (i >= 8) + { + Type = Utils.PSGTypes[i & 3]; + Channel = new SquareChannel(mixer); // TODO: PSG Channels 3 and 4 + } + else + { + Type = "PCM8"; + Channel = new PCMChannel(mixer); + } + } + // 0x819B040 + public void Init() + { + Voice = 0; + Rest = 1; // Unsure why Rest starts at 1 + PitchBendRange = 2; + NoteDuration = 0; + PitchBend = 0; + Panpot = 0; // Start centered; ROM sets this to 0x7F since it's unsigned there + DataOffset = StartOffset; + Stopped = false; + Volume = 200; + PrevCommand = 0xFF; + //Tempo = 120; + //TempoStack = 0; + } + public void Tick() + { + if (Rest != 0) + { + Rest--; + } + if (NoteDuration > 0) + { + NoteDuration--; + } + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Channel.cs b/VG Music Studio.backup/Core/GBA/MP2K/Channel.cs new file mode 100644 index 00000000..faa606d1 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/MP2K/Channel.cs @@ -0,0 +1,777 @@ +using System; +using System.Collections; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K +{ + internal abstract class Channel + { + public EnvelopeState State = EnvelopeState.Dead; + public Track Owner; + protected readonly Mixer _mixer; + + public Note Note; // Must be a struct & field + protected ADSR _adsr; + protected int _instPan; + + protected byte _velocity; + protected int _pos; + protected float _interPos; + protected float _frequency; + + protected Channel(Mixer mixer) + { + _mixer = mixer; + } + + public abstract ChannelVolume GetVolume(); + public abstract void SetVolume(byte vol, sbyte pan); + public abstract void SetPitch(int pitch); + public virtual void Release() + { + if (State < EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + } + } + + public abstract void Process(float[] buffer); + + // Returns whether the note is active or not + public virtual bool TickNote() + { + if (State < EnvelopeState.Releasing) + { + if (Note.Duration > 0) + { + Note.Duration--; + if (Note.Duration == 0) + { + State = EnvelopeState.Releasing; + return false; + } + return true; + } + else + { + return true; + } + } + else + { + return false; + } + } + public void Stop() + { + State = EnvelopeState.Dead; + if (Owner != null) + { + Owner.Channels.Remove(this); + } + Owner = null; + } + } + internal class PCM8Channel : Channel + { + private SampleHeader _sampleHeader; + private int _sampleOffset; + private GoldenSunPSG _gsPSG; + private bool _bFixed; + private bool _bGoldenSun; + private bool _bCompressed; + private byte _leftVol; + private byte _rightVol; + private sbyte[] _decompressedSample; + + public PCM8Channel(Mixer mixer) : base(mixer) { } + public void Init(Track owner, Note note, ADSR adsr, int sampleOffset, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed) + { + State = EnvelopeState.Initializing; + _pos = 0; _interPos = 0; + if (Owner != null) + { + Owner.Channels.Remove(this); + } + Owner = owner; + Owner.Channels.Add(this); + Note = note; + _adsr = adsr; + _instPan = instPan; + _sampleHeader = _mixer.Config.Reader.ReadObject(sampleOffset); + _sampleOffset = sampleOffset + 0x10; + _bFixed = bFixed; + _bCompressed = bCompressed; + _decompressedSample = bCompressed ? Utils.Decompress(_sampleOffset, _sampleHeader.Length) : null; + _bGoldenSun = _mixer.Config.HasGoldenSunSynths && _sampleHeader.DoesLoop == 0x40000000 && _sampleHeader.LoopOffset == 0 && _sampleHeader.Length == 0; + if (_bGoldenSun) + { + _gsPSG = _mixer.Config.Reader.ReadObject(_sampleOffset); + } + SetVolume(vol, pan); + SetPitch(pitch); + } + + public override ChannelVolume GetVolume() + { + const float max = 0x10000; + return new ChannelVolume + { + LeftVol = _leftVol * _velocity / max * _mixer.PCM8MasterVolume, + RightVol = _rightVol * _velocity / max * _mixer.PCM8MasterVolume + }; + } + public override void SetVolume(byte vol, sbyte pan) + { + int combinedPan = pan + _instPan; + if (combinedPan > 63) + { + combinedPan = 63; + } + else if (combinedPan < -64) + { + combinedPan = -64; + } + const int fix = 0x2000; + if (State < EnvelopeState.Releasing) + { + int a = Note.Velocity * vol; + _leftVol = (byte)(a * (-combinedPan + 0x40) / fix); + _rightVol = (byte)(a * (combinedPan + 0x40) / fix); + } + } + public override void SetPitch(int pitch) + { + _frequency = (_sampleHeader.SampleRate >> 10) * (float)Math.Pow(2, ((Note.Key - 60) / 12f) + (pitch / 768f)); + } + + private void StepEnvelope() + { + switch (State) + { + case EnvelopeState.Initializing: + { + _velocity = _adsr.A; + State = EnvelopeState.Rising; + break; + } + case EnvelopeState.Rising: + { + int nextVel = _velocity + _adsr.A; + if (nextVel >= 0xFF) + { + State = EnvelopeState.Decaying; + _velocity = 0xFF; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Decaying: + { + int nextVel = (_velocity * _adsr.D) >> 8; + if (nextVel <= _adsr.S) + { + State = EnvelopeState.Playing; + _velocity = _adsr.S; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Playing: + { + break; + } + case EnvelopeState.Releasing: + { + int nextVel = (_velocity * _adsr.R) >> 8; + if (nextVel <= 0) + { + State = EnvelopeState.Dying; + _velocity = 0; + } + else + { + _velocity = (byte)nextVel; + } + break; + } + case EnvelopeState.Dying: + { + Stop(); + break; + } + } + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + if (State == EnvelopeState.Dead) + { + return; + } + + ChannelVolume vol = GetVolume(); + float interStep = _bFixed && !_bGoldenSun ? _mixer.SampleRate * _mixer.SampleRateReciprocal : _frequency * _mixer.SampleRateReciprocal; + if (_bGoldenSun) // Most Golden Sun processing is thanks to ipatix + { + interStep /= 0x40; + switch (_gsPSG.Type) + { + case GoldenSunPSGType.Square: + { + _pos += _gsPSG.CycleSpeed << 24; + int iThreshold = (_gsPSG.MinimumCycle << 24) + _pos; + iThreshold = (iThreshold < 0 ? ~iThreshold : iThreshold) >> 8; + iThreshold = (iThreshold * _gsPSG.CycleAmplitude) + (_gsPSG.InitialCycle << 24); + float threshold = iThreshold / (float)0x100000000; + + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _interPos < threshold ? 0.5f : -0.5f; + samp += 0.5f - threshold; + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + if (_interPos >= 1) + { + _interPos--; + } + } while (--samplesPerBuffer > 0); + break; + } + case GoldenSunPSGType.Saw: + { + const int fix = 0x70; + + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + _interPos += interStep; + if (_interPos >= 1) + { + _interPos--; + } + int var1 = (int)(_interPos * 0x100) - fix; + int var2 = (int)(_interPos * 0x10000) << 17; + int var3 = var1 - (var2 >> 27); + _pos = var3 + (_pos >> 1); + + float samp = _pos / (float)0x100; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + } while (--samplesPerBuffer > 0); + break; + } + case GoldenSunPSGType.Triangle: + { + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + _interPos += interStep; + if (_interPos >= 1) + { + _interPos--; + } + float samp = _interPos < 0.5f ? (_interPos * 4) - 1 : 3 - (_interPos * 4); + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + } while (--samplesPerBuffer > 0); + break; + } + } + } + else if (_bCompressed) + { + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _decompressedSample[_pos] / (float)0x80; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _decompressedSample.Length) + { + Stop(); + break; + } + } while (--samplesPerBuffer > 0); + } + else + { + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = (sbyte)_mixer.Config.ROM[_pos + _sampleOffset] / (float)0x80; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos += posDelta; + if (_pos >= _sampleHeader.Length) + { + if (_sampleHeader.DoesLoop == 0x40000000) + { + _pos = _sampleHeader.LoopOffset; + } + else + { + Stop(); + break; + } + } + } while (--samplesPerBuffer > 0); + } + } + } + internal abstract class PSGChannel : Channel + { + protected enum GBPan : byte + { + Left, + Center, + Right + } + + private byte _processStep; + private EnvelopeState _nextState; + private byte _peakVelocity; + private byte _sustainVelocity; + protected GBPan _panpot = GBPan.Center; + + public PSGChannel(Mixer mixer) : base(mixer) { } + protected void Init(Track owner, Note note, ADSR env, int instPan) + { + State = EnvelopeState.Initializing; + if (Owner != null) + { + Owner.Channels.Remove(this); + } + Owner = owner; + Owner.Channels.Add(this); + Note = note; + _adsr.A = (byte)(env.A & 0x7); + _adsr.D = (byte)(env.D & 0x7); + _adsr.S = (byte)(env.S & 0xF); + _adsr.R = (byte)(env.R & 0x7); + _instPan = instPan; + } + + public override void Release() + { + if (State < EnvelopeState.Releasing) + { + if (_adsr.R == 0) + { + _velocity = 0; + Stop(); + } + else if (_velocity == 0) + { + Stop(); + } + else + { + _nextState = EnvelopeState.Releasing; + } + } + } + public override bool TickNote() + { + if (State < EnvelopeState.Releasing) + { + if (Note.Duration > 0) + { + Note.Duration--; + if (Note.Duration == 0) + { + if (_velocity == 0) + { + Stop(); + } + else + { + State = EnvelopeState.Releasing; + } + return false; + } + return true; + } + else + { + return true; + } + } + else + { + return false; + } + } + + public override ChannelVolume GetVolume() + { + const float max = 0x20; + return new ChannelVolume + { + LeftVol = _panpot == GBPan.Right ? 0 : _velocity / max, + RightVol = _panpot == GBPan.Left ? 0 : _velocity / max + }; + } + public override void SetVolume(byte vol, sbyte pan) + { + int combinedPan = pan + _instPan; + if (combinedPan > 63) + { + combinedPan = 63; + } + else if (combinedPan < -64) + { + combinedPan = -64; + } + if (State < EnvelopeState.Releasing) + { + _panpot = combinedPan < -21 ? GBPan.Left : combinedPan > 20 ? GBPan.Right : GBPan.Center; + _peakVelocity = (byte)((Note.Velocity * vol) >> 10); + _sustainVelocity = (byte)(((_peakVelocity * _adsr.S) + 0xF) >> 4); // TODO + if (State == EnvelopeState.Playing) + { + _velocity = _sustainVelocity; + } + } + } + + protected void StepEnvelope() + { + void dec() + { + _processStep = 0; + if (_velocity - 1 <= _sustainVelocity) + { + _velocity = _sustainVelocity; + _nextState = EnvelopeState.Playing; + } + else if (_velocity != 0) + { + _velocity--; + } + } + void sus() + { + _processStep = 0; + } + void rel() + { + if (_adsr.R == 0) + { + _velocity = 0; + Stop(); + } + else + { + _processStep = 0; + if (_velocity - 1 <= 0) + { + _nextState = EnvelopeState.Dying; + _velocity = 0; + } + else + { + _velocity--; + } + } + } + + switch (State) + { + case EnvelopeState.Initializing: + { + _nextState = EnvelopeState.Rising; + _processStep = 0; + if ((_adsr.A | _adsr.D) == 0 || (_sustainVelocity == 0 && _peakVelocity == 0)) + { + State = EnvelopeState.Playing; + _velocity = _sustainVelocity; + return; + } + else if (_adsr.A == 0 && _adsr.S < 0xF) + { + State = EnvelopeState.Decaying; + int next = _peakVelocity - 1; + if (next < 0) + { + next = 0; + } + _velocity = (byte)next; + if (_velocity < _sustainVelocity) + { + _velocity = _sustainVelocity; + } + return; + } + else if (_adsr.A == 0) + { + State = EnvelopeState.Playing; + _velocity = _sustainVelocity; + return; + } + else + { + State = EnvelopeState.Rising; + _velocity = 1; + return; + } + } + case EnvelopeState.Rising: + { + if (++_processStep >= _adsr.A) + { + if (_nextState == EnvelopeState.Decaying) + { + State = EnvelopeState.Decaying; + dec(); return; + } + if (_nextState == EnvelopeState.Playing) + { + State = EnvelopeState.Playing; + sus(); return; + } + if (_nextState == EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + rel(); return; + } + _processStep = 0; + if (++_velocity >= _peakVelocity) + { + if (_adsr.D == 0) + { + _nextState = EnvelopeState.Playing; + } + else if (_peakVelocity == _sustainVelocity) + { + _nextState = EnvelopeState.Playing; + _velocity = _peakVelocity; + } + else + { + _velocity = _peakVelocity; + _nextState = EnvelopeState.Decaying; + } + } + } + break; + } + case EnvelopeState.Decaying: + { + if (++_processStep >= _adsr.D) + { + if (_nextState == EnvelopeState.Playing) + { + State = EnvelopeState.Playing; + sus(); return; + } + if (_nextState == EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + rel(); return; + } + dec(); + } + break; + } + case EnvelopeState.Playing: + { + if (++_processStep >= 1) + { + if (_nextState == EnvelopeState.Releasing) + { + State = EnvelopeState.Releasing; + rel(); return; + } + sus(); + } + break; + } + case EnvelopeState.Releasing: + { + if (++_processStep >= _adsr.R) + { + if (_nextState == EnvelopeState.Dying) + { + Stop(); + return; + } + rel(); + } + break; + } + } + } + } + internal class SquareChannel : PSGChannel + { + private float[] _pat; + + public SquareChannel(Mixer mixer) : base(mixer) { } + public void Init(Track owner, Note note, ADSR env, int instPan, SquarePattern pattern) + { + Init(owner, note, env, instPan); + switch (pattern) + { + default: _pat = Utils.SquareD12; break; + case SquarePattern.D25: _pat = Utils.SquareD25; break; + case SquarePattern.D50: _pat = Utils.SquareD50; break; + case SquarePattern.D75: _pat = Utils.SquareD75; break; + } + } + + public override void SetPitch(int pitch) + { + _frequency = 3520 * (float)Math.Pow(2, ((Note.Key - 69) / 12f) + (pitch / 768f)); + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + if (State == EnvelopeState.Dead) + { + return; + } + + ChannelVolume vol = GetVolume(); + float interStep = _frequency * _mixer.SampleRateReciprocal; + + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _pat[_pos]; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos = (_pos + posDelta) & 0x7; + } while (--samplesPerBuffer > 0); + } + } + internal class PCM4Channel : PSGChannel + { + private float[] _sample; + + public PCM4Channel(Mixer mixer) : base(mixer) { } + public void Init(Track owner, Note note, ADSR env, int instPan, int sampleOffset) + { + Init(owner, note, env, instPan); + _sample = Utils.PCM4ToFloat(sampleOffset); + } + + public override void SetPitch(int pitch) + { + _frequency = 7040 * (float)Math.Pow(2, ((Note.Key - 69) / 12f) + (pitch / 768f)); + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + if (State == EnvelopeState.Dead) + { + return; + } + + ChannelVolume vol = GetVolume(); + float interStep = _frequency * _mixer.SampleRateReciprocal; + + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _sample[_pos]; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos = (_pos + posDelta) & 0x1F; + } while (--samplesPerBuffer > 0); + } + } + internal class NoiseChannel : PSGChannel + { + private BitArray _pat; + + public NoiseChannel(Mixer mixer) : base(mixer) { } + public void Init(Track owner, Note note, ADSR env, int instPan, NoisePattern pattern) + { + Init(owner, note, env, instPan); + _pat = pattern == NoisePattern.Fine ? Utils.NoiseFine : Utils.NoiseRough; + } + + public override void SetPitch(int pitch) + { + int key = Note.Key + (int)Math.Round(pitch / 64f); + if (key <= 20) + { + key = 0; + } + else + { + key -= 21; + if (key > 59) + { + key = 59; + } + } + byte v = Utils.NoiseFrequencyTable[key]; + // The following emulates 0x0400007C - SOUND4CNT_H + int r = v & 7; // Bits 0-2 + int s = v >> 4; // Bits 4-7 + _frequency = 524288f / (r == 0 ? 0.5f : r) / (float)Math.Pow(2, s + 1); + } + + public override void Process(float[] buffer) + { + StepEnvelope(); + if (State == EnvelopeState.Dead) + { + return; + } + + ChannelVolume vol = GetVolume(); + float interStep = _frequency * _mixer.SampleRateReciprocal; + + int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; + do + { + float samp = _pat[_pos & (_pat.Length - 1)] ? 0.5f : -0.5f; + + buffer[bufPos++] += samp * vol.LeftVol; + buffer[bufPos++] += samp * vol.RightVol; + + _interPos += interStep; + int posDelta = (int)_interPos; + _interPos -= posDelta; + _pos = (_pos + posDelta) & (_pat.Length - 1); + } while (--samplesPerBuffer > 0); + } + } +} \ No newline at end of file diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Commands.cs b/VG Music Studio.backup/Core/GBA/MP2K/Commands.cs new file mode 100644 index 00000000..776d013f --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/MP2K/Commands.cs @@ -0,0 +1,193 @@ +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K +{ + internal class CallCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Call"; + public string Arguments => $"0x{Offset:X7}"; + + public int Offset { get; set; } + } + internal class EndOfTieCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "End Of Tie"; + public string Arguments => Key == -1 ? "All Ties" : Util.Utils.GetNoteName(Key); + + public int Key { get; set; } + } + internal class FinishCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Finish"; + public string Arguments => Prev ? "Resume previous track" : "End track"; + + public bool Prev { get; set; } + } + internal class JumpCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Jump"; + public string Arguments => $"0x{Offset:X7}"; + + public int Offset { get; set; } + } + internal class LFODelayCommand : ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Delay"; + public string Arguments => Delay.ToString(); + + public byte Delay { get; set; } + } + internal class LFODepthCommand : ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Depth"; + public string Arguments => Depth.ToString(); + + public byte Depth { get; set; } + } + internal class LFOSpeedCommand : ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Speed"; + public string Arguments => Speed.ToString(); + + public byte Speed { get; set; } + } + internal class LFOTypeCommand : ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Type"; + public string Arguments => Type.ToString(); + + public LFOType Type { get; set; } + } + internal class LibraryCommand : ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Library Call"; + public string Arguments => $"{Command}, {Argument}"; + + public byte Command { get; set; } + public byte Argument { get; set; } + } + internal class MemoryAccessCommand : ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Memory Access"; + public string Arguments => $"{Operator}, {Address}, {Data}"; + + public byte Operator { get; set; } + public byte Address { get; set; } + public byte Data { get; set; } + } + internal class NoteCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Velocity} {Duration}"; + + public byte Key { get; set; } + public byte Velocity { get; set; } + public int Duration { get; set; } + } + internal class PanpotCommand : ICommand + { + public Color Color => Color.GreenYellow; + public string Label => "Panpot"; + public string Arguments => Panpot.ToString(); + + public sbyte Panpot { get; set; } + } + internal class PitchBendCommand : ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend"; + public string Arguments => Bend.ToString(); + + public sbyte Bend { get; set; } + } + internal class PitchBendRangeCommand : ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend Range"; + public string Arguments => Range.ToString(); + + public byte Range { get; set; } + } + internal class PriorityCommand : ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Priority"; + public string Arguments => Priority.ToString(); + + public byte Priority { get; set; } + } + internal class RepeatCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Repeat"; + public string Arguments => $"{Times}, 0x{Offset:X7}"; + + public byte Times { get; set; } + public int Offset { get; set; } + } + internal class RestCommand : ICommand + { + public Color Color => Color.PaleVioletRed; + public string Label => "Rest"; + public string Arguments => Rest.ToString(); + + public byte Rest { get; set; } + } + internal class ReturnCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Return"; + public string Arguments => string.Empty; + } + internal class TempoCommand : ICommand + { + public Color Color => Color.DeepSkyBlue; + public string Label => "Tempo"; + public string Arguments => Tempo.ToString(); + + public ushort Tempo { get; set; } + } + internal class TransposeCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Transpose"; + public string Arguments => Transpose.ToString(); + + public sbyte Transpose { get; set; } + } + internal class TuneCommand : ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Fine Tune"; + public string Arguments => Tune.ToString(); + + public sbyte Tune { get; set; } + } + internal class VoiceCommand : ICommand + { + public Color Color => Color.DarkSalmon; + public string Label => "Voice"; + public string Arguments => Voice.ToString(); + + public byte Voice { get; set; } + } + internal class VolumeCommand : ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Volume"; + public string Arguments => Volume.ToString(); + + public byte Volume { get; set; } + } +} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Config.cs b/VG Music Studio.backup/Core/GBA/MP2K/Config.cs new file mode 100644 index 00000000..9a0ea239 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/MP2K/Config.cs @@ -0,0 +1,242 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using YamlDotNet.RepresentationModel; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K +{ + internal class Config : Core.Config + { + public readonly byte[] ROM; + public readonly EndianBinaryReader Reader; + public readonly string GameCode; + public readonly byte Version; + + public readonly string Name; + public readonly int[] SongTableOffsets; + public readonly long[] SongTableSizes; + public readonly int SampleRate; + public readonly ReverbType ReverbType; + public readonly byte Reverb; + public readonly byte Volume; + public readonly bool HasGoldenSunSynths; + public readonly bool HasPokemonCompression; + + public Config(byte[] rom) + { + const string configFile = "MP2K.yaml"; + using (StreamReader fileStream = File.OpenText(Util.Utils.CombineWithBaseDirectory(configFile))) + { + string gcv = string.Empty; + try + { + ROM = rom; + Reader = new EndianBinaryReader(new MemoryStream(rom)); + GameCode = Reader.ReadString(4, false, 0xAC); + Version = Reader.ReadByte(0xBC); + gcv = $"{GameCode}_{Version:X2}"; + var yaml = new YamlStream(); + yaml.Load(fileStream); + + var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; + YamlMappingNode game; + try + { + game = (YamlMappingNode)mapping.Children.GetValue(gcv); + } + catch (BetterKeyNotFoundException) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); + } + + YamlNode nameNode = null, + songTableOffsetsNode = null, + songTableSizesNode = null, + sampleRateNode = null, + reverbTypeNode = null, + reverbNode = null, + volumeNode = null, + hasGoldenSunSynthsNode = null, + hasPokemonCompression = null; + void Load(YamlMappingNode gameToLoad) + { + if (gameToLoad.Children.TryGetValue("Copy", out YamlNode node)) + { + YamlMappingNode copyGame; + try + { + copyGame = (YamlMappingNode)mapping.Children.GetValue(node); + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); + } + Load(copyGame); + } + if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) + { + nameNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) + { + songTableOffsetsNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) + { + songTableSizesNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(SampleRate), out node)) + { + sampleRateNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(ReverbType), out node)) + { + reverbTypeNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Reverb), out node)) + { + reverbNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Volume), out node)) + { + volumeNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(HasGoldenSunSynths), out node)) + { + hasGoldenSunSynthsNode = node; + } + if (gameToLoad.Children.TryGetValue(nameof(HasPokemonCompression), out node)) + { + hasPokemonCompression = node; + } + if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) + { + var playlists = (YamlMappingNode)node; + foreach (KeyValuePair kvp in playlists) + { + string name = kvp.Key.ToString(); + var songs = new List(); + foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) + { + long songIndex = Util.Utils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, long.MaxValue); + if (songs.Any(s => s.Index == songIndex)) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); + } + songs.Add(new Song(songIndex, song.Value.ToString())); + } + Playlists.Add(new Playlist(name, songs)); + } + } + } + + Load(game); + + if (nameNode == null) + { + throw new BetterKeyNotFoundException(nameof(Name), null); + } + Name = nameNode.ToString(); + if (songTableOffsetsNode == null) + { + throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); + } + string[] songTables = songTableOffsetsNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); + int numSongTables = songTables.Length; + if (numSongTables == 0) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); + } + if (songTableSizesNode == null) + { + throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); + } + string[] sizes = songTableSizesNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); + if (sizes.Length != numSongTables) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); + } + SongTableOffsets = new int[numSongTables]; + SongTableSizes = new long[numSongTables]; + int maxOffset = rom.Length - 1; + for (int i = 0; i < numSongTables; i++) + { + SongTableSizes[i] = Util.Utils.ParseValue(nameof(SongTableSizes), sizes[i], 1, maxOffset); + SongTableOffsets[i] = (int)Util.Utils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); + } + if (sampleRateNode == null) + { + throw new BetterKeyNotFoundException(nameof(SampleRate), null); + } + SampleRate = (int)Util.Utils.ParseValue(nameof(SampleRate), sampleRateNode.ToString(), 0, Utils.FrequencyTable.Length - 1); + if (reverbTypeNode == null) + { + throw new BetterKeyNotFoundException(nameof(ReverbType), null); + } + ReverbType = Util.Utils.ParseEnum(nameof(ReverbType), reverbTypeNode.ToString()); + if (reverbNode == null) + { + throw new BetterKeyNotFoundException(nameof(Reverb), null); + } + Reverb = (byte)Util.Utils.ParseValue(nameof(Reverb), reverbNode.ToString(), byte.MinValue, byte.MaxValue); + if (volumeNode == null) + { + throw new BetterKeyNotFoundException(nameof(Volume), null); + } + Volume = (byte)Util.Utils.ParseValue(nameof(Volume), volumeNode.ToString(), 0, 15); + if (hasGoldenSunSynthsNode == null) + { + throw new BetterKeyNotFoundException(nameof(HasGoldenSunSynths), null); + } + HasGoldenSunSynths = Util.Utils.ParseBoolean(nameof(HasGoldenSunSynths), hasGoldenSunSynthsNode.ToString()); + if (hasPokemonCompression == null) + { + throw new BetterKeyNotFoundException(nameof(HasPokemonCompression), null); + } + HasPokemonCompression = Util.Utils.ParseBoolean(nameof(HasPokemonCompression), hasPokemonCompression.ToString()); + + // The complete playlist + if (!Playlists.Any(p => p.Name == "Music")) + { + Playlists.Insert(0, new Playlist(Strings.PlaylistMusic, Playlists.SelectMany(p => p.Songs).Distinct().OrderBy(s => s.Index))); + } + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); + } + catch (InvalidValueException ex) + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + ex.Message)); + } + catch (YamlDotNet.Core.YamlException ex) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + ex.Message)); + } + } + } + + public override string GetGameName() + { + return Name; + } + public override string GetSongName(long index) + { + Song s = GetFirstSong(index); + if (s != null) + { + return s.Name; + } + return index.ToString(); + } + + public override void Dispose() + { + Reader.Dispose(); + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Enums.cs b/VG Music Studio.backup/Core/GBA/MP2K/Enums.cs new file mode 100644 index 00000000..f22ac7e9 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/MP2K/Enums.cs @@ -0,0 +1,72 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K +{ + internal enum EnvelopeState : byte + { + Initializing, + Rising, + Decaying, + Playing, + Releasing, + Dying, + Dead + } + internal enum ReverbType : byte + { + None, + Normal, + Camelot1, + Camelot2, + MGAT + } + + internal enum GoldenSunPSGType : byte + { + Square, + Saw, + Triangle + } + internal enum LFOType : byte + { + Pitch, + Volume, + Panpot + } + internal enum SquarePattern : byte + { + D12, + D25, + D50, + D75 + } + internal enum NoisePattern : byte + { + Fine, + Rough + } + internal enum VoiceType : byte + { + PCM8, + Square1, + Square2, + PCM4, + Noise, + Invalid5, + Invalid6, + Invalid7 + } + [Flags] + internal enum VoiceFlags : byte + { + // These are flags that apply to the types + Fixed = 0x08, // PCM8 + OffWithNoise = 0x08, // Square1, Square2, PCM4, Noise + Reversed = 0x10, // PCM8 + Compressed = 0x20, // PCM8 (Only in Pokémon main series games) + + // These are flags that cancel out every other bit after them if set so they should only be checked with equality + KeySplit = 0x40, + Drum = 0x80 + } +} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs b/VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs new file mode 100644 index 00000000..738859f6 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs @@ -0,0 +1,275 @@ +using NAudio.Wave; +using System; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K +{ + internal class Mixer : Core.Mixer + { + public readonly int SampleRate; + public readonly int SamplesPerBuffer; + public readonly float SampleRateReciprocal; + private readonly float _samplesReciprocal; + public readonly float PCM8MasterVolume; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + public readonly Config Config; + private readonly WaveBuffer _audio; + private readonly float[][] _trackBuffers; + private readonly PCM8Channel[] _pcm8Channels; + private readonly SquareChannel _sq1; + private readonly SquareChannel _sq2; + private readonly PCM4Channel _pcm4; + private readonly NoiseChannel _noise; + private readonly PSGChannel[] _psgChannels; + private readonly BufferedWaveProvider _buffer; + + public Mixer(Config config) + { + Config = config; + (SampleRate, SamplesPerBuffer) = Utils.FrequencyTable[config.SampleRate]; + SampleRateReciprocal = 1f / SampleRate; + _samplesReciprocal = 1f / SamplesPerBuffer; + PCM8MasterVolume = config.Volume / 15f; + + _pcm8Channels = new PCM8Channel[24]; + for (int i = 0; i < _pcm8Channels.Length; i++) + { + _pcm8Channels[i] = new PCM8Channel(this); + } + _psgChannels = new PSGChannel[] { _sq1 = new SquareChannel(this), _sq2 = new SquareChannel(this), _pcm4 = new PCM4Channel(this), _noise = new NoiseChannel(this) }; + + int amt = SamplesPerBuffer * 2; + _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; + _trackBuffers = new float[0x10][]; + for (int i = 0; i < _trackBuffers.Length; i++) + { + _trackBuffers[i] = new float[amt]; + } + _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(SampleRate, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = SamplesPerBuffer * 64 + }; + Init(_buffer); + } + public override void Dispose() + { + base.Dispose(); + CloseWaveWriter(); + } + + public PCM8Channel AllocPCM8Channel(Track owner, ADSR env, Note note, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed, int sampleOffset) + { + PCM8Channel nChn = null; + IOrderedEnumerable byOwner = _pcm8Channels.OrderByDescending(c => c.Owner == null ? 0xFF : c.Owner.Index); + foreach (PCM8Channel i in byOwner) // Find free + { + if (i.State == EnvelopeState.Dead || i.Owner == null) + { + nChn = i; + break; + } + } + if (nChn == null) // Find releasing + { + foreach (PCM8Channel i in byOwner) + { + if (i.State == EnvelopeState.Releasing) + { + nChn = i; + break; + } + } + } + if (nChn == null) // Find prioritized + { + foreach (PCM8Channel i in byOwner) + { + if (owner.Priority > i.Owner.Priority) + { + nChn = i; + break; + } + } + } + if (nChn == null) // None available + { + PCM8Channel lowest = byOwner.First(); // Kill lowest track's instrument if the track is lower than this one + if (lowest.Owner.Index >= owner.Index) + { + nChn = lowest; + } + } + if (nChn != null) // Could still be null from the above if + { + nChn.Init(owner, note, env, sampleOffset, vol, pan, instPan, pitch, bFixed, bCompressed); + } + return nChn; + } + public PSGChannel AllocPSGChannel(Track owner, ADSR env, Note note, byte vol, sbyte pan, int instPan, int pitch, VoiceType type, object arg) + { + PSGChannel nChn; + switch (type) + { + case VoiceType.Square1: + { + nChn = _sq1; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) + { + return null; + } + _sq1.Init(owner, note, env, instPan, (SquarePattern)arg); + break; + } + case VoiceType.Square2: + { + nChn = _sq2; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) + { + return null; + } + _sq2.Init(owner, note, env, instPan, (SquarePattern)arg); + break; + } + case VoiceType.PCM4: + { + nChn = _pcm4; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) + { + return null; + } + _pcm4.Init(owner, note, env, instPan, (int)arg); + break; + } + case VoiceType.Noise: + { + nChn = _noise; + if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) + { + return null; + } + _noise.Init(owner, note, env, instPan, (NoisePattern)arg); + break; + } + default: return null; + } + nChn.SetVolume(vol, pan); + nChn.SetPitch(pitch); + return nChn; + } + + public void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBA.Utils.AGB_FPS); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + public void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBA.Utils.AGB_FPS); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + public bool IsFading() + { + return _isFading; + } + public bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + public void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + private WaveFileWriter _waveWriter; + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter?.Dispose(); + } + public void Process(bool output, bool recording) + { + for (int i = 0; i < _trackBuffers.Length; i++) + { + float[] buf = _trackBuffers[i]; + Array.Clear(buf, 0, buf.Length); + } + _audio.Clear(); + + for (int i = 0; i < _pcm8Channels.Length; i++) + { + PCM8Channel c = _pcm8Channels[i]; + if (c.Owner != null) + { + c.Process(_trackBuffers[c.Owner.Index]); + } + } + + for (int i = 0; i < _psgChannels.Length; i++) + { + PSGChannel c = _psgChannels[i]; + if (c.Owner != null) + { + c.Process(_trackBuffers[c.Owner.Index]); + } + } + + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + for (int i = 0; i < _trackBuffers.Length; i++) + { + if (!Mutes[i]) + { + float level = masterLevel; + float[] buf = _trackBuffers[i]; + for (int j = 0; j < SamplesPerBuffer; j++) + { + _audio.FloatBuffer[j * 2] += buf[j * 2] * level; + _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; + level += masterStep; + } + } + } + if (output) + { + _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + if (recording) + { + _waveWriter.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); + } + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Player.cs b/VG Music Studio.backup/Core/GBA/MP2K/Player.cs new file mode 100644 index 00000000..e00bd21a --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/MP2K/Player.cs @@ -0,0 +1,1510 @@ +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using Sanford.Multimedia.Midi; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K +{ + internal class Player : IPlayer + { + public class MIDISaveArgs + { + public bool SaveCommandsBeforeTranspose; + public bool ReverseVolume; + public List<(int AbsoluteTick, (byte Numerator, byte Denominator))> TimeSignatures; + } + + private readonly Mixer _mixer; + private readonly Config _config; + private readonly TimeBarrier _time; + private Thread _thread; + private int _voiceTableOffset = -1; + private Track[] _tracks; + private ushort _tempo; + private int _tempoStack; + private long _elapsedLoops; + + public List[] Events { get; private set; } + public long MaxTicks { get; private set; } + public long ElapsedTicks { get; private set; } + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + private int _longestTrack; + + public PlayerState State { get; private set; } + public event SongEndedEvent SongEnded; + + public Player(Mixer mixer, Config config) + { + _mixer = mixer; + _config = config; + + _time = new TimeBarrier(GBA.Utils.AGB_FPS); + } + private void CreateThread() + { + _thread = new Thread(Tick) { Name = "MP2K Player Tick" }; + _thread.Start(); + } + private void WaitThread() + { + if (_thread != null && (_thread.ThreadState == System.Threading.ThreadState.Running || _thread.ThreadState == System.Threading.ThreadState.WaitSleepJoin)) + { + _thread.Join(); + } + } + + private void InitEmulation() + { + _tempo = 150; + _tempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + _mixer.ResetFade(); + for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + { + _tracks[trackIndex].Init(); + } + } + private void SetTicks() + { + MaxTicks = 0; + bool u = false; + for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) + { + Events[trackIndex] = Events[trackIndex].OrderBy(e => e.Offset).ToList(); + List evs = Events[trackIndex]; + Track track = _tracks[trackIndex]; + track.Init(); + ElapsedTicks = 0; + while (true) + { + SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); + if (track.CallStackDepth == 0 && e.Ticks.Count > 0) + { + break; + } + else + { + e.Ticks.Add(ElapsedTicks); + ExecuteNext(track, ref u); + if (track.Stopped) + { + break; + } + else + { + ElapsedTicks += track.Rest; + track.Rest = 0; + } + } + } + if (ElapsedTicks > MaxTicks) + { + _longestTrack = trackIndex; + MaxTicks = ElapsedTicks; + } + track.StopAllChannels(); + } + } + public void LoadSong(long index) + { + if (_tracks != null) + { + for (int i = 0; i < _tracks.Length; i++) + { + _tracks[i].StopAllChannels(); + } + _tracks = null; + } + Events = null; + SongEntry entry = _config.Reader.ReadObject(_config.SongTableOffsets[0] + (index * 8)); + SongHeader header = _config.Reader.ReadObject(entry.HeaderOffset - GBA.Utils.CartridgeOffset); + int oldVoiceTableOffset = _voiceTableOffset; + _voiceTableOffset = header.VoiceTableOffset - GBA.Utils.CartridgeOffset; + if (oldVoiceTableOffset != _voiceTableOffset) + { + _voiceTypeCache = new string[byte.MaxValue + 1]; + } + _tracks = new Track[header.NumTracks]; + Events = new List[header.NumTracks]; + for (byte trackIndex = 0; trackIndex < header.NumTracks; trackIndex++) + { + int trackStart = header.TrackOffsets[trackIndex] - GBA.Utils.CartridgeOffset; + _tracks[trackIndex] = new Track(trackIndex, trackStart); + Events[trackIndex] = new List(); + bool EventExists(long offset) + { + return Events[trackIndex].Any(e => e.Offset == offset); + } + + byte runCmd = 0, prevKey = 0, prevVelocity = 0x7F; + int callStackDepth = 0; + AddEvents(trackStart); + void AddEvents(long startOffset) + { + _config.Reader.BaseStream.Position = startOffset; + bool cont = true; + while (cont) + { + long offset = _config.Reader.BaseStream.Position; + void AddEvent(ICommand command) + { + Events[trackIndex].Add(new SongEvent(offset, command)); + } + void EmulateNote(byte key, byte velocity, byte addedDuration) + { + prevKey = key; + prevVelocity = velocity; + if (!EventExists(offset)) + { + AddEvent(new NoteCommand + { + Key = key, + Velocity = velocity, + Duration = runCmd == 0xCF ? -1 : (Utils.RestTable[runCmd - 0xCF] + addedDuration) + }); + } + } + + byte cmd = _config.Reader.ReadByte(); + if (cmd >= 0xBD) // Commands that work within running status + { + runCmd = cmd; + } + + #region TIE & Notes + + if (runCmd >= 0xCF && cmd <= 0x7F) // Within running status + { + byte velocity, addedDuration; + byte[] peek = _config.Reader.PeekBytes(2); + if (peek[0] > 0x7F) + { + velocity = prevVelocity; + addedDuration = 0; + } + else if (peek[1] > 3) + { + velocity = _config.Reader.ReadByte(); + addedDuration = 0; + } + else + { + velocity = _config.Reader.ReadByte(); + addedDuration = _config.Reader.ReadByte(); + } + EmulateNote(cmd, velocity, addedDuration); + } + else if (cmd >= 0xCF) + { + byte key, velocity, addedDuration; + byte[] peek = _config.Reader.PeekBytes(3); + if (peek[0] > 0x7F) + { + key = prevKey; + velocity = prevVelocity; + addedDuration = 0; + } + else if (peek[1] > 0x7F) + { + key = _config.Reader.ReadByte(); + velocity = prevVelocity; + addedDuration = 0; + } + // TIE (0xCF) cannot have an added duration so it needs to stop here + else if (cmd == 0xCF || peek[2] > 3) + { + key = _config.Reader.ReadByte(); + velocity = _config.Reader.ReadByte(); + addedDuration = 0; + } + else + { + key = _config.Reader.ReadByte(); + velocity = _config.Reader.ReadByte(); + addedDuration = _config.Reader.ReadByte(); + } + EmulateNote(key, velocity, addedDuration); + } + + #endregion + + #region Rests + + else if (cmd >= 0x80 && cmd <= 0xB0) + { + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = Utils.RestTable[cmd - 0x80] }); + } + } + + #endregion + + #region Commands + + else if (runCmd < 0xCF && cmd <= 0x7F) + { + switch (runCmd) + { + case 0xBD: + { + if (!EventExists(offset)) + { + AddEvent(new VoiceCommand { Voice = cmd }); + } + break; + } + case 0xBE: + { + if (!EventExists(offset)) + { + AddEvent(new VolumeCommand { Volume = cmd }); + } + break; + } + case 0xBF: + { + if (!EventExists(offset)) + { + AddEvent(new PanpotCommand { Panpot = (sbyte)(cmd - 0x40) }); + } + break; + } + case 0xC0: + { + if (!EventExists(offset)) + { + AddEvent(new PitchBendCommand { Bend = (sbyte)(cmd - 0x40) }); + } + break; + } + case 0xC1: + { + if (!EventExists(offset)) + { + AddEvent(new PitchBendRangeCommand { Range = cmd }); + } + break; + } + case 0xC2: + { + if (!EventExists(offset)) + { + AddEvent(new LFOSpeedCommand { Speed = cmd }); + } + break; + } + case 0xC3: + { + if (!EventExists(offset)) + { + AddEvent(new LFODelayCommand { Delay = cmd }); + } + break; + } + case 0xC4: + { + if (!EventExists(offset)) + { + AddEvent(new LFODepthCommand { Depth = cmd }); + } + break; + } + case 0xC5: + { + if (!EventExists(offset)) + { + AddEvent(new LFOTypeCommand { Type = (LFOType)cmd }); + } + break; + } + case 0xC8: + { + if (!EventExists(offset)) + { + AddEvent(new TuneCommand { Tune = (sbyte)(cmd - 0x40) }); + } + break; + } + case 0xCD: + { + byte arg = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new LibraryCommand { Command = cmd, Argument = arg }); + } + break; + } + case 0xCE: + { + prevKey = cmd; + if (!EventExists(offset)) + { + AddEvent(new EndOfTieCommand { Key = cmd }); + } + break; + } + default: throw new Exception(string.Format(Strings.ErrorMP2KInvalidRunningStatusCommand, trackIndex, offset, runCmd)); + } + } + else if (cmd > 0xB0 && cmd < 0xCF) + { + switch (cmd) + { + case 0xB1: + case 0xB6: + { + if (!EventExists(offset)) + { + AddEvent(new FinishCommand { Prev = cmd == 0xB6 }); + } + cont = false; + break; + } + case 0xB2: + { + int jumpOffset = _config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; + if (!EventExists(offset)) + { + AddEvent(new JumpCommand { Offset = jumpOffset }); + if (!EventExists(jumpOffset)) + { + AddEvents(jumpOffset); + } + } + cont = false; + break; + } + case 0xB3: + { + int callOffset = _config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; + if (!EventExists(offset)) + { + AddEvent(new CallCommand { Offset = callOffset }); + } + if (callStackDepth < 3) + { + long backup = _config.Reader.BaseStream.Position; + callStackDepth++; + AddEvents(callOffset); + _config.Reader.BaseStream.Position = backup; + } + else + { + throw new Exception(string.Format(Strings.ErrorMP2KSDATNestedCalls, trackIndex)); + } + break; + } + case 0xB4: + { + if (!EventExists(offset)) + { + AddEvent(new ReturnCommand()); + } + if (callStackDepth != 0) + { + cont = false; + callStackDepth--; + } + break; + } + /*case 0xB5: // TODO: Logic so this isn't an infinite loop + { + byte times = config.Reader.ReadByte(); + int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; + if (!EventExists(offset)) + { + AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); + } + break; + }*/ + case 0xB9: + { + byte op = _config.Reader.ReadByte(); + byte address = _config.Reader.ReadByte(); + byte data = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new MemoryAccessCommand { Operator = op, Address = address, Data = data }); + } + break; + } + case 0xBA: + { + byte priority = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new PriorityCommand { Priority = priority }); + } + break; + } + case 0xBB: + { + byte tempoArg = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new TempoCommand { Tempo = (ushort)(tempoArg * 2) }); + } + break; + } + case 0xBC: + { + sbyte transpose = _config.Reader.ReadSByte(); + if (!EventExists(offset)) + { + AddEvent(new TransposeCommand { Transpose = transpose }); + } + break; + } + // Commands that work within running status: + case 0xBD: + { + byte voice = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new VoiceCommand { Voice = voice }); + } + break; + } + case 0xBE: + { + byte volume = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new VolumeCommand { Volume = volume }); + } + break; + } + case 0xBF: + { + byte panArg = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); + } + break; + } + case 0xC0: + { + byte bendArg = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new PitchBendCommand { Bend = (sbyte)(bendArg - 0x40) }); + } + break; + } + case 0xC1: + { + byte range = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new PitchBendRangeCommand { Range = range }); + } + break; + } + case 0xC2: + { + byte speed = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new LFOSpeedCommand { Speed = speed }); + } + break; + } + case 0xC3: + { + byte delay = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new LFODelayCommand { Delay = delay }); + } + break; + } + case 0xC4: + { + byte depth = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new LFODepthCommand { Depth = depth }); + } + break; + } + case 0xC5: + { + byte type = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new LFOTypeCommand { Type = (LFOType)type }); + } + break; + } + case 0xC8: + { + byte tuneArg = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new TuneCommand { Tune = (sbyte)(tuneArg - 0x40) }); + } + break; + } + case 0xCD: + { + byte command = _config.Reader.ReadByte(); + byte arg = _config.Reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new LibraryCommand { Command = command, Argument = arg }); + } + break; + } + case 0xCE: + { + int key = _config.Reader.PeekByte() <= 0x7F ? (prevKey = _config.Reader.ReadByte()) : -1; + if (!EventExists(offset)) + { + AddEvent(new EndOfTieCommand { Key = key }); + } + break; + } + default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, trackIndex, offset, cmd)); + } + } + + #endregion + } + } + } + SetTicks(); + } + public void SetCurrentPosition(long ticks) + { + if (Events == null) + { + SongEnded?.Invoke(); + } + else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + if (State == PlayerState.Playing) + { + Pause(); + } + InitEmulation(); + bool u = false; + while (true) + { + if (ElapsedTicks == ticks) + { + goto finish; + } + else + { + while (_tempoStack >= 150) + { + _tempoStack -= 150; + for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + { + Track track = _tracks[trackIndex]; + if (!track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track, ref u); + } + } + } + ElapsedTicks++; + if (ElapsedTicks == ticks) + { + goto finish; + } + } + _tempoStack += _tempo; + } + } + finish: + for (int i = 0; i < _tracks.Length; i++) + { + _tracks[i].StopAllChannels(); + } + Pause(); + } + } + // TODO: Don't use events, read from rom + public void SaveAsMIDI(string fileName, MIDISaveArgs args) + { + // TODO: FINE vs PREV + // TODO: https://github.com/Kermalis/VGMusicStudio/issues/36 + // TODO: Nested calls + // TODO: REPT + byte baseVolume = 0x7F; + if (args.ReverseVolume) + { + baseVolume = Events.SelectMany(e => e).Where(e => e.Command is VolumeCommand).Select(e => ((VolumeCommand)e.Command).Volume).Max(); + Debug.WriteLine($"Reversing volume back from {baseVolume}."); + } + + using (var midi = new Sequence(24) { Format = 1 }) + { + var metaTrack = new Sanford.Multimedia.Midi.Track(); + midi.Add(metaTrack); + var ts = new TimeSignatureBuilder(); + foreach ((int AbsoluteTick, (byte Numerator, byte Denominator)) e in args.TimeSignatures) + { + ts.Numerator = e.Item2.Numerator; + ts.Denominator = e.Item2.Denominator; + ts.ClocksPerMetronomeClick = 24; + ts.ThirtySecondNotesPerQuarterNote = 8; + ts.Build(); + metaTrack.Insert(e.AbsoluteTick, ts.Result); + } + + for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) + { + var track = new Sanford.Multimedia.Midi.Track(); + midi.Add(track); + + bool foundTranspose = false; + int endOfPattern = 0; + long startOfPatternTicks = 0, endOfPatternTicks = 0; + sbyte transpose = 0; + var playing = new List(); + for (int i = 0; i < Events[trackIndex].Count; i++) + { + SongEvent e = Events[trackIndex][i]; + int ticks = (int)(e.Ticks[0] + (endOfPatternTicks - startOfPatternTicks)); + + // Preliminary check for saving events before transpose + switch (e.Command) + { + case TransposeCommand keysh: foundTranspose = true; break; + default: // If we should not save before transpose then skip this event + { + if (!args.SaveCommandsBeforeTranspose && !foundTranspose) + { + continue; + } + break; + } + } + // Now do the event magic... + switch (e.Command) + { + case CallCommand patt: + { + int callCmd = Events[trackIndex].FindIndex(c => c.Offset == patt.Offset); + endOfPattern = i; + endOfPatternTicks = e.Ticks[0]; + i = callCmd - 1; // -1 for incoming ++ + startOfPatternTicks = Events[trackIndex][callCmd].Ticks[0]; + break; + } + case EndOfTieCommand eot: + { + NoteCommand nc = eot.Key == -1 ? playing.LastOrDefault() : playing.LastOrDefault(no => no.Key == eot.Key); + if (nc != null) + { + int key = nc.Key + transpose; + if (key < 0) + { + key = 0; + } + else if (key > 0x7F) + { + key = 0x7F; + } + track.Insert(ticks, new ChannelMessage(ChannelCommand.NoteOff, trackIndex, key)); + playing.Remove(nc); + } + break; + } + case FinishCommand _: + { + // If the track is not only the finish command, place the finish command at the correct tick + if (track.Count > 1) + { + track.EndOfTrackOffset = (int)(e.Ticks[0] - track.GetMidiEvent(track.Count - 2).AbsoluteTicks); + } + goto endOfTrack; + } + case JumpCommand goTo: + { + if (trackIndex == 0) + { + int jumpCmd = Events[trackIndex].FindIndex(c => c.Offset == goTo.Offset); + metaTrack.Insert((int)Events[trackIndex][jumpCmd].Ticks[0], new MetaMessage(MetaType.Marker, new byte[] { (byte)'[' })); + metaTrack.Insert(ticks, new MetaMessage(MetaType.Marker, new byte[] { (byte)']' })); + } + break; + } + case LFODelayCommand lfodl: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 26, lfodl.Delay)); + break; + } + case LFODepthCommand mod: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.ModulationWheel, mod.Depth)); + break; + } + case LFOSpeedCommand lfos: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 21, lfos.Speed)); + break; + } + case LFOTypeCommand modt: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 22, (byte)modt.Type)); + break; + } + case LibraryCommand xcmd: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 30, xcmd.Command)); + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 29, xcmd.Argument)); + break; + } + case MemoryAccessCommand memacc: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 13, memacc.Operator)); + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 14, memacc.Address)); + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 12, memacc.Data)); + break; + } + case NoteCommand note: + { + int key = note.Key + transpose; + if (key < 0) + { + key = 0; + } + else if (key > 0x7F) + { + key = 0x7F; + } + track.Insert(ticks, new ChannelMessage(ChannelCommand.NoteOn, trackIndex, key, note.Velocity)); + if (note.Duration != -1) + { + track.Insert(ticks + note.Duration, new ChannelMessage(ChannelCommand.NoteOff, trackIndex, key)); + } + else + { + playing.Add(note); + } + break; + } + case PanpotCommand pan: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.Pan, pan.Panpot + 0x40)); + break; + } + case PitchBendCommand bend: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.PitchWheel, trackIndex, 0, bend.Bend + 0x40)); + break; + } + case PitchBendRangeCommand bendr: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 20, bendr.Range)); + break; + } + case PriorityCommand prio: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.VolumeFine, prio.Priority)); + break; + } + case ReturnCommand _: + { + if (endOfPattern != 0) + { + i = endOfPattern; + endOfPattern = 0; + startOfPatternTicks = endOfPatternTicks = 0; + } + break; + } + case TempoCommand tempo: + { + var change = new TempoChangeBuilder { Tempo = 60000000 / tempo.Tempo }; + change.Build(); + metaTrack.Insert(ticks, change.Result); + break; + } + case TransposeCommand keysh: + { + transpose = keysh.Transpose; + break; + } + case TuneCommand tune: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 24, tune.Tune)); + break; + } + case VoiceCommand voice: + { + track.Insert(ticks, new ChannelMessage(ChannelCommand.ProgramChange, trackIndex, voice.Voice)); + break; + } + case VolumeCommand vol: + { + double d = baseVolume / (double)0x7F; + int volume = (int)(vol.Volume / d); + // If there are rounding errors, fix them (happens if baseVolume is not 127 and baseVolume is not vol.Volume) + if (volume * baseVolume / 0x7F == vol.Volume - 1) + { + volume++; + } + track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.Volume, volume)); + break; + } + } + } + endOfTrack:; + } + midi.Save(fileName); + } + } + public void Play() + { + if (Events == null) + { + SongEnded?.Invoke(); + } + else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + Stop(); + InitEmulation(); + State = PlayerState.Playing; + CreateThread(); + } + } + public void Pause() + { + if (State == PlayerState.Playing) + { + State = PlayerState.Paused; + WaitThread(); + } + else if (State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.Playing; + CreateThread(); + } + } + public void Stop() + { + if (State == PlayerState.Playing || State == PlayerState.Paused) + { + State = PlayerState.Stopped; + WaitThread(); + } + } + public void Record(string fileName) + { + _mixer.CreateWaveWriter(fileName); + InitEmulation(); + State = PlayerState.Recording; + CreateThread(); + WaitThread(); + _mixer.CloseWaveWriter(); + } + public void Dispose() + { + if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.ShutDown; + WaitThread(); + } + } + private string[] _voiceTypeCache; + public void GetSongState(UI.SongInfoControl.SongInfo info) + { + info.Tempo = _tempo; + for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + { + Track track = _tracks[trackIndex]; + UI.SongInfoControl.SongInfo.Track tin = info.Tracks[trackIndex]; + tin.Position = track.DataOffset; + tin.Rest = track.Rest; + tin.Voice = track.Voice; + tin.LFO = track.LFODepth; + if (_voiceTypeCache[track.Voice] == null) + { + byte t = _config.ROM[_voiceTableOffset + (track.Voice * 0xC)]; + if (t == (byte)VoiceFlags.KeySplit) + { + _voiceTypeCache[track.Voice] = "Key Split"; + } + else if (t == (byte)VoiceFlags.Drum) + { + _voiceTypeCache[track.Voice] = "Drum"; + } + else + { + switch ((VoiceType)(t & 0x7)) + { + case VoiceType.PCM8: _voiceTypeCache[track.Voice] = "PCM8"; break; + case VoiceType.Square1: _voiceTypeCache[track.Voice] = "Square 1"; break; + case VoiceType.Square2: _voiceTypeCache[track.Voice] = "Square 2"; break; + case VoiceType.PCM4: _voiceTypeCache[track.Voice] = "PCM4"; break; + case VoiceType.Noise: _voiceTypeCache[track.Voice] = "Noise"; break; + case VoiceType.Invalid5: _voiceTypeCache[track.Voice] = "Invalid 5"; break; + case VoiceType.Invalid6: _voiceTypeCache[track.Voice] = "Invalid 6"; break; + case VoiceType.Invalid7: _voiceTypeCache[track.Voice] = "Invalid 7"; break; + } + } + } + tin.Type = _voiceTypeCache[track.Voice]; + tin.Volume = track.GetVolume(); + tin.PitchBend = track.GetPitch(); + tin.Panpot = track.GetPanpot(); + + Channel[] channels = track.Channels.ToArray(); + if (channels.Length == 0) + { + tin.Keys[0] = byte.MaxValue; + tin.LeftVolume = 0f; + tin.RightVolume = 0f; + } + else + { + int numKeys = 0; + float left = 0f; + float right = 0f; + for (int j = 0; j < channels.Length; j++) + { + Channel c = channels[j]; + if (c.State < EnvelopeState.Releasing) + { + tin.Keys[numKeys++] = c.Note.OriginalKey; + } + ChannelVolume vol = c.GetVolume(); + if (vol.LeftVol > left) + { + left = vol.LeftVol; + } + if (vol.RightVol > right) + { + right = vol.RightVol; + } + } + tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array + tin.LeftVolume = left; + tin.RightVolume = right; + } + } + } + + // TODO: Don't use config.Reader (Or make ReadObjectCached(offset)) + private void PlayNote(Track track, byte key, byte velocity, byte addedDuration) + { + int k = key + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + key = (byte)k; + track.PrevKey = key; + track.PrevVelocity = velocity; + if (track.Ready) + { + bool fromDrum = false; + int offset = _voiceTableOffset + (track.Voice * 12); + while (true) + { + VoiceEntry v = _config.Reader.ReadObject(offset); + if (v.Type == (int)VoiceFlags.KeySplit) + { + fromDrum = false; // In case there is a multi within a drum + byte inst = _config.Reader.ReadByte(v.Int8 - GBA.Utils.CartridgeOffset + key); + offset = v.Int4 - GBA.Utils.CartridgeOffset + (inst * 12); + } + else if (v.Type == (int)VoiceFlags.Drum) + { + fromDrum = true; + offset = v.Int4 - GBA.Utils.CartridgeOffset + (key * 12); + } + else + { + var note = new Note + { + Duration = track.RunCmd == 0xCF ? -1 : (Utils.RestTable[track.RunCmd - 0xCF] + addedDuration), + Velocity = velocity, + OriginalKey = key, + Key = fromDrum ? v.RootKey : key + }; + var type = (VoiceType)(v.Type & 0x7); + int instPan = v.Pan; + instPan = (instPan & 0x80) != 0 ? instPan - 0xC0 : 0; + switch (type) + { + case VoiceType.PCM8: + { + bool bFixed = (v.Type & (int)VoiceFlags.Fixed) != 0; + bool bCompressed = _config.HasPokemonCompression && ((v.Type & (int)VoiceFlags.Compressed) != 0); + _mixer.AllocPCM8Channel(track, v.ADSR, note, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + bFixed, bCompressed, v.Int4 - GBA.Utils.CartridgeOffset); + return; + } + case VoiceType.Square1: + case VoiceType.Square2: + { + _mixer.AllocPSGChannel(track, v.ADSR, note, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, (SquarePattern)v.Int4); + return; + } + case VoiceType.PCM4: + { + _mixer.AllocPSGChannel(track, v.ADSR, note, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, v.Int4 - GBA.Utils.CartridgeOffset); + return; + } + case VoiceType.Noise: + { + _mixer.AllocPSGChannel(track, v.ADSR, note, + track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), + type, (NoisePattern)v.Int4); + return; + } + } + } + } + } + } + private void ExecuteNext(Track track, ref bool update) + { + byte cmd = _config.ROM[track.DataOffset++]; + if (cmd >= 0xBD) // Commands that work within running status + { + track.RunCmd = cmd; + } + if (track.RunCmd >= 0xCF && cmd <= 0x7F) // Within running status + { + byte peek0 = _config.ROM[track.DataOffset]; + byte peek1 = _config.ROM[track.DataOffset + 1]; + byte velocity, addedDuration; + if (peek0 > 0x7F) + { + velocity = track.PrevVelocity; + addedDuration = 0; + } + else if (peek1 > 3) + { + track.DataOffset++; + velocity = peek0; + addedDuration = 0; + } + else + { + track.DataOffset += 2; + velocity = peek0; + addedDuration = peek1; + } + PlayNote(track, cmd, velocity, addedDuration); + } + else if (cmd >= 0xCF) + { + byte peek0 = _config.ROM[track.DataOffset]; + byte peek1 = _config.ROM[track.DataOffset + 1]; + byte peek2 = _config.ROM[track.DataOffset + 2]; + byte key, velocity, addedDuration; + if (peek0 > 0x7F) + { + key = track.PrevKey; + velocity = track.PrevVelocity; + addedDuration = 0; + } + else if (peek1 > 0x7F) + { + track.DataOffset++; + key = peek0; + velocity = track.PrevVelocity; + addedDuration = 0; + } + else if (cmd == 0xCF || peek2 > 3) + { + track.DataOffset += 2; + key = peek0; + velocity = peek1; + addedDuration = 0; + } + else + { + track.DataOffset += 3; + key = peek0; + velocity = peek1; + addedDuration = peek2; + } + PlayNote(track, key, velocity, addedDuration); + } + else if (cmd >= 0x80 && cmd <= 0xB0) + { + track.Rest = Utils.RestTable[cmd - 0x80]; + } + else if (track.RunCmd < 0xCF && cmd <= 0x7F) + { + switch (track.RunCmd) + { + case 0xBD: + { + track.Voice = cmd; + //track.Ready = true; // This is unnecessary because if we're in running status of a voice command, then Ready was already set + break; + } + case 0xBE: + { + track.Volume = cmd; + update = true; + break; + } + case 0xBF: + { + track.Panpot = (sbyte)(cmd - 0x40); + update = true; + break; + } + case 0xC0: + { + track.PitchBend = (sbyte)(cmd - 0x40); + update = true; + break; + } + case 0xC1: + { + track.PitchBendRange = cmd; + update = true; + break; + } + case 0xC2: + { + track.LFOSpeed = cmd; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } + case 0xC3: + { + track.LFODelay = cmd; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } + case 0xC4: + { + track.LFODepth = cmd; + update = true; + break; + } + case 0xC5: + { + track.LFOType = (LFOType)cmd; + update = true; + break; + } + case 0xC8: + { + track.Tune = (sbyte)(cmd - 0x40); + update = true; + break; + } + case 0xCD: + { + track.DataOffset++; + break; + } + case 0xCE: + { + track.PrevKey = cmd; + int k = cmd + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + track.ReleaseChannels(k); + break; + } + default: throw new Exception(string.Format(Strings.ErrorMP2KInvalidRunningStatusCommand, track.Index, track.DataOffset, track.RunCmd)); + } + } + else if (cmd > 0xB0 && cmd < 0xCF) + { + switch (cmd) + { + case 0xB1: + case 0xB6: + { + track.Stopped = true; + //track.ReleaseAllTieingChannels(); // Necessary? + break; + } + case 0xB2: + { + track.DataOffset = (_config.ROM[track.DataOffset++] | (_config.ROM[track.DataOffset++] << 8) | (_config.ROM[track.DataOffset++] << 16) | (_config.ROM[track.DataOffset++] << 24)) - GBA.Utils.CartridgeOffset; + break; + } + case 0xB3: + { + if (track.CallStackDepth < 3) + { + int callOffset = (_config.ROM[track.DataOffset++] | (_config.ROM[track.DataOffset++] << 8) | (_config.ROM[track.DataOffset++] << 16) | (_config.ROM[track.DataOffset++] << 24)) - GBA.Utils.CartridgeOffset; + track.CallStack[track.CallStackDepth] = track.DataOffset; + track.CallStackDepth++; + track.DataOffset = callOffset; + } + else + { + throw new Exception(string.Format(Strings.ErrorMP2KSDATNestedCalls, track.Index)); + } + break; + } + case 0xB4: + { + if (track.CallStackDepth != 0) + { + track.CallStackDepth--; + track.DataOffset = track.CallStack[track.CallStackDepth]; + } + break; + } + /*case 0xB5: // TODO: Logic so this isn't an infinite loop + { + byte times = config.Reader.ReadByte(); + int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; + if (!EventExists(offset)) + { + AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); + } + break; + }*/ + case 0xB9: + { + track.DataOffset += 3; + break; + } + case 0xBA: + { + track.Priority = _config.ROM[track.DataOffset++]; + break; + } + case 0xBB: + { + _tempo = (ushort)(_config.ROM[track.DataOffset++] * 2); + break; + } + case 0xBC: + { + track.Transpose = (sbyte)_config.ROM[track.DataOffset++]; + break; + } + // Commands that work within running status: + case 0xBD: + { + track.Voice = _config.ROM[track.DataOffset++]; + track.Ready = true; + break; + } + case 0xBE: + { + track.Volume = _config.ROM[track.DataOffset++]; + update = true; + break; + } + case 0xBF: + { + track.Panpot = (sbyte)(_config.ROM[track.DataOffset++] - 0x40); + update = true; + break; + } + case 0xC0: + { + track.PitchBend = (sbyte)(_config.ROM[track.DataOffset++] - 0x40); + update = true; + break; + } + case 0xC1: + { + track.PitchBendRange = _config.ROM[track.DataOffset++]; + update = true; + break; + } + case 0xC2: + { + track.LFOSpeed = _config.ROM[track.DataOffset++]; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } + case 0xC3: + { + track.LFODelay = _config.ROM[track.DataOffset++]; + track.LFOPhase = 0; + track.LFODelayCount = 0; + update = true; + break; + } + case 0xC4: + { + track.LFODepth = _config.ROM[track.DataOffset++]; + update = true; + break; + } + case 0xC5: + { + track.LFOType = (LFOType)_config.ROM[track.DataOffset++]; + update = true; + break; + } + case 0xC8: + { + track.Tune = (sbyte)(_config.ROM[track.DataOffset++] - 0x40); + update = true; + break; + } + case 0xCD: + { + track.DataOffset += 2; + break; + } + case 0xCE: + { + byte peek = _config.ROM[track.DataOffset]; + if (peek > 0x7F) + { + track.ReleaseChannels(track.PrevKey); + } + else + { + track.DataOffset++; + track.PrevKey = peek; + int k = peek + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + track.ReleaseChannels(k); + } + break; + } + default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, track.Index, track.DataOffset, cmd)); + } + } + } + + private void Tick() + { + _time.Start(); + while (true) + { + PlayerState state = State; + bool playing = state == PlayerState.Playing; + bool recording = state == PlayerState.Recording; + if (!playing && !recording) + { + goto stop; + } + + void MixerProcess() + { + _mixer.Process(playing, recording); + } + + while (_tempoStack >= 150) + { + _tempoStack -= 150; + bool allDone = true; + for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + { + Track track = _tracks[trackIndex]; + track.Tick(); + bool update = false; + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track, ref update); + } + if (trackIndex == _longestTrack) + { + if (ElapsedTicks == MaxTicks) + { + if (!track.Stopped) + { + List evs = Events[trackIndex]; + for (int i = 0; i < evs.Count; i++) + { + SongEvent ev = evs[i]; + if (ev.Offset == track.DataOffset) + { + ElapsedTicks = ev.Ticks[0] - track.Rest; + break; + } + } + _elapsedLoops++; + if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) + { + _mixer.BeginFadeOut(); + } + } + } + else + { + ElapsedTicks++; + } + } + if (!track.Stopped) + { + allDone = false; + } + if (track.Channels.Count > 0) + { + allDone = false; + if (update || track.LFODepth > 0) + { + track.UpdateChannels(); + } + } + } + if (_mixer.IsFadeDone()) + { + allDone = true; + } + if (allDone) + { + MixerProcess(); + State = PlayerState.Stopped; + SongEnded?.Invoke(); + } + } + _tempoStack += _tempo; + MixerProcess(); + if (playing) + { + _time.Wait(); + } + } + stop: + _time.Stop(); + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Structs.cs b/VG Music Studio.backup/Core/GBA/MP2K/Structs.cs new file mode 100644 index 00000000..2da7d2c4 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/MP2K/Structs.cs @@ -0,0 +1,73 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K +{ + internal class SongEntry + { + public int HeaderOffset { get; set; } + public short Player { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown { get; set; } + } + internal class SongHeader + { + public byte NumTracks { get; set; } + public byte NumBlocks { get; set; } + public byte Priority { get; set; } + public byte Reverb { get; set; } + public int VoiceTableOffset { get; set; } + [BinaryArrayVariableLength(nameof(NumTracks))] + public int[] TrackOffsets { get; set; } + } + internal class VoiceEntry + { + public byte Type { get; set; } // 0 + public byte RootKey { get; set; } // 1 + public byte Unknown { get; set; } // 2 + public byte Pan { get; set; } // 3 + /// SquarePattern for Square1/Square2, NoisePattern for Noise, Address for PCM8/PCM4/KeySplit/Drum + public int Int4 { get; set; } // 4 + /// ADSR for PCM8/Square1/Square2/PCM4/Noise, KeysAddress for KeySplit + public ADSR ADSR { get; set; } // 8 + [BinaryIgnore] + public int Int8 => (ADSR.R << 24) | (ADSR.S << 16) | (ADSR.D << 8) | (ADSR.A); + } + internal struct ADSR // Used as a struct in GBChannel + { + public byte A { get; set; } + public byte D { get; set; } + public byte S { get; set; } + public byte R { get; set; } + } + internal class GoldenSunPSG + { + /// Always 0x80 + public byte Unknown { get; set; } + public GoldenSunPSGType Type { get; set; } + public byte InitialCycle { get; set; } + public byte CycleSpeed { get; set; } + public byte CycleAmplitude { get; set; } + public byte MinimumCycle { get; set; } + } + internal class SampleHeader + { + /// 0x40000000 if True + public int DoesLoop { get; set; } + /// Right shift 10 for value + public int SampleRate { get; set; } + public int LoopOffset { get; set; } + public int Length { get; set; } + } + + internal struct ChannelVolume + { + public float LeftVol, RightVol; + } + internal struct Note + { + public byte Key, OriginalKey; + public byte Velocity; + /// -1 if forever + public int Duration; + } +} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Track.cs b/VG Music Studio.backup/Core/GBA/MP2K/Track.cs new file mode 100644 index 00000000..1288008a --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/MP2K/Track.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K +{ + internal class Track + { + public readonly byte Index; + private readonly int _startOffset; + public byte Voice; + public byte PitchBendRange; + public byte Priority; + public byte Volume; + public byte Rest; + public byte LFOPhase; + public byte LFODelayCount; + public byte LFOSpeed; + public byte LFODelay; + public byte LFODepth; + public LFOType LFOType; + public sbyte PitchBend; + public sbyte Tune; + public sbyte Panpot; + public sbyte Transpose; + public bool Ready; + public bool Stopped; + public int DataOffset; + public int[] CallStack = new int[3]; + public byte CallStackDepth; + public byte RunCmd; + public byte PrevKey; + public byte PrevVelocity; + + public readonly List Channels = new List(); + + public int GetPitch() + { + int lfo = LFOType == LFOType.Pitch ? (Utils.Tri(LFOPhase) * LFODepth) >> 8 : 0; + return (PitchBend * PitchBendRange) + Tune + lfo; + } + public byte GetVolume() + { + int lfo = LFOType == LFOType.Volume ? (Utils.Tri(LFOPhase) * LFODepth * 3 * Volume) >> 19 : 0; + int v = Volume + lfo; + if (v < 0) + { + v = 0; + } + else if (v > 0x7F) + { + v = 0x7F; + } + return (byte)v; + } + public sbyte GetPanpot() + { + int lfo = LFOType == LFOType.Panpot ? (Utils.Tri(LFOPhase) * LFODepth * 3) >> 12 : 0; + int p = Panpot + lfo; + if (p < -0x40) + { + p = -0x40; + } + else if (p > 0x3F) + { + p = 0x3F; + } + return (sbyte)p; + } + + public Track(byte i, int startOffset) + { + Index = i; + _startOffset = startOffset; + } + public void Init() + { + Voice = 0; + Priority = 0; + Rest = 0; + LFODelay = 0; + LFODelayCount = 0; + LFOPhase = 0; + LFODepth = 0; + CallStackDepth = 0; + PitchBend = 0; + Tune = 0; + Panpot = 0; + Transpose = 0; + DataOffset = _startOffset; + RunCmd = 0; + PrevKey = 0; + PrevVelocity = 0x7F; + PitchBendRange = 2; + LFOType = LFOType.Pitch; + Ready = false; + Stopped = false; + LFOSpeed = 22; + Volume = 100; + StopAllChannels(); + } + public void Tick() + { + if (Rest != 0) + { + Rest--; + } + if (LFODepth > 0) + { + LFOPhase += LFOSpeed; + } + else + { + LFOPhase = 0; + } + int active = 0; + Channel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + if (chans[i].TickNote()) + { + active++; + } + } + if (active != 0) + { + if (LFODelayCount > 0) + { + LFODelayCount--; + LFOPhase = 0; + } + } + else + { + LFODelayCount = LFODelay; + } + if ((LFODelay == LFODelayCount && LFODelay != 0) || LFOSpeed == 0) + { + LFOPhase = 0; + } + } + + public void ReleaseChannels(int key) + { + Channel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + Channel c = chans[i]; + if (c.Note.OriginalKey == key && c.Note.Duration == -1) + { + c.Release(); + } + } + } + public void StopAllChannels() + { + Channel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + chans[i].Stop(); + } + } + public void UpdateChannels() + { + byte vol = GetVolume(); + sbyte pan = GetPanpot(); + int pitch = GetPitch(); + for (int i = 0; i < Channels.Count; i++) + { + Channel c = Channels[i]; + c.SetVolume(vol, pan); + c.SetPitch(pitch); + } + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Utils.cs b/VG Music Studio.backup/Core/GBA/MP2K/Utils.cs new file mode 100644 index 00000000..fc10fbba --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/MP2K/Utils.cs @@ -0,0 +1,174 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.GBA.MP2K +{ + internal static class Utils + { + public static readonly byte[] RestTable = new byte[49] + { + 00, 01, 02, 03, 04, 05, 06, 07, + 08, 09, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 28, 30, 32, 36, 40, 42, 44, + 48, 52, 54, 56, 60, 64, 66, 68, + 72, 76, 78, 80, 84, 88, 90, 92, + 96 + }; + public static readonly (int sampleRate, int samplesPerBuffer)[] FrequencyTable = new (int, int)[12] + { + (05734, 096), // 59.72916666666667 + (07884, 132), // 59.72727272727273 + (10512, 176), // 59.72727272727273 + (13379, 224), // 59.72767857142857 + (15768, 264), // 59.72727272727273 + (18157, 304), // 59.72697368421053 + (21024, 352), // 59.72727272727273 + (26758, 448), // 59.72767857142857 + (31536, 528), // 59.72727272727273 + (36314, 608), // 59.72697368421053 + (40137, 672), // 59.72767857142857 + (42048, 704) // 59.72727272727273 + }; + + // Squares + public static readonly float[] SquareD12 = new float[8] { 0.875f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f }; + public static readonly float[] SquareD25 = new float[8] { 0.750f, 0.750f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f }; + public static readonly float[] SquareD50 = new float[8] { 0.500f, 0.500f, 0.500f, 0.500f, -0.500f, -0.500f, -0.500f, -0.500f }; + public static readonly float[] SquareD75 = new float[8] { 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, -0.750f, -0.750f }; + + // Noises + public static readonly BitArray NoiseFine; + public static readonly BitArray NoiseRough; + public static readonly byte[] NoiseFrequencyTable = new byte[60] + { + 0xD7, 0xD6, 0xD5, 0xD4, + 0xC7, 0xC6, 0xC5, 0xC4, + 0xB7, 0xB6, 0xB5, 0xB4, + 0xA7, 0xA6, 0xA5, 0xA4, + 0x97, 0x96, 0x95, 0x94, + 0x87, 0x86, 0x85, 0x84, + 0x77, 0x76, 0x75, 0x74, + 0x67, 0x66, 0x65, 0x64, + 0x57, 0x56, 0x55, 0x54, + 0x47, 0x46, 0x45, 0x44, + 0x37, 0x36, 0x35, 0x34, + 0x27, 0x26, 0x25, 0x24, + 0x17, 0x16, 0x15, 0x14, + 0x07, 0x06, 0x05, 0x04, + 0x03, 0x02, 0x01, 0x00 + }; + + // PCM4 + public static float[] PCM4ToFloat(int sampleOffset) + { + var config = (Config)Engine.Instance.Config; + float[] sample = new float[0x20]; + float sum = 0; + for (int i = 0; i < 0x10; i++) + { + byte b = config.ROM[sampleOffset + i]; + float first = (b >> 4) / 16f; + float second = (b & 0xF) / 16f; + sum += sample[i * 2] = first; + sum += sample[(i * 2) + 1] = second; + } + float dcCorrection = sum / 0x20; + for (int i = 0; i < 0x20; i++) + { + sample[i] -= dcCorrection; + } + return sample; + } + + // Pokémon Only + private static readonly sbyte[] _compressionLookup = new sbyte[16] + { + 0, 1, 4, 9, 16, 25, 36, 49, -64, -49, -36, -25, -16, -9, -4, -1 + }; + public static sbyte[] Decompress(int sampleOffset, int sampleLength) + { + var config = (Config)Engine.Instance.Config; + var samples = new List(); + sbyte compressionLevel = 0; + int compressionByte = 0, compressionIdx = 0; + + for (int i = 0; true; i++) + { + byte b = config.ROM[sampleOffset + i]; + if (compressionByte == 0) + { + compressionByte = 0x20; + compressionLevel = (sbyte)b; + samples.Add(compressionLevel); + if (++compressionIdx >= sampleLength) + { + break; + } + } + else + { + if (compressionByte < 0x20) + { + compressionLevel += _compressionLookup[b >> 4]; + samples.Add(compressionLevel); + if (++compressionIdx >= sampleLength) + { + break; + } + } + compressionByte--; + compressionLevel += _compressionLookup[b & 0xF]; + samples.Add(compressionLevel); + if (++compressionIdx >= sampleLength) + { + break; + } + } + } + + return samples.ToArray(); + } + + static Utils() + { + NoiseFine = new BitArray(0x8000); + int reg = 0x4000; + for (int i = 0; i < NoiseFine.Length; i++) + { + if ((reg & 1) == 1) + { + reg >>= 1; + reg ^= 0x6000; + NoiseFine[i] = true; + } + else + { + reg >>= 1; + NoiseFine[i] = false; + } + } + NoiseRough = new BitArray(0x80); + reg = 0x40; + for (int i = 0; i < NoiseRough.Length; i++) + { + if ((reg & 1) == 1) + { + reg >>= 1; + reg ^= 0x60; + NoiseRough[i] = true; + } + else + { + reg >>= 1; + NoiseRough[i] = false; + } + } + } + public static int Tri(int index) + { + index = (index - 64) & 0xFF; + return (index < 128) ? (index * 12) - 768 : 2304 - (index * 12); + } + } +} diff --git a/VG Music Studio.backup/Core/GBA/Utils.cs b/VG Music Studio.backup/Core/GBA/Utils.cs new file mode 100644 index 00000000..d5858818 --- /dev/null +++ b/VG Music Studio.backup/Core/GBA/Utils.cs @@ -0,0 +1,13 @@ +namespace Kermalis.VGMusicStudio.Core.GBA +{ + internal static class Utils + { + public const double AGB_FPS = 59.7275; + public const int SystemClock = 16777216; // 16.777216 MHz (16*1024*1024 Hz) + + public const int CartridgeOffset = 0x08000000; + public const int CartridgeCapacity = 0x02000000; + + public static readonly string[] PSGTypes = new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; + } +} diff --git a/VG Music Studio.backup/Core/GlobalConfig.cs b/VG Music Studio.backup/Core/GlobalConfig.cs new file mode 100644 index 00000000..4b3b57f7 --- /dev/null +++ b/VG Music Studio.backup/Core/GlobalConfig.cs @@ -0,0 +1,109 @@ +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.IO; +using YamlDotNet.RepresentationModel; + +namespace Kermalis.VGMusicStudio.Core +{ + internal enum PlaylistMode : byte + { + Random, + Sequential + } + + internal sealed class GlobalConfig + { + public static GlobalConfig Instance { get; private set; } + + public readonly bool TaskbarProgress; + public readonly ushort RefreshRate; + public readonly bool CenterIndicators; + public readonly bool PanpotIndicators; + public readonly PlaylistMode PlaylistMode; + public readonly long PlaylistSongLoops; + public readonly long PlaylistFadeOutMilliseconds; + public readonly sbyte MiddleCOctave; + public readonly HSLColor[] Colors; + + private GlobalConfig() + { + const string configFile = "Config.yaml"; + using (StreamReader fileStream = File.OpenText(Utils.CombineWithBaseDirectory(configFile))) + { + try + { + var yaml = new YamlStream(); + yaml.Load(fileStream); + + var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; + TaskbarProgress = mapping.GetValidBoolean(nameof(TaskbarProgress)); + RefreshRate = (ushort)mapping.GetValidValue(nameof(RefreshRate), 1, 1000); + CenterIndicators = mapping.GetValidBoolean(nameof(CenterIndicators)); + PanpotIndicators = mapping.GetValidBoolean(nameof(PanpotIndicators)); + PlaylistMode = mapping.GetValidEnum(nameof(PlaylistMode)); + PlaylistSongLoops = mapping.GetValidValue(nameof(PlaylistSongLoops), 0, long.MaxValue); + PlaylistFadeOutMilliseconds = mapping.GetValidValue(nameof(PlaylistFadeOutMilliseconds), 0, long.MaxValue); + MiddleCOctave = (sbyte)mapping.GetValidValue(nameof(MiddleCOctave), sbyte.MinValue, sbyte.MaxValue); + + var cmap = (YamlMappingNode)mapping.Children[nameof(Colors)]; + Colors = new HSLColor[256]; + foreach (KeyValuePair c in cmap) + { + int i = (int)Utils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Colors)), c.Key.ToString(), 0, 127); + if (Colors[i] != null) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigColorRepeated, i))); + } + double h = 0, s = 0, l = 0; + foreach (KeyValuePair v in ((YamlMappingNode)c.Value).Children) + { + string key = v.Key.ToString(); + string valueName = string.Format(Strings.ConfigKeySubkey, string.Format("{0} {1}", nameof(Colors), i)); + if (key == "H") + { + h = Utils.ParseValue(valueName, v.Value.ToString(), 0, 240); + } + else if (key == "S") + { + s = Utils.ParseValue(valueName, v.Value.ToString(), 0, 240); + } + else if (key == "L") + { + l = Utils.ParseValue(valueName, v.Value.ToString(), 0, 240); + } + else + { + throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigColorInvalidKey, i))); + } + } + var co = new HSLColor(h, s, l); + Colors[i] = co; + Colors[i + 128] = co; + } + for (int i = 0; i < Colors.Length; i++) + { + if (Colors[i] == null) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigColorMissing, i))); + } + } + } + catch (BetterKeyNotFoundException ex) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); + } + catch (Exception ex) when (ex is InvalidValueException || ex is YamlDotNet.Core.YamlException) + { + throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + ex.Message)); + } + } + } + + public static void Init() + { + Instance = new GlobalConfig(); + } + } +} diff --git a/VG Music Studio.backup/Core/Mixer.cs b/VG Music Studio.backup/Core/Mixer.cs new file mode 100644 index 00000000..e0a242f6 --- /dev/null +++ b/VG Music Studio.backup/Core/Mixer.cs @@ -0,0 +1,87 @@ +using Kermalis.VGMusicStudio.UI; +using NAudio.CoreAudioApi; +using NAudio.CoreAudioApi.Interfaces; +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core +{ + internal abstract class Mixer : IAudioSessionEventsHandler, IDisposable + { + public readonly bool[] Mutes = new bool[SongInfoControl.SongInfo.MaxTracks]; + private IWavePlayer _out; + private AudioSessionControl _appVolume; + + protected void Init(IWaveProvider waveProvider) + { + _out = new WasapiOut(); + _out.Init(waveProvider); + using (var en = new MMDeviceEnumerator()) + { + SessionCollection sessions = en.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia).AudioSessionManager.Sessions; + int id = System.Diagnostics.Process.GetCurrentProcess().Id; + for (int i = 0; i < sessions.Count; i++) + { + AudioSessionControl session = sessions[i]; + if (session.GetProcessID == id) + { + _appVolume = session; + _appVolume.RegisterEventClient(this); + break; + } + } + } + _out.Play(); + } + + private bool _volChange = true; + public void OnVolumeChanged(float volume, bool isMuted) + { + if (_volChange) + { + MainForm.Instance.SetVolumeBarValue(volume); + } + _volChange = true; + } + public void OnDisplayNameChanged(string displayName) + { + throw new NotImplementedException(); + } + public void OnIconPathChanged(string iconPath) + { + throw new NotImplementedException(); + } + public void OnChannelVolumeChanged(uint channelCount, IntPtr newVolumes, uint channelIndex) + { + throw new NotImplementedException(); + } + public void OnGroupingParamChanged(ref Guid groupingId) + { + throw new NotImplementedException(); + } + // Fires on @out.Play() and @out.Stop() + public void OnStateChanged(AudioSessionState state) + { + if (state == AudioSessionState.AudioSessionStateActive) + { + OnVolumeChanged(_appVolume.SimpleAudioVolume.Volume, _appVolume.SimpleAudioVolume.Mute); + } + } + public void OnSessionDisconnected(AudioSessionDisconnectReason disconnectReason) + { + throw new NotImplementedException(); + } + public void SetVolume(float volume) + { + _volChange = false; + _appVolume.SimpleAudioVolume.Volume = volume; + } + + public virtual void Dispose() + { + _out.Stop(); + _out.Dispose(); + _appVolume.Dispose(); + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Channel.cs b/VG Music Studio.backup/Core/NDS/DSE/Channel.cs new file mode 100644 index 00000000..2ff239db --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/Channel.cs @@ -0,0 +1,368 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal class Channel + { + public readonly byte Index; + + public Track Owner; + public EnvelopeState State; + public byte RootKey; + public byte Key; + public byte NoteVelocity; + public sbyte Panpot; // Not necessary + public ushort BaseTimer; + public ushort Timer; + public uint NoteLength; + public byte Volume; + + private int _pos; + private short _prevLeft; + private short _prevRight; + + private int _envelopeTimeLeft; + private int _volumeIncrement; + private int _velocity; // From 0-0x3FFFFFFF ((128 << 23) - 1) + private byte _targetVolume; + + private byte _attackVolume; + private byte _attack; + private byte _decay; + private byte _sustain; + private byte _hold; + private byte _decay2; + private byte _release; + + // PCM8, PCM16, ADPCM + private SWD.SampleBlock _sample; + // PCM8, PCM16 + private int _dataOffset; + // ADPCM + private ADPCMDecoder _adpcmDecoder; + private short _adpcmLoopLastSample; + private short _adpcmLoopStepIndex; + + public Channel(byte i) + { + Index = i; + } + + public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint noteLength) + { + SWD.IProgramInfo programInfo = localswd.Programs.ProgramInfos[voice]; + if (programInfo != null) + { + for (int i = 0; i < programInfo.SplitEntries.Length; i++) + { + SWD.ISplitEntry split = programInfo.SplitEntries[i]; + if (key >= split.LowKey && key <= split.HighKey) + { + _sample = masterswd.Samples[split.SampleId]; + Key = (byte)key; + RootKey = split.SampleRootKey; + BaseTimer = (ushort)(NDS.Utils.ARM7_CLOCK / _sample.WavInfo.SampleRate); + if (_sample.WavInfo.SampleFormat == SampleFormat.ADPCM) + { + _adpcmDecoder = new ADPCMDecoder(_sample.Data); + } + //attackVolume = sample.WavInfo.AttackVolume == 0 ? split.AttackVolume : sample.WavInfo.AttackVolume; + //attack = sample.WavInfo.Attack == 0 ? split.Attack : sample.WavInfo.Attack; + //decay = sample.WavInfo.Decay == 0 ? split.Decay : sample.WavInfo.Decay; + //sustain = sample.WavInfo.Sustain == 0 ? split.Sustain : sample.WavInfo.Sustain; + //hold = sample.WavInfo.Hold == 0 ? split.Hold : sample.WavInfo.Hold; + //decay2 = sample.WavInfo.Decay2 == 0 ? split.Decay2 : sample.WavInfo.Decay2; + //release = sample.WavInfo.Release == 0 ? split.Release : sample.WavInfo.Release; + //attackVolume = split.AttackVolume == 0 ? sample.WavInfo.AttackVolume : split.AttackVolume; + //attack = split.Attack == 0 ? sample.WavInfo.Attack : split.Attack; + //decay = split.Decay == 0 ? sample.WavInfo.Decay : split.Decay; + //sustain = split.Sustain == 0 ? sample.WavInfo.Sustain : split.Sustain; + //hold = split.Hold == 0 ? sample.WavInfo.Hold : split.Hold; + //decay2 = split.Decay2 == 0 ? sample.WavInfo.Decay2 : split.Decay2; + //release = split.Release == 0 ? sample.WavInfo.Release : split.Release; + _attackVolume = split.AttackVolume == 0 ? _sample.WavInfo.AttackVolume == 0 ? (byte)0x7F : _sample.WavInfo.AttackVolume : split.AttackVolume; + _attack = split.Attack == 0 ? _sample.WavInfo.Attack == 0 ? (byte)0x7F : _sample.WavInfo.Attack : split.Attack; + _decay = split.Decay == 0 ? _sample.WavInfo.Decay == 0 ? (byte)0x7F : _sample.WavInfo.Decay : split.Decay; + _sustain = split.Sustain == 0 ? _sample.WavInfo.Sustain == 0 ? (byte)0x7F : _sample.WavInfo.Sustain : split.Sustain; + _hold = split.Hold == 0 ? _sample.WavInfo.Hold == 0 ? (byte)0x7F : _sample.WavInfo.Hold : split.Hold; + _decay2 = split.Decay2 == 0 ? _sample.WavInfo.Decay2 == 0 ? (byte)0x7F : _sample.WavInfo.Decay2 : split.Decay2; + _release = split.Release == 0 ? _sample.WavInfo.Release == 0 ? (byte)0x7F : _sample.WavInfo.Release : split.Release; + DetermineEnvelopeStartingPoint(); + _pos = 0; + _prevLeft = _prevRight = 0; + NoteLength = noteLength; + return true; + } + } + } + return false; + } + + public void Stop() + { + if (Owner != null) + { + Owner.Channels.Remove(this); + } + Owner = null; + Volume = 0; + } + + private bool CMDB1___sub_2074CA0() + { + bool b = true; + bool ge = _sample.WavInfo.EnvMult >= 0x7F; + bool ee = _sample.WavInfo.EnvMult == 0x7F; + if (_sample.WavInfo.EnvMult > 0x7F) + { + ge = _attackVolume >= 0x7F; + ee = _attackVolume == 0x7F; + } + if (!ee & ge + && _attack > 0x7F + && _decay > 0x7F + && _sustain > 0x7F + && _hold > 0x7F + && _decay2 > 0x7F + && _release > 0x7F) + { + b = false; + } + return b; + } + private void DetermineEnvelopeStartingPoint() + { + State = EnvelopeState.Two; // This isn't actually placed in this func + bool atLeastOneThingIsValid = CMDB1___sub_2074CA0(); // Neither is this + if (atLeastOneThingIsValid) + { + if (_attack != 0) + { + _velocity = _attackVolume << 23; + State = EnvelopeState.Hold; + UpdateEnvelopePlan(0x7F, _attack); + } + else + { + _velocity = 0x7F << 23; + if (_hold != 0) + { + UpdateEnvelopePlan(0x7F, _hold); + State = EnvelopeState.Decay; + } + else if (_decay != 0) + { + UpdateEnvelopePlan(_sustain, _decay); + State = EnvelopeState.Decay2; + } + else + { + UpdateEnvelopePlan(0, _release); + State = EnvelopeState.Six; + } + } + // Unk1E = 1 + } + else if (State != EnvelopeState.One) // What should it be? + { + State = EnvelopeState.Zero; + _velocity = 0x7F << 23; + } + } + public void SetEnvelopePhase7_2074ED8() + { + if (State != EnvelopeState.Zero) + { + UpdateEnvelopePlan(0, _release); + State = EnvelopeState.Seven; + } + } + public int StepEnvelope() + { + if (State > EnvelopeState.Two) + { + if (_envelopeTimeLeft != 0) + { + _envelopeTimeLeft--; + _velocity += _volumeIncrement; + if (_velocity < 0) + { + _velocity = 0; + } + else if (_velocity > 0x3FFFFFFF) + { + _velocity = 0x3FFFFFFF; + } + } + else + { + _velocity = _targetVolume << 23; + switch (State) + { + default: return _velocity >> 23; // case 8 + case EnvelopeState.Hold: + { + if (_hold == 0) + { + goto LABEL_6; + } + else + { + UpdateEnvelopePlan(0x7F, _hold); + State = EnvelopeState.Decay; + } + break; + } + case EnvelopeState.Decay: + LABEL_6: + { + if (_decay == 0) + { + _velocity = _sustain << 23; + goto LABEL_9; + } + else + { + UpdateEnvelopePlan(_sustain, _decay); + State = EnvelopeState.Decay2; + } + break; + } + case EnvelopeState.Decay2: + LABEL_9: + { + if (_decay2 == 0) + { + goto LABEL_11; + } + else + { + UpdateEnvelopePlan(0, _decay2); + State = EnvelopeState.Six; + } + break; + } + case EnvelopeState.Six: + LABEL_11: + { + UpdateEnvelopePlan(0, 0); + State = EnvelopeState.Two; + break; + } + case EnvelopeState.Seven: + { + State = EnvelopeState.Eight; + _velocity = 0; + _envelopeTimeLeft = 0; + break; + } + } + } + } + return _velocity >> 23; + } + private void UpdateEnvelopePlan(byte targetVolume, int envelopeParam) + { + if (envelopeParam == 0x7F) + { + _volumeIncrement = 0; + _envelopeTimeLeft = int.MaxValue; + } + else + { + _targetVolume = targetVolume; + _envelopeTimeLeft = _sample.WavInfo.EnvMult == 0 + ? Utils.Duration32[envelopeParam] * 1000 / 10000 + : Utils.Duration16[envelopeParam] * _sample.WavInfo.EnvMult * 1000 / 10000; + _volumeIncrement = _envelopeTimeLeft == 0 ? 0 : ((targetVolume << 23) - _velocity) / _envelopeTimeLeft; + } + } + + public void Process(out short left, out short right) + { + if (Timer != 0) + { + int numSamples = (_pos + 0x100) / Timer; + _pos = (_pos + 0x100) % Timer; + // prevLeft and prevRight are stored because numSamples can be 0. + for (int i = 0; i < numSamples; i++) + { + short samp; + switch (_sample.WavInfo.SampleFormat) + { + case SampleFormat.PCM8: + { + // If hit end + if (_dataOffset >= _sample.Data.Length) + { + if (_sample.WavInfo.Loop) + { + _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = (short)((sbyte)_sample.Data[_dataOffset++] << 8); + break; + } + case SampleFormat.PCM16: + { + // If hit end + if (_dataOffset >= _sample.Data.Length) + { + if (_sample.WavInfo.Loop) + { + _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = (short)(_sample.Data[_dataOffset++] | (_sample.Data[_dataOffset++] << 8)); + break; + } + case SampleFormat.ADPCM: + { + // If just looped + if (_adpcmDecoder.DataOffset == _sample.WavInfo.LoopStart * 4 && !_adpcmDecoder.OnSecondNibble) + { + _adpcmLoopLastSample = _adpcmDecoder.LastSample; + _adpcmLoopStepIndex = _adpcmDecoder.StepIndex; + } + // If hit end + if (_adpcmDecoder.DataOffset >= _sample.Data.Length && !_adpcmDecoder.OnSecondNibble) + { + if (_sample.WavInfo.Loop) + { + _adpcmDecoder.DataOffset = (int)(_sample.WavInfo.LoopStart * 4); + _adpcmDecoder.StepIndex = _adpcmLoopStepIndex; + _adpcmDecoder.LastSample = _adpcmLoopLastSample; + _adpcmDecoder.OnSecondNibble = false; + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = _adpcmDecoder.GetSample(); + break; + } + default: samp = 0; break; + } + samp = (short)(samp * Volume / 0x7F); + _prevLeft = (short)(samp * (-Panpot + 0x40) / 0x80); + _prevRight = (short)(samp * (Panpot + 0x40) / 0x80); + } + } + left = _prevLeft; + right = _prevRight; + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Commands.cs b/VG Music Studio.backup/Core/NDS/DSE/Commands.cs new file mode 100644 index 00000000..a7d8eaa3 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/Commands.cs @@ -0,0 +1,130 @@ +using System.Drawing; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal class ExpressionCommand : ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Expression"; + public string Arguments => Expression.ToString(); + + public byte Expression { get; set; } + } + internal class FinishCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Finish"; + public string Arguments => string.Empty; + } + internal class InvalidCommand : ICommand + { + public Color Color => Color.MediumVioletRed; + public string Label => $"Invalid 0x{Command:X}"; + public string Arguments => string.Empty; + + public byte Command { get; set; } + } + internal class LoopStartCommand : ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Loop Start"; + public string Arguments => $"0x{Offset:X}"; + + public long Offset { get; set; } + } + internal class NoteCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{Util.Utils.GetPianoKeyName(Key)} {OctaveChange} {Velocity} {Duration}"; + + public byte Key { get; set; } + public sbyte OctaveChange { get; set; } + public byte Velocity { get; set; } + public uint Duration { get; set; } + } + internal class OctaveAddCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Add To Octave"; + public string Arguments => OctaveChange.ToString(); + + public sbyte OctaveChange { get; set; } + } + internal class OctaveSetCommand : ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Set Octave"; + public string Arguments => Octave.ToString(); + + public byte Octave { get; set; } + } + internal class PanpotCommand : ICommand + { + public Color Color => Color.GreenYellow; + public string Label => "Panpot"; + public string Arguments => Panpot.ToString(); + + public sbyte Panpot { get; set; } + } + internal class PitchBendCommand : ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend"; + public string Arguments => $"{(sbyte)Bend}, {(sbyte)(Bend >> 8)}"; + + public ushort Bend { get; set; } + } + internal class RestCommand : ICommand + { + public Color Color => Color.PaleVioletRed; + public string Label => "Rest"; + public string Arguments => Rest.ToString(); + + public uint Rest { get; set; } + } + internal class SkipBytesCommand : ICommand + { + public Color Color => Color.MediumVioletRed; + public string Label => $"Skip 0x{Command:X}"; + public string Arguments => string.Join(", ", SkippedBytes.Select(b => $"0x{b:X}")); + + public byte Command { get; set; } + public byte[] SkippedBytes { get; set; } + } + internal class TempoCommand : ICommand + { + public Color Color => Color.DeepSkyBlue; + public string Label => $"Tempo {Command - 0xA3}"; // The two possible tempo commands are 0xA4 and 0xA5 + public string Arguments => Tempo.ToString(); + + public byte Command { get; set; } + public byte Tempo { get; set; } + } + internal class UnknownCommand : ICommand + { + public Color Color => Color.MediumVioletRed; + public string Label => $"Unknown 0x{Command:X}"; + public string Arguments => string.Join(", ", Args.Select(b => $"0x{b:X}")); + + public byte Command { get; set; } + public byte[] Args { get; set; } + } + internal class VoiceCommand : ICommand + { + public Color Color => Color.DarkSalmon; + public string Label => "Voice"; + public string Arguments => Voice.ToString(); + + public byte Voice { get; set; } + } + internal class VolumeCommand : ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Volume"; + public string Arguments => Volume.ToString(); + + public byte Volume { get; set; } + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Config.cs b/VG Music Studio.backup/Core/NDS/DSE/Config.cs new file mode 100644 index 00000000..25f856ca --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/Config.cs @@ -0,0 +1,45 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Properties; +using System; +using System.IO; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal class Config : Core.Config + { + public readonly string BGMPath; + public readonly string[] BGMFiles; + + public Config(string bgmPath) + { + BGMPath = bgmPath; + BGMFiles = Directory.GetFiles(bgmPath, "bgm*.smd", SearchOption.TopDirectoryOnly); + if (BGMFiles.Length == 0) + { + throw new Exception(Strings.ErrorDSENoSequences); + } + var songs = new Song[BGMFiles.Length]; + for (int i = 0; i < BGMFiles.Length; i++) + { + using (var reader = new EndianBinaryReader(File.OpenRead(BGMFiles[i]))) + { + SMD.Header header = reader.ReadObject(); + songs[i] = new Song(i, $"{Path.GetFileNameWithoutExtension(BGMFiles[i])} - {new string(header.Label.TakeWhile(c => c != '\0').ToArray())}"); + } + } + Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); + } + + public override string GetGameName() + { + return "DSE"; + } + public override string GetSongName(long index) + { + return index < 0 || index >= BGMFiles.Length + ? index.ToString() + : '\"' + BGMFiles[index] + '\"'; + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Enums.cs b/VG Music Studio.backup/Core/NDS/DSE/Enums.cs new file mode 100644 index 00000000..6cec60df --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/Enums.cs @@ -0,0 +1,22 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal enum EnvelopeState : byte + { + Zero = 0, + One = 1, + Two = 2, + Hold = 3, + Decay = 4, + Decay2 = 5, + Six = 6, + Seven = 7, + Eight = 8 + } + + internal enum SampleFormat : ushort + { + PCM8 = 0x000, + PCM16 = 0x100, + ADPCM = 0x200 + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Mixer.cs b/VG Music Studio.backup/Core/NDS/DSE/Mixer.cs new file mode 100644 index 00000000..29c8de54 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/Mixer.cs @@ -0,0 +1,220 @@ +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal class Mixer : Core.Mixer + { + private const int _numChannels = 0x20; + private readonly float _samplesReciprocal; + private readonly int _samplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + private readonly Channel[] _channels; + private readonly BufferedWaveProvider _buffer; + + public Mixer() + { + // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. + // - gbatek + // I'm not using either of those because the samples per buffer leads to an overflow eventually + const int sampleRate = 65456; + _samplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / _samplesPerBuffer; + + _channels = new Channel[_numChannels]; + for (byte i = 0; i < _numChannels; i++) + { + _channels[i] = new Channel(i); + } + + _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = _samplesPerBuffer * 64 + }; + Init(_buffer); + } + public override void Dispose() + { + base.Dispose(); + CloseWaveWriter(); + } + + public Channel AllocateChannel() + { + int GetScore(Channel c) + { + // Free channels should be used before releasing channels + return c.Owner == null ? -2 : Utils.IsStateRemovable(c.State) ? -1 : 0; + } + Channel nChan = null; + for (int i = 0; i < _numChannels; i++) + { + Channel c = _channels[i]; + if (nChan != null) + { + int nScore = GetScore(nChan); + int cScore = GetScore(c); + if (cScore <= nScore && (cScore < nScore || c.Volume <= nChan.Volume)) + { + nChan = c; + } + } + else + { + nChan = c; + } + } + return nChan != null && 0 >= GetScore(nChan) ? nChan : null; + } + + public void ChannelTick() + { + for (int i = 0; i < _numChannels; i++) + { + Channel chan = _channels[i]; + if (chan.Owner != null) + { + chan.Volume = (byte)chan.StepEnvelope(); + if (chan.NoteLength == 0 && !Utils.IsStateRemovable(chan.State)) + { + chan.SetEnvelopePhase7_2074ED8(); + } + int vol = SDAT.Utils.SustainTable[chan.NoteVelocity] + SDAT.Utils.SustainTable[chan.Volume] + SDAT.Utils.SustainTable[chan.Owner.Volume] + SDAT.Utils.SustainTable[chan.Owner.Expression]; + //int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" + int pitch = (chan.Key - chan.RootKey) << 6; // "<< 6" is "* 0x40" + if (Utils.IsStateRemovable(chan.State) && vol <= -92544) + { + chan.Stop(); + } + else + { + chan.Volume = SDAT.Utils.GetChannelVolume(vol); + chan.Panpot = chan.Owner.Panpot; + chan.Timer = SDAT.Utils.GetChannelTimer(chan.BaseTimer, pitch); + } + } + } + } + + public void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + public void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + public bool IsFading() + { + return _isFading; + } + public bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + public void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + private WaveFileWriter _waveWriter; + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter?.Dispose(); + } + public void Process(bool output, bool recording) + { + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + byte[] b = new byte[4]; + for (int i = 0; i < _samplesPerBuffer; i++) + { + int left = 0, + right = 0; + for (int j = 0; j < _numChannels; j++) + { + Channel chan = _channels[j]; + if (chan.Owner != null) + { + bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null + chan.Process(out short channelLeft, out short channelRight); + if (!muted) + { + left += channelLeft; + right += channelRight; + } + } + } + float f = left * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + left = (int)f; + b[0] = (byte)left; + b[1] = (byte)(left >> 8); + f = right * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + right = (int)f; + b[2] = (byte)right; + b[3] = (byte)(right >> 8); + masterLevel += masterStep; + if (output) + { + _buffer.AddSamples(b, 0, 4); + } + if (recording) + { + _waveWriter.Write(b, 0, 4); + } + } + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Player.cs b/VG Music Studio.backup/Core/NDS/DSE/Player.cs new file mode 100644 index 00000000..4fa2b81e --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/Player.cs @@ -0,0 +1,1040 @@ +using Kermalis.EndianBinaryIO; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal class Player : IPlayer + { + private readonly Mixer _mixer; + private readonly Config _config; + private readonly TimeBarrier _time; + private Thread _thread; + private readonly SWD _masterSWD; + private SWD _localSWD; + private byte[] _smdFile; + private Track[] _tracks; + private byte _tempo; + private int _tempoStack; + private long _elapsedLoops; + + public List[] Events { get; private set; } + public long MaxTicks { get; private set; } + public long ElapsedTicks { get; private set; } + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + private int _longestTrack; + + public PlayerState State { get; private set; } + public event SongEndedEvent SongEnded; + + public Player(Mixer mixer, Config config) + { + _mixer = mixer; + _config = config; + _masterSWD = new SWD(Path.Combine(config.BGMPath, "bgm.swd")); + + _time = new TimeBarrier(192); + } + private void CreateThread() + { + _thread = new Thread(Tick) { Name = "DSE Player Tick" }; + _thread.Start(); + } + private void WaitThread() + { + if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) + { + _thread.Join(); + } + } + + private void InitEmulation() + { + _tempo = 120; + _tempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + _mixer.ResetFade(); + for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + { + _tracks[trackIndex].Init(); + } + } + private void SetTicks() + { + MaxTicks = 0; + for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) + { + Events[trackIndex] = Events[trackIndex].OrderBy(e => e.Offset).ToList(); + List evs = Events[trackIndex]; + Track track = _tracks[trackIndex]; + track.Init(); + ElapsedTicks = 0; + while (true) + { + SongEvent e = evs.Single(ev => ev.Offset == track.CurOffset); + if (e.Ticks.Count > 0) + { + break; + } + else + { + e.Ticks.Add(ElapsedTicks); + ExecuteNext(track); + if (track.Stopped) + { + break; + } + else + { + ElapsedTicks += track.Rest; + track.Rest = 0; + } + } + } + if (ElapsedTicks > MaxTicks) + { + _longestTrack = trackIndex; + MaxTicks = ElapsedTicks; + } + track.StopAllChannels(); + } + } + public void LoadSong(long index) + { + if (_tracks != null) + { + for (int i = 0; i < _tracks.Length; i++) + { + _tracks[i].StopAllChannels(); + } + _tracks = null; + } + string bgm = _config.BGMFiles[index]; + _localSWD = new SWD(Path.ChangeExtension(bgm, "swd")); + _smdFile = File.ReadAllBytes(bgm); + using (var reader = new EndianBinaryReader(new MemoryStream(_smdFile))) + { + SMD.Header header = reader.ReadObject(); + SMD.ISongChunk songChunk; + switch (header.Version) + { + case 0x402: + { + songChunk = reader.ReadObject(); + break; + } + case 0x415: + { + songChunk = reader.ReadObject(); + break; + } + default: throw new Exception(string.Format(Strings.ErrorDSEInvalidHeaderVersion, header.Version)); + } + _tracks = new Track[songChunk.NumTracks]; + Events = new List[songChunk.NumTracks]; + for (byte trackIndex = 0; trackIndex < songChunk.NumTracks; trackIndex++) + { + Events[trackIndex] = new List(); + bool EventExists(long offset) + { + return Events[trackIndex].Any(e => e.Offset == offset); + } + + long chunkStart = reader.BaseStream.Position; + reader.BaseStream.Position += 0x14; // Skip header + _tracks[trackIndex] = new Track(trackIndex, reader.BaseStream.Position); + + uint lastNoteDuration = 0, lastRest = 0; + bool cont = true; + while (cont) + { + long offset = reader.BaseStream.Position; + void AddEvent(ICommand command) + { + Events[trackIndex].Add(new SongEvent(offset, command)); + } + byte cmd = reader.ReadByte(); + if (cmd <= 0x7F) + { + byte arg = reader.ReadByte(); + int numParams = (arg & 0xC0) >> 6; + int oct = ((arg & 0x30) >> 4) - 2; + int k = arg & 0xF; + if (k < 12) + { + uint duration; + if (numParams == 0) + { + duration = lastNoteDuration; + } + else // Big Endian reading of 8, 16, or 24 bits + { + duration = 0; + for (int b = 0; b < numParams; b++) + { + duration = (duration << 8) | reader.ReadByte(); + } + lastNoteDuration = duration; + } + if (!EventExists(offset)) + { + AddEvent(new NoteCommand { Key = (byte)k, OctaveChange = (sbyte)oct, Velocity = cmd, Duration = duration }); + } + } + else + { + throw new Exception(string.Format(Strings.ErrorDSEInvalidKey, trackIndex, offset, k)); + } + } + else if (cmd >= 0x80 && cmd <= 0x8F) + { + lastRest = Utils.FixedRests[cmd - 0x80]; + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = lastRest }); + } + } + else // 0x90-0xFF + { + // TODO: 0x95 - a rest that may or may not repeat depending on some condition within channels + // TODO: 0x9E - may or may not jump somewhere else depending on an unknown structure + switch (cmd) + { + case 0x90: + { + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = lastRest }); + } + break; + } + case 0x91: + { + lastRest = (uint)(lastRest + reader.ReadSByte()); + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = lastRest }); + } + break; + } + case 0x92: + { + lastRest = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = lastRest }); + } + break; + } + case 0x93: + { + lastRest = reader.ReadUInt16(); + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = lastRest }); + } + break; + } + case 0x94: + { + lastRest = (uint)(reader.ReadByte() | (reader.ReadByte() << 8) | (reader.ReadByte() << 16)); + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = lastRest }); + } + break; + } + case 0x96: + case 0x97: + case 0x9A: + case 0x9B: + case 0x9F: + case 0xA2: + case 0xA3: + case 0xA6: + case 0xA7: + case 0xAD: + case 0xAE: + case 0xB7: + case 0xB8: + case 0xB9: + case 0xBA: + case 0xBB: + case 0xBD: + case 0xC1: + case 0xC2: + case 0xC4: + case 0xC5: + case 0xC6: + case 0xC7: + case 0xC8: + case 0xC9: + case 0xCA: + case 0xCC: + case 0xCD: + case 0xCE: + case 0xCF: + case 0xD9: + case 0xDA: + case 0xDE: + case 0xE6: + case 0xEB: + case 0xEE: + case 0xF4: + case 0xF5: + case 0xF7: + case 0xF9: + case 0xFA: + case 0xFB: + case 0xFC: + case 0xFD: + case 0xFE: + case 0xFF: + { + if (!EventExists(offset)) + { + AddEvent(new InvalidCommand { Command = cmd }); + } + break; + } + case 0x98: + { + if (!EventExists(offset)) + { + AddEvent(new FinishCommand()); + } + cont = false; + break; + } + case 0x99: + { + if (!EventExists(offset)) + { + AddEvent(new LoopStartCommand { Offset = reader.BaseStream.Position }); + } + break; + } + case 0xA0: + { + byte octave = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new OctaveSetCommand { Octave = octave }); + } + break; + } + case 0xA1: + { + sbyte change = reader.ReadSByte(); + if (!EventExists(offset)) + { + AddEvent(new OctaveAddCommand { OctaveChange = change }); + } + break; + } + case 0xA4: + case 0xA5: // The code for these two is identical + { + byte tempoArg = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new TempoCommand { Command = cmd, Tempo = tempoArg }); + } + break; + } + case 0xAB: + { + byte[] bytes = reader.ReadBytes(1); + if (!EventExists(offset)) + { + AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + } + break; + } + case 0xAC: + { + byte voice = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new VoiceCommand { Voice = voice }); + } + break; + } + case 0xCB: + case 0xF8: + { + byte[] bytes = reader.ReadBytes(2); + if (!EventExists(offset)) + { + AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); + } + break; + } + case 0xD7: + { + ushort bend = reader.ReadUInt16(); + if (!EventExists(offset)) + { + AddEvent(new PitchBendCommand { Bend = bend }); + } + break; + } + case 0xE0: + { + byte volume = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new VolumeCommand { Volume = volume }); + } + break; + } + case 0xE3: + { + byte expression = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new ExpressionCommand { Expression = expression }); + } + break; + } + case 0xE8: + { + byte panArg = reader.ReadByte(); + if (!EventExists(offset)) + { + AddEvent(new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); + } + break; + } + case 0x9D: + case 0xB0: + case 0xC0: + { + if (!EventExists(offset)) + { + AddEvent(new UnknownCommand { Command = cmd, Args = Array.Empty() }); + } + break; + } + case 0x9C: + case 0xA9: + case 0xAA: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB5: + case 0xB6: + case 0xBC: + case 0xBE: + case 0xBF: + case 0xC3: + case 0xD0: + case 0xD1: + case 0xD2: + case 0xDB: + case 0xDF: + case 0xE1: + case 0xE7: + case 0xE9: + case 0xEF: + case 0xF6: + { + byte[] args = reader.ReadBytes(1); + if (!EventExists(offset)) + { + AddEvent(new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + case 0xA8: + case 0xB4: + case 0xD3: + case 0xD5: + case 0xD6: + case 0xD8: + case 0xF2: + { + byte[] args = reader.ReadBytes(2); + if (!EventExists(offset)) + { + AddEvent(new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + case 0xAF: + case 0xD4: + case 0xE2: + case 0xEA: + case 0xF3: + { + byte[] args = reader.ReadBytes(3); + if (!EventExists(offset)) + { + AddEvent(new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + case 0xDD: + case 0xE5: + case 0xED: + case 0xF1: + { + byte[] args = reader.ReadBytes(4); + if (!EventExists(offset)) + { + AddEvent(new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + case 0xDC: + case 0xE4: + case 0xEC: + case 0xF0: + { + byte[] args = reader.ReadBytes(5); + if (!EventExists(offset)) + { + AddEvent(new UnknownCommand { Command = cmd, Args = args }); + } + break; + } + default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, trackIndex, offset, cmd)); + } + } + } + uint chunkLength = reader.ReadUInt32(chunkStart + 0xC); + reader.BaseStream.Position += chunkLength; + // Align 4 + while (reader.BaseStream.Position % 4 != 0) + { + reader.BaseStream.Position++; + } + } + SetTicks(); + } + } + public void SetCurrentPosition(long ticks) + { + if (_tracks == null) + { + SongEnded?.Invoke(); + } + else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + if (State == PlayerState.Playing) + { + Pause(); + } + InitEmulation(); + while (true) + { + if (ElapsedTicks == ticks) + { + goto finish; + } + else + { + while (_tempoStack >= 240) + { + _tempoStack -= 240; + for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + { + Track track = _tracks[trackIndex]; + if (!track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track); + } + } + } + ElapsedTicks++; + if (ElapsedTicks == ticks) + { + goto finish; + } + } + _tempoStack += _tempo; + } + } + finish: + for (int i = 0; i < _tracks.Length; i++) + { + _tracks[i].StopAllChannels(); + } + Pause(); + } + } + public void Play() + { + if (_tracks == null) + { + SongEnded?.Invoke(); + } + else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + Stop(); + InitEmulation(); + State = PlayerState.Playing; + CreateThread(); + } + } + public void Pause() + { + if (State == PlayerState.Playing) + { + State = PlayerState.Paused; + WaitThread(); + } + else if (State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.Playing; + CreateThread(); + } + } + public void Stop() + { + if (State == PlayerState.Playing || State == PlayerState.Paused) + { + State = PlayerState.Stopped; + WaitThread(); + } + } + public void Record(string fileName) + { + _mixer.CreateWaveWriter(fileName); + InitEmulation(); + State = PlayerState.Recording; + CreateThread(); + WaitThread(); + _mixer.CloseWaveWriter(); + } + public void Dispose() + { + if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.ShutDown; + WaitThread(); + } + } + public void GetSongState(UI.SongInfoControl.SongInfo info) + { + info.Tempo = _tempo; + for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + { + Track track = _tracks[trackIndex]; + UI.SongInfoControl.SongInfo.Track tin = info.Tracks[trackIndex]; + tin.Position = track.CurOffset; + tin.Rest = track.Rest; + tin.Voice = track.Voice; + tin.Type = "PCM"; + tin.Volume = track.Volume; + tin.PitchBend = track.PitchBend; + tin.Extra = track.Octave; + tin.Panpot = track.Panpot; + + Channel[] channels = track.Channels.ToArray(); + if (channels.Length == 0) + { + tin.Keys[0] = byte.MaxValue; + tin.LeftVolume = 0f; + tin.RightVolume = 0f; + //tin.Type = string.Empty; + } + else + { + int numKeys = 0; + float left = 0f; + float right = 0f; + for (int j = 0; j < channels.Length; j++) + { + Channel c = channels[j]; + if (!Utils.IsStateRemovable(c.State)) + { + tin.Keys[numKeys++] = c.Key; + } + float a = (float)(-c.Panpot + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > left) + { + left = a; + } + a = (float)(c.Panpot + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > right) + { + right = a; + } + } + tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array + tin.LeftVolume = left; + tin.RightVolume = right; + //tin.Type = string.Join(", ", channels.Select(c => c.State.ToString())); + } + } + } + + private void ExecuteNext(Track track) + { + byte cmd = _smdFile[track.CurOffset++]; + if (cmd <= 0x7F) + { + byte arg = _smdFile[track.CurOffset++]; + int numParams = (arg & 0xC0) >> 6; + int oct = ((arg & 0x30) >> 4) - 2; + int k = arg & 0xF; + if (k < 12) + { + uint duration; + if (numParams == 0) + { + duration = track.LastNoteDuration; + } + else + { + duration = 0; + for (int b = 0; b < numParams; b++) + { + duration = (duration << 8) | _smdFile[track.CurOffset++]; + } + track.LastNoteDuration = duration; + } + Channel channel = _mixer.AllocateChannel(); + channel.Stop(); + track.Octave = (byte)(track.Octave + oct); + if (channel.StartPCM(_localSWD, _masterSWD, track.Voice, k + (12 * track.Octave), duration)) + { + channel.NoteVelocity = cmd; + channel.Owner = track; + track.Channels.Add(channel); + } + } + else + { + throw new Exception(string.Format(Strings.ErrorDSEInvalidKey, track.Index, track.CurOffset, k)); + } + } + else if (cmd >= 0x80 && cmd <= 0x8F) + { + track.LastRest = Utils.FixedRests[cmd - 0x80]; + track.Rest = track.LastRest; + } + else // 0x90-0xFF + { + // TODO: 0x95, 0x9E + switch (cmd) + { + case 0x90: + { + track.Rest = track.LastRest; + break; + } + case 0x91: + { + track.LastRest = (uint)(track.LastRest + (sbyte)_smdFile[track.CurOffset++]); + track.Rest = track.LastRest; + break; + } + case 0x92: + { + track.LastRest = _smdFile[track.CurOffset++]; + track.Rest = track.LastRest; + break; + } + case 0x93: + { + track.LastRest = (uint)(_smdFile[track.CurOffset++] | (_smdFile[track.CurOffset++] << 8)); + track.Rest = track.LastRest; + break; + } + case 0x94: + { + track.LastRest = (uint)(_smdFile[track.CurOffset++] | (_smdFile[track.CurOffset++] << 8) | (_smdFile[track.CurOffset++] << 16)); + track.Rest = track.LastRest; + break; + } + case 0x96: + case 0x97: + case 0x9A: + case 0x9B: + case 0x9F: + case 0xA2: + case 0xA3: + case 0xA6: + case 0xA7: + case 0xAD: + case 0xAE: + case 0xB7: + case 0xB8: + case 0xB9: + case 0xBA: + case 0xBB: + case 0xBD: + case 0xC1: + case 0xC2: + case 0xC4: + case 0xC5: + case 0xC6: + case 0xC7: + case 0xC8: + case 0xC9: + case 0xCA: + case 0xCC: + case 0xCD: + case 0xCE: + case 0xCF: + case 0xD9: + case 0xDA: + case 0xDE: + case 0xE6: + case 0xEB: + case 0xEE: + case 0xF4: + case 0xF5: + case 0xF7: + case 0xF9: + case 0xFA: + case 0xFB: + case 0xFC: + case 0xFD: + case 0xFE: + case 0xFF: + { + track.Stopped = true; + break; + } + case 0x98: + { + if (track.LoopOffset == -1) + { + track.Stopped = true; + } + else + { + track.CurOffset = track.LoopOffset; + } + break; + } + case 0x99: + { + track.LoopOffset = track.CurOffset; + break; + } + case 0xA0: + { + track.Octave = _smdFile[track.CurOffset++]; + break; + } + case 0xA1: + { + track.Octave = (byte)(track.Octave + (sbyte)_smdFile[track.CurOffset++]); + break; + } + case 0xA4: + case 0xA5: + { + _tempo = _smdFile[track.CurOffset++]; + break; + } + case 0xAB: + { + track.CurOffset++; + break; + } + case 0xAC: + { + track.Voice = _smdFile[track.CurOffset++]; + break; + } + case 0xCB: + case 0xF8: + { + track.CurOffset += 2; + break; + } + case 0xD7: + { + track.PitchBend = (ushort)(_smdFile[track.CurOffset++] | (_smdFile[track.CurOffset++] << 8)); + break; + } + case 0xE0: + { + track.Volume = _smdFile[track.CurOffset++]; + break; + } + case 0xE3: + { + track.Expression = _smdFile[track.CurOffset++]; + break; + } + case 0xE8: + { + track.Panpot = (sbyte)(_smdFile[track.CurOffset++] - 0x40); + break; + } + case 0x9D: + case 0xB0: + case 0xC0: + { + break; + } + case 0x9C: + case 0xA9: + case 0xAA: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB5: + case 0xB6: + case 0xBC: + case 0xBE: + case 0xBF: + case 0xC3: + case 0xD0: + case 0xD1: + case 0xD2: + case 0xDB: + case 0xDF: + case 0xE1: + case 0xE7: + case 0xE9: + case 0xEF: + case 0xF6: + { + track.CurOffset++; + break; + } + case 0xA8: + case 0xB4: + case 0xD3: + case 0xD5: + case 0xD6: + case 0xD8: + case 0xF2: + { + track.CurOffset += 2; + break; + } + case 0xAF: + case 0xD4: + case 0xE2: + case 0xEA: + case 0xF3: + { + track.CurOffset += 3; + break; + } + case 0xDD: + case 0xE5: + case 0xED: + case 0xF1: + { + track.CurOffset += 4; + break; + } + case 0xDC: + case 0xE4: + case 0xEC: + case 0xF0: + { + track.CurOffset += 5; + break; + } + default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, track.Index, track.CurOffset, cmd)); + } + } + } + + private void Tick() + { + _time.Start(); + while (true) + { + PlayerState state = State; + bool playing = state == PlayerState.Playing; + bool recording = state == PlayerState.Recording; + if (!playing && !recording) + { + goto stop; + } + + void MixerProcess() + { + _mixer.ChannelTick(); + _mixer.Process(playing, recording); + } + + while (_tempoStack >= 240) + { + _tempoStack -= 240; + bool allDone = true; + for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) + { + Track track = _tracks[trackIndex]; + track.Tick(); + while (track.Rest == 0 && !track.Stopped) + { + ExecuteNext(track); + } + if (trackIndex == _longestTrack) + { + if (ElapsedTicks == MaxTicks) + { + if (!track.Stopped) + { + List evs = Events[trackIndex]; + for (int i = 0; i < evs.Count; i++) + { + SongEvent ev = evs[i]; + if (ev.Offset == track.CurOffset) + { + ElapsedTicks = ev.Ticks[0] - track.Rest; + break; + } + } + _elapsedLoops++; + if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) + { + _mixer.BeginFadeOut(); + } + } + } + else + { + ElapsedTicks++; + } + } + if (!track.Stopped || track.Channels.Count != 0) + { + allDone = false; + } + } + if (_mixer.IsFadeDone()) + { + allDone = true; + } + if (allDone) + { + MixerProcess(); + State = PlayerState.Stopped; + SongEnded?.Invoke(); + } + } + _tempoStack += _tempo; + MixerProcess(); + if (playing) + { + _time.Wait(); + } + } + stop: + _time.Stop(); + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/SMD.cs b/VG Music Studio.backup/Core/NDS/DSE/SMD.cs new file mode 100644 index 00000000..706cb06c --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/SMD.cs @@ -0,0 +1,61 @@ +using Kermalis.EndianBinaryIO; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal class SMD + { + public class Header + { + [BinaryStringFixedLength(4)] + public string Type { get; set; } // "smdb" or "smdl" + [BinaryArrayFixedLength(4)] + public byte[] Unknown1 { get; set; } + public uint Length { get; set; } + public ushort Version { get; set; } + [BinaryArrayFixedLength(10)] + public byte[] Unknown2 { get; set; } + public ushort Year { get; set; } + public byte Month { get; set; } + public byte Day { get; set; } + public byte Hour { get; set; } + public byte Minute { get; set; } + public byte Second { get; set; } + public byte Centisecond { get; set; } + [BinaryStringFixedLength(16)] + public string Label { get; set; } + [BinaryArrayFixedLength(16)] + public byte[] Unknown3 { get; set; } + } + + public interface ISongChunk + { + byte NumTracks { get; } + } + public class SongChunk_V402 : ISongChunk + { + [BinaryStringFixedLength(4)] + public string Type { get; set; } + [BinaryArrayFixedLength(16)] + public byte[] Unknown1 { get; set; } + public byte NumTracks { get; set; } + public byte NumChannels { get; set; } + [BinaryArrayFixedLength(4)] + public byte[] Unknown2 { get; set; } + public sbyte MasterVolume { get; set; } + public sbyte MasterPanpot { get; set; } + [BinaryArrayFixedLength(4)] + public byte[] Unknown3 { get; set; } + } + public class SongChunk_V415 : ISongChunk + { + [BinaryStringFixedLength(4)] + public string Type { get; set; } + [BinaryArrayFixedLength(18)] + public byte[] Unknown1 { get; set; } + public byte NumTracks { get; set; } + public byte NumChannels { get; set; } + [BinaryArrayFixedLength(40)] + public byte[] Unknown2 { get; set; } + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/SWD.cs b/VG Music Studio.backup/Core/NDS/DSE/SWD.cs new file mode 100644 index 00000000..4e9f984a --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/SWD.cs @@ -0,0 +1,471 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal class SWD + { + public interface IHeader + { + + } + private class Header_V402 : IHeader + { + [BinaryArrayFixedLength(10)] + public byte[] Unknown1 { get; set; } + public ushort Year { get; set; } + public byte Month { get; set; } + public byte Day { get; set; } + public byte Hour { get; set; } + public byte Minute { get; set; } + public byte Second { get; set; } + public byte Centisecond { get; set; } + [BinaryStringFixedLength(16)] + public string Label { get; set; } + [BinaryArrayFixedLength(22)] + public byte[] Unknown2 { get; set; } + public byte NumWAVISlots { get; set; } + public byte NumPRGISlots { get; set; } + public byte NumKeyGroups { get; set; } + [BinaryArrayFixedLength(7)] + public byte[] Padding { get; set; } + } + private class Header_V415 : IHeader + { + [BinaryArrayFixedLength(10)] + public byte[] Unknown1 { get; set; } + public ushort Year { get; set; } + public byte Month { get; set; } + public byte Day { get; set; } + public byte Hour { get; set; } + public byte Minute { get; set; } + public byte Second { get; set; } + public byte Centisecond { get; set; } + [BinaryStringFixedLength(16)] + public string Label { get; set; } + [BinaryArrayFixedLength(16)] + public byte[] Unknown2 { get; set; } + public uint PCMDLength { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown3 { get; set; } + public ushort NumWAVISlots { get; set; } + public ushort NumPRGISlots { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown4 { get; set; } + public uint WAVILength { get; set; } + } + + public interface ISplitEntry + { + byte LowKey { get; } + byte HighKey { get; } + int SampleId { get; } + byte SampleRootKey { get; } + sbyte SampleTranspose { get; } + byte AttackVolume { get; set; } + byte Attack { get; set; } + byte Decay { get; set; } + byte Sustain { get; set; } + byte Hold { get; set; } + byte Decay2 { get; set; } + byte Release { get; set; } + } + public class SplitEntry_V402 : ISplitEntry + { + public ushort Id { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown1 { get; set; } + public byte LowKey { get; set; } + public byte HighKey { get; set; } + public byte LowKey2 { get; set; } + public byte HighKey2 { get; set; } + public byte LowVelocity { get; set; } + public byte HighVelocity { get; set; } + public byte LowVelocity2 { get; set; } + public byte HighVelocity2 { get; set; } + [BinaryArrayFixedLength(5)] + public byte[] Unknown2 { get; set; } + public byte SampleId { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown3 { get; set; } + public byte SampleRootKey { get; set; } + public sbyte SampleTranspose { get; set; } + public byte SampleVolume { get; set; } + public sbyte SamplePanpot { get; set; } + public byte KeyGroupId { get; set; } + [BinaryArrayFixedLength(15)] + public byte[] Unknown4 { get; set; } + public byte AttackVolume { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Hold { get; set; } + public byte Decay2 { get; set; } + public byte Release { get; set; } + public byte Unknown5 { get; set; } + + int ISplitEntry.SampleId => SampleId; + } + public class SplitEntry_V415 : ISplitEntry + { + public ushort Id { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown1 { get; set; } + public byte LowKey { get; set; } + public byte HighKey { get; set; } + public byte LowKey2 { get; set; } + public byte HighKey2 { get; set; } + public byte LowVelocity { get; set; } + public byte HighVelocity { get; set; } + public byte LowVelocity2 { get; set; } + public byte HighVelocity2 { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown2 { get; set; } + public ushort SampleId { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown3 { get; set; } + public byte SampleRootKey { get; set; } + public sbyte SampleTranspose { get; set; } + public byte SampleVolume { get; set; } + public sbyte SamplePanpot { get; set; } + public byte KeyGroupId { get; set; } + [BinaryArrayFixedLength(13)] + public byte[] Unknown4 { get; set; } + public byte AttackVolume { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Hold { get; set; } + public byte Decay2 { get; set; } + public byte Release { get; set; } + public byte Unknown5 { get; set; } + + int ISplitEntry.SampleId => SampleId; + } + + public interface IProgramInfo + { + ISplitEntry[] SplitEntries { get; } + } + public class ProgramInfo_V402 : IProgramInfo + { + public byte Id { get; set; } + public byte NumSplits { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown1 { get; set; } + public byte Volume { get; set; } + public byte Panpot { get; set; } + [BinaryArrayFixedLength(5)] + public byte[] Unknown2 { get; set; } + public byte NumLFOs { get; set; } + [BinaryArrayFixedLength(4)] + public byte[] Unknown3 { get; set; } + [BinaryArrayFixedLength(16)] + public KeyGroup[] KeyGroups { get; set; } + [BinaryArrayVariableLength(nameof(NumLFOs))] + public LFOInfo LFOInfos { get; set; } + [BinaryArrayVariableLength(nameof(NumSplits))] + public SplitEntry_V402[] SplitEntries { get; set; } + + [BinaryIgnore] + ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; + } + public class ProgramInfo_V415 : IProgramInfo + { + public ushort Id { get; set; } + public ushort NumSplits { get; set; } + public byte Volume { get; set; } + public byte Panpot { get; set; } + [BinaryArrayFixedLength(5)] + public byte[] Unknown1 { get; set; } + public byte NumLFOs { get; set; } + [BinaryArrayFixedLength(4)] + public byte[] Unknown2 { get; set; } + [BinaryArrayVariableLength(nameof(NumLFOs))] + public LFOInfo[] LFOInfos { get; set; } + [BinaryArrayFixedLength(16)] + public byte[] Unknown3 { get; set; } + [BinaryArrayVariableLength(nameof(NumSplits))] + public SplitEntry_V415[] SplitEntries { get; set; } + + [BinaryIgnore] + ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; + } + + public interface IWavInfo + { + byte RootKey { get; } + sbyte Transpose { get; } + SampleFormat SampleFormat { get; } + bool Loop { get; } + uint SampleRate { get; } + uint SampleOffset { get; } + uint LoopStart { get; } + uint LoopEnd { get; } + byte EnvMult { get; } + byte AttackVolume { get; } + byte Attack { get; } + byte Decay { get; } + byte Sustain { get; } + byte Hold { get; } + byte Decay2 { get; } + byte Release { get; } + } + public class WavInfo_V402 : IWavInfo + { + public byte Unknown1 { get; set; } + public byte Id { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown2 { get; set; } + public byte RootKey { get; set; } + public sbyte Transpose { get; set; } + public byte Volume { get; set; } + public sbyte Panpot { get; set; } + public SampleFormat SampleFormat { get; set; } + [BinaryArrayFixedLength(7)] + public byte[] Unknown3 { get; set; } + public bool Loop { get; set; } + public uint SampleRate { get; set; } + public uint SampleOffset { get; set; } + public uint LoopStart { get; set; } + public uint LoopEnd { get; set; } + [BinaryArrayFixedLength(16)] + public byte[] Unknown4 { get; set; } + public byte EnvOn { get; set; } + public byte EnvMult { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown5 { get; set; } + public byte AttackVolume { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Hold { get; set; } + public byte Decay2 { get; set; } + public byte Release { get; set; } + public byte Unknown6 { get; set; } + } + public class WavInfo_V415 : IWavInfo + { + [BinaryArrayFixedLength(2)] + public byte[] Unknown1 { get; set; } + public ushort Id { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown2 { get; set; } + public byte RootKey { get; set; } + public sbyte Transpose { get; set; } + public byte Volume { get; set; } + public sbyte Panpot { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown3 { get; set; } + public ushort Version { get; set; } + public SampleFormat SampleFormat { get; set; } + public byte Unknown4 { get; set; } + public bool Loop { get; set; } + public byte Unknown5 { get; set; } + public byte SamplesPer32Bits { get; set; } + public byte Unknown6 { get; set; } + public byte BitDepth { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown7 { get; set; } + public uint SampleRate { get; set; } + public uint SampleOffset { get; set; } + public uint LoopStart { get; set; } + public uint LoopEnd { get; set; } + public byte EnvOn { get; set; } + public byte EnvMult { get; set; } + [BinaryArrayFixedLength(6)] + public byte[] Unknown8 { get; set; } + public byte AttackVolume { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Hold { get; set; } + public byte Decay2 { get; set; } + public byte Release { get; set; } + public byte Unknown9 { get; set; } + } + + public class SampleBlock + { + public IWavInfo WavInfo; + public byte[] Data; + } + public class ProgramBank + { + public IProgramInfo[] ProgramInfos; + public KeyGroup[] KeyGroups; + } + public class KeyGroup + { + public ushort Id { get; set; } + public byte Poly { get; set; } + public byte Priority { get; set; } + public byte Low { get; set; } + public byte High { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown { get; set; } + } + public class LFOInfo + { + [BinaryArrayFixedLength(16)] + public byte[] Unknown { get; set; } + } + + public string Type; // "swdb" or "swdl" + public byte[] Unknown; + public uint Length; + public ushort Version; + public IHeader Header; + + public ProgramBank Programs; + public SampleBlock[] Samples; + + public SWD(string path) + { + using (var reader = new EndianBinaryReader(new MemoryStream(File.ReadAllBytes(path)))) + { + Type = reader.ReadString(4, false); + Unknown = reader.ReadBytes(4); + Length = reader.ReadUInt32(); + Version = reader.ReadUInt16(); + switch (Version) + { + case 0x402: + { + Header_V402 header = reader.ReadObject(); + Header = header; + Programs = ReadPrograms(reader, header.NumPRGISlots); + Samples = ReadSamples(reader, header.NumWAVISlots); + break; + } + case 0x415: + { + Header_V415 header = reader.ReadObject(); + Header = header; + Programs = ReadPrograms(reader, header.NumPRGISlots); + if (header.PCMDLength != 0 && (header.PCMDLength & 0xFFFF0000) != 0xAAAA0000) + { + Samples = ReadSamples(reader, header.NumWAVISlots); + } + break; + } + default: throw new InvalidDataException(); + } + } + } + + private static long FindChunk(EndianBinaryReader reader, string chunk) + { + long pos = -1; + long oldPosition = reader.BaseStream.Position; + reader.BaseStream.Position = 0; + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + string str = reader.ReadString(4, false); + if (str == chunk) + { + pos = reader.BaseStream.Position - 4; + break; + } + switch (str) + { + case "swdb": + case "swdl": + { + reader.BaseStream.Position += 0x4C; + break; + } + default: + { + reader.BaseStream.Position += 0x8; + uint length = reader.ReadUInt32(); + reader.BaseStream.Position += length; + // Align 4 + while (reader.BaseStream.Position % 4 != 0) + { + reader.BaseStream.Position++; + } + break; + } + } + } + reader.BaseStream.Position = oldPosition; + return pos; + } + + private static SampleBlock[] ReadSamples(EndianBinaryReader reader, int numWAVISlots) where T : IWavInfo, new() + { + long waviChunkOffset = FindChunk(reader, "wavi"); + long pcmdChunkOffset = FindChunk(reader, "pcmd"); + if (waviChunkOffset == -1 || pcmdChunkOffset == -1) + { + throw new InvalidDataException(); + } + else + { + waviChunkOffset += 0x10; + pcmdChunkOffset += 0x10; + var samples = new SampleBlock[numWAVISlots]; + for (int i = 0; i < numWAVISlots; i++) + { + ushort offset = reader.ReadUInt16(waviChunkOffset + (2 * i)); + if (offset != 0) + { + T wavInfo = reader.ReadObject(offset + waviChunkOffset); + samples[i] = new SampleBlock + { + WavInfo = wavInfo, + Data = reader.ReadBytes((int)((wavInfo.LoopStart + wavInfo.LoopEnd) * 4), pcmdChunkOffset + wavInfo.SampleOffset) + }; + } + } + return samples; + } + } + private static ProgramBank ReadPrograms(EndianBinaryReader reader, int numPRGISlots) where T : IProgramInfo, new() + { + long chunkOffset = FindChunk(reader, "prgi"); + if (chunkOffset == -1) + { + return null; + } + else + { + chunkOffset += 0x10; + var programInfos = new IProgramInfo[numPRGISlots]; + for (int i = 0; i < programInfos.Length; i++) + { + ushort offset = reader.ReadUInt16(chunkOffset + (2 * i)); + if (offset != 0) + { + programInfos[i] = reader.ReadObject(offset + chunkOffset); + } + } + return new ProgramBank + { + ProgramInfos = programInfos, + KeyGroups = ReadKeyGroups(reader) + }; + } + } + private static KeyGroup[] ReadKeyGroups(EndianBinaryReader reader) + { + long chunkOffset = FindChunk(reader, "kgrp"); + if (chunkOffset == -1) + { + return Array.Empty(); + } + else + { + uint chunkLength = reader.ReadUInt32(chunkOffset + 0xC); + var keyGroups = new KeyGroup[chunkLength / 8]; // 8 is the size of a KeyGroup + for (int i = 0; i < keyGroups.Length; i++) + { + keyGroups[i] = reader.ReadObject(); + } + return keyGroups; + } + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Track.cs b/VG Music Studio.backup/Core/NDS/DSE/Track.cs new file mode 100644 index 00000000..9a330cab --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/Track.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal class Track + { + public readonly byte Index; + private readonly long _startOffset; + public byte Octave; + public byte Voice; + public byte Expression; + public byte Volume; + public sbyte Panpot; + public uint Rest; + public ushort PitchBend; + public long CurOffset; + public long LoopOffset; + public bool Stopped; + public uint LastNoteDuration; + public uint LastRest; + + public readonly List Channels = new List(0x10); + + public Track(byte i, long startOffset) + { + Index = i; + _startOffset = startOffset; + } + + public void Init() + { + Expression = 0; + Voice = 0; + Volume = 0; + Octave = 4; + Panpot = 0; + Rest = 0; + PitchBend = 0; + CurOffset = _startOffset; + LoopOffset = -1; + Stopped = false; + LastNoteDuration = 0; + LastRest = 0; + StopAllChannels(); + } + + public void Tick() + { + if (Rest > 0) + { + Rest--; + } + for (int i = 0; i < Channels.Count; i++) + { + Channel c = Channels[i]; + if (c.NoteLength > 0) + { + c.NoteLength--; + } + } + } + + public void StopAllChannels() + { + Channel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + chans[i].Stop(); + } + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Utils.cs b/VG Music Studio.backup/Core/NDS/DSE/Utils.cs new file mode 100644 index 00000000..1f16b1a5 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/DSE/Utils.cs @@ -0,0 +1,53 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.DSE +{ + internal static class Utils + { + public static short[] Duration16 = new short[128] + { + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, + 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, + 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, + 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F, + 0x0020, 0x0023, 0x0028, 0x002D, 0x0033, 0x0039, 0x0040, 0x0048, + 0x0050, 0x0058, 0x0062, 0x006D, 0x0078, 0x0083, 0x0090, 0x009E, + 0x00AC, 0x00BC, 0x00CC, 0x00DE, 0x00F0, 0x0104, 0x0119, 0x012F, + 0x0147, 0x0160, 0x017A, 0x0196, 0x01B3, 0x01D2, 0x01F2, 0x0214, + 0x0238, 0x025E, 0x0285, 0x02AE, 0x02D9, 0x0307, 0x0336, 0x0367, + 0x039B, 0x03D1, 0x0406, 0x0442, 0x047E, 0x04C4, 0x0500, 0x0546, + 0x058C, 0x0622, 0x0672, 0x06CC, 0x071C, 0x0776, 0x07DA, 0x0834, + 0x0898, 0x0906, 0x096A, 0x09D8, 0x0A50, 0x0ABE, 0x0B40, 0x0BB8, + 0x0C3A, 0x0CBC, 0x0D48, 0x0DDE, 0x0E6A, 0x0F00, 0x0FA0, 0x1040, + 0x10EA, 0x1194, 0x123E, 0x12F2, 0x13B0, 0x146E, 0x1536, 0x15FE, + 0x16D0, 0x17A2, 0x187E, 0x195A, 0x1A40, 0x1B30, 0x1C20, 0x1D1A, + 0x1E1E, 0x1F22, 0x2030, 0x2148, 0x2260, 0x2382, 0x2710, 0x7FFF + }; + public static int[] Duration32 = new int[128] + { + 0x00000000, 0x00000004, 0x00000007, 0x0000000A, 0x0000000F, 0x00000015, 0x0000001C, 0x00000024, + 0x0000002E, 0x0000003A, 0x00000048, 0x00000057, 0x00000068, 0x0000007B, 0x00000091, 0x000000A8, + 0x00000185, 0x000001BE, 0x000001FC, 0x0000023F, 0x00000288, 0x000002D6, 0x0000032A, 0x00000385, + 0x000003E5, 0x0000044C, 0x000004BA, 0x0000052E, 0x000005A9, 0x0000062C, 0x000006B5, 0x00000746, + 0x00000BCF, 0x00000CC0, 0x00000DBD, 0x00000EC6, 0x00000FDC, 0x000010FF, 0x0000122F, 0x0000136C, + 0x000014B6, 0x0000160F, 0x00001775, 0x000018EA, 0x00001A6D, 0x00001BFF, 0x00001DA0, 0x00001F51, + 0x00002C16, 0x00002E80, 0x00003100, 0x00003395, 0x00003641, 0x00003902, 0x00003BDB, 0x00003ECA, + 0x000041D0, 0x000044EE, 0x00004824, 0x00004B73, 0x00004ED9, 0x00005259, 0x000055F2, 0x000059A4, + 0x000074CC, 0x000079AB, 0x00007EAC, 0x000083CE, 0x00008911, 0x00008E77, 0x000093FF, 0x000099AA, + 0x00009F78, 0x0000A56A, 0x0000AB80, 0x0000B1BB, 0x0000B81A, 0x0000BE9E, 0x0000C547, 0x0000CC17, + 0x0000FD42, 0x000105CB, 0x00010E82, 0x00011768, 0x0001207E, 0x000129C4, 0x0001333B, 0x00013CE2, + 0x000146BB, 0x000150C5, 0x00015B02, 0x00016572, 0x00017015, 0x00017AEB, 0x000185F5, 0x00019133, + 0x0001E16D, 0x0001EF07, 0x0001FCE0, 0x00020AF7, 0x0002194F, 0x000227E6, 0x000236BE, 0x000245D7, + 0x00025532, 0x000264CF, 0x000274AE, 0x000284D0, 0x00029536, 0x0002A5E0, 0x0002B6CE, 0x0002C802, + 0x000341B0, 0x000355F8, 0x00036A90, 0x00037F79, 0x000394B4, 0x0003AA41, 0x0003C021, 0x0003D654, + 0x0003ECDA, 0x000403B5, 0x00041AE5, 0x0004326A, 0x00044A45, 0x00046277, 0x00047B00, 0x7FFFFFFF + }; + public static readonly byte[] FixedRests = new byte[0x10] + { + 96, 72, 64, 48, 36, 32, 24, 18, 16, 12, 9, 8, 6, 4, 3, 2 + }; + + public static bool IsStateRemovable(EnvelopeState state) + { + return state == EnvelopeState.Two || state >= EnvelopeState.Seven; + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Channel.cs b/VG Music Studio.backup/Core/NDS/SDAT/Channel.cs new file mode 100644 index 00000000..95b89010 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/Channel.cs @@ -0,0 +1,387 @@ +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class Channel + { + public readonly byte Index; + + public Track Owner; + public InstrumentType Type; + public EnvelopeState State; + public bool AutoSweep; + public byte BaseKey; + public byte Key; + public byte NoteVelocity; + public sbyte StartingPan; + public sbyte Pan; + public int SweepCounter; + public int SweepLength; + public short SweepPitch; + public int Velocity; // The SEQ Player treats 0 as the 100% amplitude value and -92544 (-723*128) as the 0% amplitude value. The starting ampltitude is 0% (-92544). + public byte Volume; // From 0x00-0x7F (Calculated from Utils) + public ushort BaseTimer; + public ushort Timer; + public int NoteDuration; + + private byte _attack; + private int _sustain; + private ushort _decay; + private ushort _release; + public byte LFORange; + public byte LFOSpeed; + public byte LFODepth; + public ushort LFODelay; + public ushort LFOPhase; + public int LFOParam; + public ushort LFODelayCount; + public LFOType LFOType; + public byte Priority; + + private int _pos; + private short _prevLeft; + private short _prevRight; + + // PCM8, PCM16, ADPCM + private SWAR.SWAV _swav; + // PCM8, PCM16 + private int _dataOffset; + // ADPCM + private ADPCMDecoder _adpcmDecoder; + private short _adpcmLoopLastSample; + private short _adpcmLoopStepIndex; + // PSG + private byte _psgDuty; + private int _psgCounter; + // Noise + private ushort _noiseCounter; + + public Channel(byte i) + { + Index = i; + } + + public void StartPCM(SWAR.SWAV swav, int noteDuration) + { + Type = InstrumentType.PCM; + _dataOffset = 0; + _swav = swav; + if (swav.Format == SWAVFormat.ADPCM) + { + _adpcmDecoder = new ADPCMDecoder(swav.Samples); + } + BaseTimer = swav.Timer; + Start(noteDuration); + } + public void StartPSG(byte duty, int noteDuration) + { + Type = InstrumentType.PSG; + _psgCounter = 0; + _psgDuty = duty; + BaseTimer = 8006; + Start(noteDuration); + } + public void StartNoise(int noteLength) + { + Type = InstrumentType.Noise; + _noiseCounter = 0x7FFF; + BaseTimer = 8006; + Start(noteLength); + } + + private void Start(int noteDuration) + { + State = EnvelopeState.Attack; + Velocity = -92544; + _pos = 0; + _prevLeft = _prevRight = 0; + NoteDuration = noteDuration; + } + + public void Stop() + { + if (Owner != null) + { + Owner.Channels.Remove(this); + } + Owner = null; + Volume = 0; + Priority = 0; + } + + public int SweepMain() + { + if (SweepPitch != 0 && SweepCounter < SweepLength) + { + int sweep = (int)(Math.BigMul(SweepPitch, SweepLength - SweepCounter) / SweepLength); + if (AutoSweep) + { + SweepCounter++; + } + return sweep; + } + else + { + return 0; + } + } + public void LFOTick() + { + if (LFODelayCount > 0) + { + LFODelayCount--; + LFOPhase = 0; + } + else + { + int param = LFORange * Utils.Sin(LFOPhase >> 8) * LFODepth; + if (LFOType == LFOType.Volume) + { + param = (param * 60) >> 14; + } + else + { + param >>= 8; + } + LFOParam = param; + int counter = LFOPhase + (LFOSpeed << 6); // "<< 6" is "* 0x40" + while (counter >= 0x8000) + { + counter -= 0x8000; + } + LFOPhase = (ushort)counter; + } + } + + public void SetAttack(int a) + { + _attack = Utils.AttackTable[a]; + } + public void SetDecay(int d) + { + _decay = Utils.DecayTable[d]; + } + public void SetSustain(byte s) + { + _sustain = Utils.SustainTable[s]; + } + public void SetRelease(int r) + { + _release = Utils.DecayTable[r]; + } + public void StepEnvelope() + { + switch (State) + { + case EnvelopeState.Attack: + { + Velocity = _attack * Velocity / 0xFF; + if (Velocity == 0) + { + State = EnvelopeState.Decay; + } + break; + } + case EnvelopeState.Decay: + { + Velocity -= _decay; + if (Velocity <= _sustain) + { + State = EnvelopeState.Sustain; + Velocity = _sustain; + } + break; + } + case EnvelopeState.Release: + { + Velocity -= _release; + if (Velocity < -92544) + { + Velocity = -92544; + } + break; + } + } + } + + /// EmulateProcess doesn't care about samples that loop; it only cares about ones that force the track to wait for them to end + public void EmulateProcess() + { + if (Timer != 0) + { + int numSamples = (_pos + 0x100) / Timer; + _pos = (_pos + 0x100) % Timer; + for (int i = 0; i < numSamples; i++) + { + if (Type == InstrumentType.PCM && !_swav.DoesLoop) + { + switch (_swav.Format) + { + case SWAVFormat.PCM8: + { + if (_dataOffset >= _swav.Samples.Length) + { + Stop(); + } + else + { + _dataOffset++; + } + return; + } + case SWAVFormat.PCM16: + { + if (_dataOffset >= _swav.Samples.Length) + { + Stop(); + } + else + { + _dataOffset += 2; + } + return; + } + case SWAVFormat.ADPCM: + { + if (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) + { + Stop(); + } + else + { + // This is a faster emulation of adpcmDecoder.GetSample() without caring about the sample + if (_adpcmDecoder.OnSecondNibble) + { + _adpcmDecoder.DataOffset++; + } + _adpcmDecoder.OnSecondNibble = !_adpcmDecoder.OnSecondNibble; + } + return; + } + } + } + } + } + } + public void Process(out short left, out short right) + { + if (Timer != 0) + { + int numSamples = (_pos + 0x100) / Timer; + _pos = (_pos + 0x100) % Timer; + // prevLeft and prevRight are stored because numSamples can be 0. + for (int i = 0; i < numSamples; i++) + { + short samp; + switch (Type) + { + case InstrumentType.PCM: + { + switch (_swav.Format) + { + case SWAVFormat.PCM8: + { + // If hit end + if (_dataOffset >= _swav.Samples.Length) + { + if (_swav.DoesLoop) + { + _dataOffset = _swav.LoopOffset * 4; + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = (short)((sbyte)_swav.Samples[_dataOffset++] << 8); + break; + } + case SWAVFormat.PCM16: + { + // If hit end + if (_dataOffset >= _swav.Samples.Length) + { + if (_swav.DoesLoop) + { + _dataOffset = _swav.LoopOffset * 4; + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = (short)(_swav.Samples[_dataOffset++] | (_swav.Samples[_dataOffset++] << 8)); + break; + } + case SWAVFormat.ADPCM: + { + // If just looped + if (_swav.DoesLoop && _adpcmDecoder.DataOffset == _swav.LoopOffset * 4 && !_adpcmDecoder.OnSecondNibble) + { + _adpcmLoopLastSample = _adpcmDecoder.LastSample; + _adpcmLoopStepIndex = _adpcmDecoder.StepIndex; + } + // If hit end + if (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) + { + if (_swav.DoesLoop) + { + _adpcmDecoder.DataOffset = _swav.LoopOffset * 4; + _adpcmDecoder.StepIndex = _adpcmLoopStepIndex; + _adpcmDecoder.LastSample = _adpcmLoopLastSample; + _adpcmDecoder.OnSecondNibble = false; + } + else + { + left = right = _prevLeft = _prevRight = 0; + Stop(); + return; + } + } + samp = _adpcmDecoder.GetSample(); + break; + } + default: samp = 0; break; + } + break; + } + case InstrumentType.PSG: + { + samp = _psgCounter <= _psgDuty ? short.MinValue : short.MaxValue; + _psgCounter++; + if (_psgCounter >= 8) + { + _psgCounter = 0; + } + break; + } + case InstrumentType.Noise: + { + if ((_noiseCounter & 1) != 0) + { + _noiseCounter = (ushort)((_noiseCounter >> 1) ^ 0x6000); + samp = -0x7FFF; + } + else + { + _noiseCounter = (ushort)(_noiseCounter >> 1); + samp = 0x7FFF; + } + break; + } + default: samp = 0; break; + } + samp = (short)(samp * Volume / 0x7F); + _prevLeft = (short)(samp * (-Pan + 0x40) / 0x80); + _prevRight = (short)(samp * (Pan + 0x40) / 0x80); + } + } + left = _prevLeft; + right = _prevRight; + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Commands.cs b/VG Music Studio.backup/Core/NDS/SDAT/Commands.cs new file mode 100644 index 00000000..7a83fe13 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/Commands.cs @@ -0,0 +1,439 @@ +using System; +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal abstract class SDATCommand + { + public bool RandMod { get; set; } + public bool VarMod { get; set; } + + protected string GetValues(int value, string ifNot) + { + return RandMod ? $"[{(short)value}, {(short)(value >> 16)}]" + : VarMod ? $"[{(byte)value}]" + : ifNot; + } + } + + internal class AllocTracksCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Alloc Tracks"; + public string Arguments => $"{Convert.ToString(Tracks, 2).PadLeft(16, '0')}b"; + + public ushort Tracks { get; set; } + } + internal class CallCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Call"; + public string Arguments => $"0x{Offset:X4}"; + + public int Offset { get; set; } + } + internal class FinishCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Finish"; + public string Arguments => string.Empty; + } + internal class ForceAttackCommand : SDATCommand, ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "Force Attack"; + public string Arguments => GetValues(Attack, Attack.ToString()); + + public int Attack { get; set; } + } + internal class ForceDecayCommand : SDATCommand, ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "Force Decay"; + public string Arguments => GetValues(Decay, Decay.ToString()); + + public int Decay { get; set; } + } + internal class ForceReleaseCommand : SDATCommand, ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "Force Release"; + public string Arguments => GetValues(Release, Release.ToString()); + + public int Release { get; set; } + } + internal class ForceSustainCommand : SDATCommand, ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "Force Sustain"; + public string Arguments => GetValues(Sustain, Sustain.ToString()); + + public int Sustain { get; set; } + } + internal class JumpCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Jump"; + public string Arguments => $"0x{Offset:X4}"; + + public int Offset { get; set; } + } + internal class LFODelayCommand : SDATCommand, ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Delay"; + public string Arguments => GetValues(Delay, Delay.ToString()); + + public int Delay { get; set; } + } + internal class LFODepthCommand : SDATCommand, ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Depth"; + public string Arguments => GetValues(Depth, Depth.ToString()); + + public int Depth { get; set; } + } + internal class LFORangeCommand : SDATCommand, ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Range"; + public string Arguments => GetValues(Range, Range.ToString()); + + public int Range { get; set; } + } + internal class LFOSpeedCommand : SDATCommand, ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Speed"; + public string Arguments => GetValues(Speed, Speed.ToString()); + + public int Speed { get; set; } + } + internal class LFOTypeCommand : SDATCommand, ICommand + { + public Color Color => Color.LightSteelBlue; + public string Label => "LFO Type"; + public string Arguments => GetValues(Type, Type.ToString()); + + public int Type { get; set; } + } + internal class LoopEndCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Loop End"; + public string Arguments => string.Empty; + } + internal class LoopStartCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Loop Start"; + public string Arguments => GetValues(NumLoops, NumLoops.ToString()); + + public int NumLoops { get; set; } + } + internal class ModIfCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "If Modifier"; + public string Arguments => string.Empty; + } + internal class ModRandCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Rand Modifier"; + public string Arguments => string.Empty; + } + internal class ModVarCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var Modifier"; + public string Arguments => string.Empty; + } + internal class MonophonyCommand : SDATCommand, ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Monophony Toggle"; + public string Arguments => GetValues(Mono, (Mono == 1).ToString()); + + public int Mono { get; set; } + } + internal class NoteComand : SDATCommand, ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Note"; + public string Arguments => $"{Util.Utils.GetNoteName(Key)}, {Velocity}, {GetValues(Duration, Duration.ToString())}"; + + public byte Key { get; set; } + public byte Velocity { get; set; } + public int Duration { get; set; } + } + internal class OpenTrackCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Open Track"; + public string Arguments => $"{Track}, 0x{Offset:X4}"; + + public int Track { get; set; } + public int Offset { get; set; } + } + internal class PanpotCommand : SDATCommand, ICommand + { + public Color Color => Color.GreenYellow; + public string Label => "Panpot"; + public string Arguments => GetValues(Panpot, Panpot.ToString()); + + public int Panpot { get; set; } + } + internal class PitchBendCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend"; + public string Arguments => GetValues(Bend, Bend.ToString()); + + public int Bend { get; set; } + } + internal class PitchBendRangeCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Pitch Bend Range"; + public string Arguments => GetValues(Range, Range.ToString()); + + public int Range { get; set; } + } + internal class PlayerVolumeCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Player Volume"; + public string Arguments => GetValues(Volume, Volume.ToString()); + + public int Volume { get; set; } + } + internal class PortamentoControlCommand : SDATCommand, ICommand + { + public Color Color => Color.HotPink; + public string Label => "Portamento Control"; + public string Arguments => GetValues(Portamento, Portamento.ToString()); + + public int Portamento { get; set; } + } + internal class PortamentoToggleCommand : SDATCommand, ICommand + { + public Color Color => Color.HotPink; + public string Label => "Portamento Toggle"; + public string Arguments => GetValues(Portamento, (Portamento == 1).ToString()); + + public int Portamento { get; set; } + } + internal class PortamentoTimeCommand : SDATCommand, ICommand + { + public Color Color => Color.HotPink; + public string Label => "Portamento Time"; + public string Arguments => GetValues(Time, Time.ToString()); + + public int Time { get; set; } + } + internal class PriorityCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Priority"; + public string Arguments => GetValues(Priority, Priority.ToString()); + + public int Priority { get; set; } + } + internal class RestCommand : SDATCommand, ICommand + { + public Color Color => Color.PaleVioletRed; + public string Label => "Rest"; + public string Arguments => GetValues(Rest, Rest.ToString()); + + public int Rest { get; set; } + } + internal class ReturnCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumSpringGreen; + public string Label => "Return"; + public string Arguments => string.Empty; + } + internal class SweepPitchCommand : SDATCommand, ICommand + { + public Color Color => Color.MediumPurple; + public string Label => "Sweep Pitch"; + public string Arguments => GetValues(Pitch, Pitch.ToString()); + + public int Pitch { get; set; } + } + internal class TempoCommand : SDATCommand, ICommand + { + public Color Color => Color.DeepSkyBlue; + public string Label => "Tempo"; + public string Arguments => GetValues(Tempo, Tempo.ToString()); + + public int Tempo { get; set; } + } + internal class TieCommand : SDATCommand, ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Tie"; + public string Arguments => GetValues(Tie, (Tie == 1).ToString()); + + public int Tie { get; set; } + } + internal class TrackExpressionCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Track Expression"; + public string Arguments => GetValues(Expression, Expression.ToString()); + + public int Expression { get; set; } + } + internal class TrackVolumeCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Track Volume"; + public string Arguments => GetValues(Volume, Volume.ToString()); + + public int Volume { get; set; } + } + internal class TransposeCommand : SDATCommand, ICommand + { + public Color Color => Color.SkyBlue; + public string Label => "Transpose"; + public string Arguments => GetValues(Transpose, Transpose.ToString()); + + public int Transpose { get; set; } + } + internal class VarAddCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var Add"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarCmpEECommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var =="; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarCmpGECommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var >="; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarCmpGGCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var >"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarCmpLECommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var <="; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarCmpLLCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var <"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarCmpNECommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var !="; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarDivCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var Div"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarMulCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var Mul"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarPrintCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var Print"; + public string Arguments => GetValues(Variable, Variable.ToString()); + + public int Variable { get; set; } + } + internal class VarRandCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var Rand"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarSetCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var Set"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarShiftCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var Shift"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VarSubCommand : SDATCommand, ICommand + { + public Color Color => Color.SteelBlue; + public string Label => "Var Sub"; + public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; + + public byte Variable { get; set; } + public int Argument { get; set; } + } + internal class VoiceCommand : SDATCommand, ICommand + { + public Color Color => Color.DarkSalmon; + public string Label => "Voice"; + public string Arguments => GetValues(Voice, Voice.ToString()); + + public int Voice { get; set; } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Config.cs b/VG Music Studio.backup/Core/NDS/SDAT/Config.cs new file mode 100644 index 00000000..f7f52813 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/Config.cs @@ -0,0 +1,40 @@ +using Kermalis.VGMusicStudio.Properties; +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class Config : Core.Config + { + public readonly SDAT SDAT; + + public Config(SDAT sdat) + { + if (sdat.INFOBlock.SequenceInfos.NumEntries == 0) + { + throw new Exception(Strings.ErrorSDATNoSequences); + } + SDAT = sdat; + var songs = new List(sdat.INFOBlock.SequenceInfos.NumEntries); + for (int i = 0; i < sdat.INFOBlock.SequenceInfos.NumEntries; i++) + { + if (sdat.INFOBlock.SequenceInfos.Entries[i] != null) + { + songs.Add(new Song(i, sdat.SYMBBlock is null ? i.ToString() : sdat.SYMBBlock.SequenceSymbols.Entries[i])); + } + } + Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); + } + + public override string GetGameName() + { + return "SDAT"; + } + public override string GetSongName(long index) + { + return SDAT.SYMBBlock is null || index < 0 || index >= SDAT.SYMBBlock.SequenceSymbols.NumEntries + ? index.ToString() + : '\"' + SDAT.SYMBBlock.SequenceSymbols.Entries[index] + '\"'; + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Enums.cs b/VG Music Studio.backup/Core/NDS/SDAT/Enums.cs new file mode 100644 index 00000000..9f4aa423 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/Enums.cs @@ -0,0 +1,40 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal enum EnvelopeState : byte + { + Attack, + Decay, + Sustain, + Release + } + internal enum ArgType : byte + { + None, + Byte, + Short, + VarLen, + Rand, + PlayerVar + } + + internal enum LFOType : byte + { + Pitch, + Volume, + Panpot + } + internal enum InstrumentType : byte + { + PCM = 0x1, + PSG = 0x2, + Noise = 0x3, + Drum = 0x10, + KeySplit = 0x11 + } + internal enum SWAVFormat : byte + { + PCM8, + PCM16, + ADPCM + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/FileHeader.cs b/VG Music Studio.backup/Core/NDS/SDAT/FileHeader.cs new file mode 100644 index 00000000..50b863e5 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/FileHeader.cs @@ -0,0 +1,31 @@ +using Kermalis.EndianBinaryIO; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class FileHeader : IBinarySerializable + { + public string FileType; + public ushort Endianness; + public ushort Version; + public int FileSize; + public ushort HeaderSize; // 16 + public ushort NumBlocks; + + public void Read(EndianBinaryReader er) + { + FileType = er.ReadString(4, false); + er.Endianness = EndianBinaryIO.Endianness.BigEndian; + Endianness = er.ReadUInt16(); + er.Endianness = Endianness == 0xFFFE ? EndianBinaryIO.Endianness.LittleEndian : EndianBinaryIO.Endianness.BigEndian; + Version = er.ReadUInt16(); + FileSize = er.ReadInt32(); + HeaderSize = er.ReadUInt16(); + NumBlocks = er.ReadUInt16(); + } + public void Write(EndianBinaryWriter ew) + { + throw new NotImplementedException(); + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs b/VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs new file mode 100644 index 00000000..a42b4e43 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs @@ -0,0 +1,252 @@ +using NAudio.Wave; +using System; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class Mixer : Core.Mixer + { + private readonly float _samplesReciprocal; + private readonly int _samplesPerBuffer; + private bool _isFading; + private long _fadeMicroFramesLeft; + private float _fadePos; + private float _fadeStepPerMicroframe; + + public Channel[] Channels; + private readonly BufferedWaveProvider _buffer; + + public Mixer() + { + // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. + // - gbatek + // I'm not using either of those because the samples per buffer leads to an overflow eventually + const int sampleRate = 65456; + _samplesPerBuffer = 341; // TODO + _samplesReciprocal = 1f / _samplesPerBuffer; + + Channels = new Channel[0x10]; + for (byte i = 0; i < 0x10; i++) + { + Channels[i] = new Channel(i); + } + + _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) + { + DiscardOnBufferOverflow = true, + BufferLength = _samplesPerBuffer * 64 + }; + Init(_buffer); + } + public override void Dispose() + { + base.Dispose(); + CloseWaveWriter(); + } + + private static readonly int[] _pcmChanOrder = new int[] { 4, 5, 6, 7, 2, 0, 3, 1, 8, 9, 10, 11, 14, 12, 15, 13 }; + private static readonly int[] _psgChanOrder = new int[] { 8, 9, 10, 11, 12, 13 }; + private static readonly int[] _noiseChanOrder = new int[] { 14, 15 }; + public Channel AllocateChannel(InstrumentType type, Track track) + { + int[] allowedChannels; + switch (type) + { + case InstrumentType.PCM: allowedChannels = _pcmChanOrder; break; + case InstrumentType.PSG: allowedChannels = _psgChanOrder; break; + case InstrumentType.Noise: allowedChannels = _noiseChanOrder; break; + default: return null; + } + Channel nChan = null; + for (int i = 0; i < allowedChannels.Length; i++) + { + Channel c = Channels[allowedChannels[i]]; + if (nChan != null && c.Priority >= nChan.Priority && (c.Priority != nChan.Priority || nChan.Volume <= c.Volume)) + { + continue; + } + nChan = c; + } + if (nChan == null || track.Priority < nChan.Priority) + { + return null; + } + return nChan; + } + + public void ChannelTick() + { + for (int i = 0; i < 0x10; i++) + { + Channel chan = Channels[i]; + if (chan.Owner != null) + { + chan.StepEnvelope(); + if (chan.NoteDuration == 0 && !chan.Owner.WaitingForNoteToFinishBeforeContinuingXD) + { + chan.Priority = 1; + chan.State = EnvelopeState.Release; + } + int vol = Utils.SustainTable[chan.NoteVelocity] + chan.Velocity + chan.Owner.GetVolume(); + int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" + int pan = 0; + chan.LFOTick(); + switch (chan.LFOType) + { + case LFOType.Pitch: pitch += chan.LFOParam; break; + case LFOType.Volume: vol += chan.LFOParam; break; + case LFOType.Panpot: pan += chan.LFOParam; break; + } + if (chan.State == EnvelopeState.Release && vol <= -92544) + { + chan.Stop(); + } + else + { + chan.Volume = Utils.GetChannelVolume(vol); + chan.Timer = Utils.GetChannelTimer(chan.BaseTimer, pitch); + int p = chan.StartingPan + chan.Owner.GetPan() + pan; + if (p < -0x40) + { + p = -0x40; + } + else if (p > 0x3F) + { + p = 0x3F; + } + chan.Pan = (sbyte)p; + } + } + } + } + + public void BeginFadeIn() + { + _fadePos = 0f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; + _isFading = true; + } + public void BeginFadeOut() + { + _fadePos = 1f; + _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); + _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; + _isFading = true; + } + public bool IsFading() + { + return _isFading; + } + public bool IsFadeDone() + { + return _isFading && _fadeMicroFramesLeft == 0; + } + public void ResetFade() + { + _isFading = false; + _fadeMicroFramesLeft = 0; + } + + private WaveFileWriter _waveWriter; + public void CreateWaveWriter(string fileName) + { + _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); + } + public void CloseWaveWriter() + { + _waveWriter?.Dispose(); + } + public void EmulateProcess() + { + for (int i = 0; i < _samplesPerBuffer; i++) + { + for (int j = 0; j < 0x10; j++) + { + Channel chan = Channels[j]; + if (chan.Owner != null) + { + chan.EmulateProcess(); + } + } + } + } + public void Process(bool output, bool recording) + { + float masterStep; + float masterLevel; + if (_isFading && _fadeMicroFramesLeft == 0) + { + masterStep = 0; + masterLevel = 0; + } + else + { + float fromMaster = 1f; + float toMaster = 1f; + if (_fadeMicroFramesLeft > 0) + { + const float scale = 10f / 6f; + fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadePos += _fadeStepPerMicroframe; + toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); + _fadeMicroFramesLeft--; + } + masterStep = (toMaster - fromMaster) * _samplesReciprocal; + masterLevel = fromMaster; + } + byte[] b = new byte[4]; + for (int i = 0; i < _samplesPerBuffer; i++) + { + int left = 0, + right = 0; + for (int j = 0; j < 0x10; j++) + { + Channel chan = Channels[j]; + if (chan.Owner != null) + { + bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null + chan.Process(out short channelLeft, out short channelRight); + if (!muted) + { + left += channelLeft; + right += channelRight; + } + } + } + float f = left * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + left = (int)f; + b[0] = (byte)left; + b[1] = (byte)(left >> 8); + f = right * masterLevel; + if (f < short.MinValue) + { + f = short.MinValue; + } + else if (f > short.MaxValue) + { + f = short.MaxValue; + } + right = (int)f; + b[2] = (byte)right; + b[3] = (byte)(right >> 8); + masterLevel += masterStep; + if (output) + { + _buffer.AddSamples(b, 0, 4); + } + if (recording) + { + _waveWriter.Write(b, 0, 4); + } + } + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Player.cs b/VG Music Studio.backup/Core/NDS/SDAT/Player.cs new file mode 100644 index 00000000..2722786f --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/Player.cs @@ -0,0 +1,1680 @@ +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class Player : IPlayer + { + public readonly byte Priority = 0x40; + private readonly short[] _vars = new short[0x20]; // 16 player variables, then 16 global variables + private readonly Track[] _tracks = new Track[0x10]; + private readonly Mixer _mixer; + private readonly Config _config; + private readonly TimeBarrier _time; + private Thread _thread; + private int _randSeed; + private Random _rand; + private SDAT.INFO.SequenceInfo _seqInfo; + private SSEQ _sseq; + private SBNK _sbnk; + public byte Volume; + private ushort _tempo; + private int _tempoStack; + private long _elapsedLoops; + + public List[] Events { get; private set; } + public long MaxTicks { get; private set; } + public long ElapsedTicks { get; private set; } + public bool ShouldFadeOut { get; set; } + public long NumLoops { get; set; } + private int _longestTrack; + + public PlayerState State { get; private set; } + public event SongEndedEvent SongEnded; + + public Player(Mixer mixer, Config config) + { + for (byte i = 0; i < 0x10; i++) + { + _tracks[i] = new Track(i, this); + } + _mixer = mixer; + _config = config; + + _time = new TimeBarrier(192); + } + private void CreateThread() + { + _thread = new Thread(Tick) { Name = "SDAT Player Tick" }; + _thread.Start(); + } + private void WaitThread() + { + if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) + { + _thread.Join(); + } + } + + private void InitEmulation() + { + _tempo = 120; // Confirmed: default tempo is 120 (MKDS 75) + _tempoStack = 0; + _elapsedLoops = 0; + ElapsedTicks = 0; + _mixer.ResetFade(); + Volume = _seqInfo.Volume; + _rand = new Random(_randSeed); + for (int i = 0; i < 0x10; i++) + { + _tracks[i].Init(); + } + // Initialize player and global variables. Global variables should not have a global effect in this program. + for (int i = 0; i < 0x20; i++) + { + _vars[i] = i % 8 == 0 ? short.MaxValue : (short)0; + } + } + private void SetTicks() + { + // TODO: (NSMB 81) (Spirit Tracks 18) does not count all ticks because the songs keep jumping backwards while changing vars and then using ModIfCommand to change events + MaxTicks = 0; + for (int i = 0; i < 0x10; i++) + { + if (Events[i] != null) + { + Events[i] = Events[i].OrderBy(e => e.Offset).ToList(); + } + } + InitEmulation(); + bool[] done = new bool[0x10]; // We use this instead of track.Stopped just to be certain that emulating Monophony works as intended + while (_tracks.Any(t => t.Allocated && t.Enabled && !done[t.Index])) + { + while (_tempoStack >= 240) + { + _tempoStack -= 240; + for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) + { + Track track = _tracks[trackIndex]; + List evs = Events[trackIndex]; + if (track.Enabled && !track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) + { + SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); + ExecuteNext(track); + if (!done[trackIndex]) + { + e.Ticks.Add(ElapsedTicks); + bool b; + if (track.Stopped) + { + b = true; + } + else + { + SongEvent newE = evs.Single(ev => ev.Offset == track.DataOffset); + b = (track.CallStackDepth == 0 && newE.Ticks.Count > 0) // If we already counted the tick of this event and we're not looping/calling + || (track.CallStackDepth != 0 && track.CallStackLoops.All(l => l == 0) && newE.Ticks.Count > 0); // If we have "LoopStart (0)" and already counted the tick of this event + } + if (b) + { + done[trackIndex] = true; + if (ElapsedTicks > MaxTicks) + { + _longestTrack = trackIndex; + MaxTicks = ElapsedTicks; + } + } + } + } + } + } + ElapsedTicks++; + } + _tempoStack += _tempo; + _mixer.ChannelTick(); + _mixer.EmulateProcess(); + } + for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) + { + _tracks[trackIndex].StopAllChannels(); + } + } + public void LoadSong(long index) + { + Stop(); + SDAT.INFO.SequenceInfo oldSeqInfo = _seqInfo; + _seqInfo = _config.SDAT.INFOBlock.SequenceInfos.Entries[index]; + if (_seqInfo == null) + { + _sseq = null; + _sbnk = null; + Events = null; + } + else + { + if (oldSeqInfo == null || _seqInfo.Bank != oldSeqInfo.Bank) + { + _voiceTypeCache = new string[byte.MaxValue + 1]; + } + _sseq = new SSEQ(_config.SDAT.FATBlock.Entries[_seqInfo.FileId].Data); + SDAT.INFO.BankInfo bankInfo = _config.SDAT.INFOBlock.BankInfos.Entries[_seqInfo.Bank]; + _sbnk = new SBNK(_config.SDAT.FATBlock.Entries[bankInfo.FileId].Data); + for (int i = 0; i < 4; i++) + { + if (bankInfo.SWARs[i] != 0xFFFF) + { + _sbnk.SWARs[i] = new SWAR(_config.SDAT.FATBlock.Entries[_config.SDAT.INFOBlock.WaveArchiveInfos.Entries[bankInfo.SWARs[i]].FileId].Data); + } + } + _randSeed = new Random().Next(); + + // RECURSION INCOMING + Events = new List[0x10]; + AddTrackEvents(0, 0); + void AddTrackEvents(int i, int trackStartOffset) + { + if (Events[i] == null) + { + Events[i] = new List(); + } + int callStackDepth = 0; + AddEvents(trackStartOffset); + bool EventExists(long offset) + { + return Events[i].Any(e => e.Offset == offset); + } + void AddEvents(int startOffset) + { + int dataOffset = startOffset; + int ReadArg(ArgType type) + { + switch (type) + { + case ArgType.Byte: + { + return _sseq.Data[dataOffset++]; + } + case ArgType.Short: + { + return _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8); + } + case ArgType.VarLen: + { + int read = 0, value = 0; + byte b; + do + { + b = _sseq.Data[dataOffset++]; + value = (value << 7) | (b & 0x7F); + read++; + } + while (read < 4 && (b & 0x80) != 0); + return value; + } + case ArgType.Rand: + { + // Combine min and max into one int + return _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16) | (_sseq.Data[dataOffset++] << 24); + } + case ArgType.PlayerVar: + { + // Return var index + return _sseq.Data[dataOffset++]; + } + default: throw new Exception(); + } + } + bool cont = true; + while (cont) + { + bool @if = false; + int offset = dataOffset; + ArgType argOverrideType = ArgType.None; + again: + byte cmd = _sseq.Data[dataOffset++]; + void AddEvent(T command) where T : SDATCommand, ICommand + { + command.RandMod = argOverrideType == ArgType.Rand; + command.VarMod = argOverrideType == ArgType.PlayerVar; + Events[i].Add(new SongEvent(offset, command)); + } + void Invalid() + { + throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, i, offset, cmd)); + } + + if (cmd <= 0x7F) + { + byte velocity = _sseq.Data[dataOffset++]; + int duration = ReadArg(argOverrideType == ArgType.None ? ArgType.VarLen : argOverrideType); + if (!EventExists(offset)) + { + AddEvent(new NoteComand { Key = cmd, Velocity = velocity, Duration = duration }); + } + } + else + { + int cmdGroup = cmd & 0xF0; + if (cmdGroup == 0x80) + { + int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.VarLen : argOverrideType); + switch (cmd) + { + case 0x80: + { + if (!EventExists(offset)) + { + AddEvent(new RestCommand { Rest = arg }); + } + break; + } + case 0x81: // RAND PROGRAM: [BW2 (2249)] + { + if (!EventExists(offset)) + { + AddEvent(new VoiceCommand { Voice = arg }); // TODO: Bank change + } + break; + } + default: Invalid(); break; + } + } + else if (cmdGroup == 0x90) + { + switch (cmd) + { + case 0x93: + { + int trackIndex = _sseq.Data[dataOffset++]; + int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); + if (!EventExists(offset)) + { + AddEvent(new OpenTrackCommand { Track = trackIndex, Offset = offset24bit }); + AddTrackEvents(trackIndex, offset24bit); + } + break; + } + case 0x94: + { + int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); + if (!EventExists(offset)) + { + AddEvent(new JumpCommand { Offset = offset24bit }); + if (!EventExists(offset24bit)) + { + AddEvents(offset24bit); + } + } + if (!@if) + { + cont = false; + } + break; + } + case 0x95: + { + int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); + if (!EventExists(offset)) + { + AddEvent(new CallCommand { Offset = offset24bit }); + } + if (callStackDepth < 3) + { + if (!EventExists(offset24bit)) + { + callStackDepth++; + AddEvents(offset24bit); + } + } + else + { + throw new Exception(string.Format(Strings.ErrorMP2KSDATNestedCalls, i)); + } + break; + } + default: Invalid(); break; + } + } + else if (cmdGroup == 0xA0) + { + switch (cmd) + { + case 0xA0: // [New Super Mario Bros (BGM_AMB_CHIKA)] [BW2 (1917, 1918)] + { + if (!EventExists(offset)) + { + AddEvent(new ModRandCommand()); + } + argOverrideType = ArgType.Rand; + offset++; + goto again; + } + case 0xA1: // [New Super Mario Bros (BGM_AMB_SABAKU)] + { + if (!EventExists(offset)) + { + AddEvent(new ModVarCommand()); + } + argOverrideType = ArgType.PlayerVar; + offset++; + goto again; + } + case 0xA2: // [Mario Kart DS (75)] [BW2 (1917, 1918)] + { + if (!EventExists(offset)) + { + AddEvent(new ModIfCommand()); + } + @if = true; + offset++; + goto again; + } + default: Invalid(); break; + } + } + else if (cmdGroup == 0xB0) + { + byte varIndex = _sseq.Data[dataOffset++]; + int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.Short : argOverrideType); + switch (cmd) + { + case 0xB0: + { + if (!EventExists(offset)) + { + AddEvent(new VarSetCommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xB1: + { + if (!EventExists(offset)) + { + AddEvent(new VarAddCommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xB2: + { + if (!EventExists(offset)) + { + AddEvent(new VarSubCommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xB3: + { + if (!EventExists(offset)) + { + AddEvent(new VarMulCommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xB4: + { + if (!EventExists(offset)) + { + AddEvent(new VarDivCommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xB5: + { + if (!EventExists(offset)) + { + AddEvent(new VarShiftCommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xB6: // [Mario Kart DS (75)] + { + if (!EventExists(offset)) + { + AddEvent(new VarRandCommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xB8: + { + if (!EventExists(offset)) + { + AddEvent(new VarCmpEECommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xB9: + { + if (!EventExists(offset)) + { + AddEvent(new VarCmpGECommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xBA: + { + if (!EventExists(offset)) + { + AddEvent(new VarCmpGGCommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xBB: + { + if (!EventExists(offset)) + { + AddEvent(new VarCmpLECommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xBC: + { + if (!EventExists(offset)) + { + AddEvent(new VarCmpLLCommand { Variable = varIndex, Argument = arg }); + } + break; + } + case 0xBD: + { + if (!EventExists(offset)) + { + AddEvent(new VarCmpNECommand { Variable = varIndex, Argument = arg }); + } + break; + } + default: Invalid(); break; + } + } + else if (cmdGroup == 0xC0 || cmdGroup == 0xD0) + { + int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.Byte : argOverrideType); + switch (cmd) + { + case 0xC0: + { + if (!EventExists(offset)) + { + AddEvent(new PanpotCommand { Panpot = arg }); + } + break; + } + case 0xC1: + { + if (!EventExists(offset)) + { + AddEvent(new TrackVolumeCommand { Volume = arg }); + } + break; + } + case 0xC2: + { + if (!EventExists(offset)) + { + AddEvent(new PlayerVolumeCommand { Volume = arg }); + } + break; + } + case 0xC3: + { + if (!EventExists(offset)) + { + AddEvent(new TransposeCommand { Transpose = arg }); + } + break; + } + case 0xC4: + { + if (!EventExists(offset)) + { + AddEvent(new PitchBendCommand { Bend = arg }); + } + break; + } + case 0xC5: + { + if (!EventExists(offset)) + { + AddEvent(new PitchBendRangeCommand { Range = arg }); + } + break; + } + case 0xC6: + { + if (!EventExists(offset)) + { + AddEvent(new PriorityCommand { Priority = arg }); + } + break; + } + case 0xC7: + { + if (!EventExists(offset)) + { + AddEvent(new MonophonyCommand { Mono = arg }); + } + break; + } + case 0xC8: + { + if (!EventExists(offset)) + { + AddEvent(new TieCommand { Tie = arg }); + } + break; + } + case 0xC9: + { + if (!EventExists(offset)) + { + AddEvent(new PortamentoControlCommand { Portamento = arg }); + } + break; + } + case 0xCA: + { + if (!EventExists(offset)) + { + AddEvent(new LFODepthCommand { Depth = arg }); + } + break; + } + case 0xCB: + { + if (!EventExists(offset)) + { + AddEvent(new LFOSpeedCommand { Speed = arg }); + } + break; + } + case 0xCC: + { + if (!EventExists(offset)) + { + AddEvent(new LFOTypeCommand { Type = arg }); + } + break; + } + case 0xCD: + { + if (!EventExists(offset)) + { + AddEvent(new LFORangeCommand { Range = arg }); + } + break; + } + case 0xCE: + { + if (!EventExists(offset)) + { + AddEvent(new PortamentoToggleCommand { Portamento = arg }); + } + break; + } + case 0xCF: + { + if (!EventExists(offset)) + { + AddEvent(new PortamentoTimeCommand { Time = arg }); + } + break; + } + case 0xD0: + { + if (!EventExists(offset)) + { + AddEvent(new ForceAttackCommand { Attack = arg }); + } + break; + } + case 0xD1: + { + if (!EventExists(offset)) + { + AddEvent(new ForceDecayCommand { Decay = arg }); + } + break; + } + case 0xD2: + { + if (!EventExists(offset)) + { + AddEvent(new ForceSustainCommand { Sustain = arg }); + } + break; + } + case 0xD3: + { + if (!EventExists(offset)) + { + AddEvent(new ForceReleaseCommand { Release = arg }); + } + break; + } + case 0xD4: + { + if (!EventExists(offset)) + { + AddEvent(new LoopStartCommand { NumLoops = arg }); + } + break; + } + case 0xD5: + { + if (!EventExists(offset)) + { + AddEvent(new TrackExpressionCommand { Expression = arg }); + } + break; + } + case 0xD6: + { + if (!EventExists(offset)) + { + AddEvent(new VarPrintCommand { Variable = arg }); + } + break; + } + default: Invalid(); break; + } + } + else if (cmdGroup == 0xE0) + { + int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.Short : argOverrideType); + switch (cmd) + { + case 0xE0: + { + if (!EventExists(offset)) + { + AddEvent(new LFODelayCommand { Delay = arg }); + } + break; + } + case 0xE1: + { + if (!EventExists(offset)) + { + AddEvent(new TempoCommand { Tempo = arg }); + } + break; + } + case 0xE3: + { + if (!EventExists(offset)) + { + AddEvent(new SweepPitchCommand { Pitch = arg }); + } + break; + } + default: Invalid(); break; + } + } + else // if (cmdGroup == 0xF0) + { + switch (cmd) + { + case 0xFC: // [HGSS(1353)] + { + if (!EventExists(offset)) + { + AddEvent(new LoopEndCommand()); + } + break; + } + case 0xFD: + { + if (!EventExists(offset)) + { + AddEvent(new ReturnCommand()); + } + if (!@if && callStackDepth != 0) + { + cont = false; + callStackDepth--; + } + break; + } + case 0xFE: + { + ushort bits = (ushort)ReadArg(ArgType.Short); + if (!EventExists(offset)) + { + AddEvent(new AllocTracksCommand { Tracks = bits }); + } + break; + } + case 0xFF: + { + if (!EventExists(offset)) + { + AddEvent(new FinishCommand()); + } + if (!@if) + { + cont = false; + } + break; + } + default: Invalid(); break; + } + } + } + } + } + } + SetTicks(); + } + } + public void SetCurrentPosition(long ticks) + { + if (_seqInfo == null) + { + SongEnded?.Invoke(); + } + else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + if (State == PlayerState.Playing) + { + Pause(); + } + InitEmulation(); + while (true) + { + if (ElapsedTicks == ticks) + { + goto finish; + } + else + { + while (_tempoStack >= 240) + { + _tempoStack -= 240; + for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) + { + Track track = _tracks[trackIndex]; + if (track.Enabled && !track.Stopped) + { + track.Tick(); + while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) + { + ExecuteNext(track); + } + } + } + ElapsedTicks++; + if (ElapsedTicks == ticks) + { + goto finish; + } + } + _tempoStack += _tempo; + _mixer.ChannelTick(); + _mixer.EmulateProcess(); + } + } + finish: + for (int i = 0; i < 0x10; i++) + { + _tracks[i].StopAllChannels(); + } + Pause(); + } + } + public void Play() + { + if (_seqInfo == null) + { + SongEnded?.Invoke(); + } + else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + Stop(); + InitEmulation(); + State = PlayerState.Playing; + CreateThread(); + } + } + public void Pause() + { + if (State == PlayerState.Playing) + { + State = PlayerState.Paused; + WaitThread(); + } + else if (State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.Playing; + CreateThread(); + } + } + public void Stop() + { + if (State == PlayerState.Playing || State == PlayerState.Paused) + { + State = PlayerState.Stopped; + WaitThread(); + } + } + public void Record(string fileName) + { + _mixer.CreateWaveWriter(fileName); + InitEmulation(); + State = PlayerState.Recording; + CreateThread(); + WaitThread(); + _mixer.CloseWaveWriter(); + } + public void Dispose() + { + if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) + { + State = PlayerState.ShutDown; + WaitThread(); + } + } + private string[] _voiceTypeCache; + public void GetSongState(UI.SongInfoControl.SongInfo info) + { + info.Tempo = _tempo; + for (int i = 0; i < 0x10; i++) + { + Track track = _tracks[i]; + if (track.Enabled) + { + UI.SongInfoControl.SongInfo.Track tin = info.Tracks[i]; + tin.Position = track.DataOffset; + tin.Rest = track.Rest; + tin.Voice = track.Voice; + tin.LFO = track.LFODepth * track.LFORange; + if (_voiceTypeCache[track.Voice] == null) + { + if (_sbnk.NumInstruments <= track.Voice) + { + _voiceTypeCache[track.Voice] = "Empty"; + } + else + { + InstrumentType t = _sbnk.Instruments[track.Voice].Type; + switch (t) + { + case InstrumentType.PCM: _voiceTypeCache[track.Voice] = "PCM"; break; + case InstrumentType.PSG: _voiceTypeCache[track.Voice] = "PSG"; break; + case InstrumentType.Noise: _voiceTypeCache[track.Voice] = "Noise"; break; + case InstrumentType.Drum: _voiceTypeCache[track.Voice] = "Drum"; break; + case InstrumentType.KeySplit: _voiceTypeCache[track.Voice] = "Key Split"; break; + default: _voiceTypeCache[track.Voice] = string.Format("Invalid {0}", (byte)t); break; + } + } + } + tin.Type = _voiceTypeCache[track.Voice]; + tin.Volume = track.Volume; + tin.PitchBend = track.GetPitch(); + tin.Extra = track.Portamento ? track.PortamentoTime : (byte)0; + tin.Panpot = track.GetPan(); + + Channel[] channels = track.Channels.ToArray(); + if (channels.Length == 0) + { + tin.Keys[0] = byte.MaxValue; + tin.LeftVolume = 0f; + tin.RightVolume = 0f; + } + else + { + int numKeys = 0; + float left = 0f; + float right = 0f; + for (int j = 0; j < channels.Length; j++) + { + Channel c = channels[j]; + if (c.State != EnvelopeState.Release) + { + tin.Keys[numKeys++] = c.Key; + } + float a = (float)(-c.Pan + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > left) + { + left = a; + } + a = (float)(c.Pan + 0x40) / 0x80 * c.Volume / 0x7F; + if (a > right) + { + right = a; + } + } + tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array + tin.LeftVolume = left; + tin.RightVolume = right; + } + } + } + } + + public void PlayNote(Track track, byte key, byte velocity, int duration) + { + Channel channel = null; + if (track.Tie && track.Channels.Count != 0) + { + channel = track.Channels.Last(); + channel.Key = key; + channel.NoteVelocity = velocity; + } + else + { + SBNK.InstrumentData inst = _sbnk.GetInstrumentData(track.Voice, key); + if (inst != null) + { + InstrumentType type = inst.Type; + channel = _mixer.AllocateChannel(type, track); + if (channel != null) + { + if (track.Tie) + { + duration = -1; + } + SBNK.InstrumentData.DataParam param = inst.Param; + byte release = param.Release; + if (release == 0xFF) + { + duration = -1; + release = 0; + } + bool started = false; + switch (type) + { + case InstrumentType.PCM: + { + ushort[] info = param.Info; + SWAR.SWAV swav = _sbnk.GetSWAV(info[1], info[0]); + if (swav != null) + { + channel.StartPCM(swav, duration); + started = true; + } + break; + } + case InstrumentType.PSG: + { + channel.StartPSG((byte)param.Info[0], duration); + started = true; + break; + } + case InstrumentType.Noise: + { + channel.StartNoise(duration); + started = true; + break; + } + } + channel.Stop(); + if (started) + { + channel.Key = key; + byte baseKey = param.BaseKey; + channel.BaseKey = type != InstrumentType.PCM && baseKey == 0x7F ? (byte)60 : baseKey; + channel.NoteVelocity = velocity; + channel.SetAttack(param.Attack); + channel.SetDecay(param.Decay); + channel.SetSustain(param.Sustain); + channel.SetRelease(release); + channel.StartingPan = (sbyte)(param.Pan - 0x40); + channel.Owner = track; + channel.Priority = track.Priority; + track.Channels.Add(channel); + } + else + { + return; + } + } + } + } + if (channel != null) + { + if (track.Attack != 0xFF) + { + channel.SetAttack(track.Attack); + } + if (track.Decay != 0xFF) + { + channel.SetDecay(track.Decay); + } + if (track.Sustain != 0xFF) + { + channel.SetSustain(track.Sustain); + } + if (track.Release != 0xFF) + { + channel.SetRelease(track.Release); + } + channel.SweepPitch = track.SweepPitch; + if (track.Portamento) + { + channel.SweepPitch += (short)((track.PortamentoKey - key) << 6); // "<< 6" is "* 0x40" + } + if (track.PortamentoTime != 0) + { + channel.SweepLength = (track.PortamentoTime * track.PortamentoTime * Math.Abs(channel.SweepPitch)) >> 11; // ">> 11" is "/ 0x800" + channel.AutoSweep = true; + } + else + { + channel.SweepLength = duration; + channel.AutoSweep = false; + } + channel.SweepCounter = 0; + } + } + private void ExecuteNext(Track track) + { + int ReadArg(ArgType type) + { + if (track.ArgOverrideType != ArgType.None) + { + type = track.ArgOverrideType; + } + switch (type) + { + case ArgType.Byte: + { + return _sseq.Data[track.DataOffset++]; + } + case ArgType.Short: + { + return _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8); + } + case ArgType.VarLen: + { + int read = 0, value = 0; + byte b; + do + { + b = _sseq.Data[track.DataOffset++]; + value = (value << 7) | (b & 0x7F); + read++; + } + while (read < 4 && (b & 0x80) != 0); + return value; + } + case ArgType.Rand: + { + short min = (short)(_sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8)); + short max = (short)(_sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8)); + return _rand.Next(min, max + 1); + } + case ArgType.PlayerVar: + { + byte varIndex = _sseq.Data[track.DataOffset++]; + return _vars[varIndex]; + } + default: throw new Exception(); + } + } + + bool resetOverride = true; + bool resetCmdWork = true; + byte cmd = _sseq.Data[track.DataOffset++]; + if (cmd < 0x80) // Notes + { + byte velocity = _sseq.Data[track.DataOffset++]; + int duration = ReadArg(ArgType.VarLen); + if (track.DoCommandWork) + { + int k = cmd + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + byte key = (byte)k; + PlayNote(track, key, velocity, duration); + track.PortamentoKey = key; + if (track.Mono) + { + track.Rest = duration; + if (duration == 0) + { + track.WaitingForNoteToFinishBeforeContinuingXD = true; + } + } + } + } + else + { + int cmdGroup = cmd & 0xF0; + switch (cmdGroup) + { + case 0x80: + { + int arg = ReadArg(ArgType.VarLen); + if (track.DoCommandWork) + { + switch (cmd) + { + case 0x80: // Rest + { + track.Rest = arg; + break; + } + case 0x81: // Program Change + { + if (arg <= byte.MaxValue) + { + track.Voice = (byte)arg; + } + break; + } + } + } + break; + } + case 0x90: + { + switch (cmd) + { + case 0x93: // Open Track + { + int index = _sseq.Data[track.DataOffset++]; + int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); + if (track.DoCommandWork && track.Index == 0) + { + Track other = _tracks[index]; + if (other.Allocated && !other.Enabled) + { + other.Enabled = true; + other.DataOffset = offset24bit; + } + } + break; + } + case 0x94: // Jump + { + int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); + if (track.DoCommandWork) + { + track.DataOffset = offset24bit; + } + break; + } + case 0x95: // Call + { + int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); + if (track.DoCommandWork && track.CallStackDepth < 3) + { + track.CallStack[track.CallStackDepth] = track.DataOffset; + track.CallStackLoops[track.CallStackDepth] = byte.MaxValue; // This is only necessary for SetTicks() to deal with LoopStart (0) + track.CallStackDepth++; + track.DataOffset = offset24bit; + } + break; + } + } + break; + } + case 0xA0: + { + if (track.DoCommandWork) + { + switch (cmd) + { + case 0xA0: // Rand Mod + { + track.ArgOverrideType = ArgType.Rand; + resetOverride = false; + break; + } + case 0xA1: // Var Mod + { + track.ArgOverrideType = ArgType.PlayerVar; + resetOverride = false; + break; + } + case 0xA2: // If Mod + { + track.DoCommandWork = track.VariableFlag; + resetCmdWork = false; + break; + } + } + } + break; + } + case 0xB0: + { + byte varIndex = _sseq.Data[track.DataOffset++]; + short mathArg = (short)ReadArg(ArgType.Short); + if (track.DoCommandWork) + { + switch (cmd) + { + case 0xB0: // VarSet + { + _vars[varIndex] = mathArg; + break; + } + case 0xB1: // VarAdd + { + _vars[varIndex] += mathArg; + break; + } + case 0xB2: // VarSub + { + _vars[varIndex] -= mathArg; + break; + } + case 0xB3: // VarMul + { + _vars[varIndex] *= mathArg; + break; + } + case 0xB4: // VarDiv + { + if (mathArg != 0) + { + _vars[varIndex] /= mathArg; + } + break; + } + case 0xB5: // VarShift + { + _vars[varIndex] = mathArg < 0 ? (short)(_vars[varIndex] >> -mathArg) : (short)(_vars[varIndex] << mathArg); + break; + } + case 0xB6: // VarRand + { + bool negate = false; + if (mathArg < 0) + { + negate = true; + mathArg = (short)-mathArg; + } + short val = (short)_rand.Next(mathArg + 1); + if (negate) + { + val = (short)-val; + } + _vars[varIndex] = val; + break; + } + case 0xB8: // VarCmpEE + { + track.VariableFlag = _vars[varIndex] == mathArg; + break; + } + case 0xB9: // VarCmpGE + { + track.VariableFlag = _vars[varIndex] >= mathArg; + break; + } + case 0xBA: // VarCmpGG + { + track.VariableFlag = _vars[varIndex] > mathArg; + break; + } + case 0xBB: // VarCmpLE + { + track.VariableFlag = _vars[varIndex] <= mathArg; + break; + } + case 0xBC: // VarCmpLL + { + track.VariableFlag = _vars[varIndex] < mathArg; + break; + } + case 0xBD: // VarCmpNE + { + track.VariableFlag = _vars[varIndex] != mathArg; + break; + } + } + } + break; + } + case 0xC0: + case 0xD0: + { + int cmdArg = ReadArg(ArgType.Byte); + if (track.DoCommandWork) + { + switch (cmd) + { + case 0xC0: // Panpot + { + track.Panpot = (sbyte)(cmdArg - 0x40); + break; + } + case 0xC1: // Track Volume + { + track.Volume = (byte)cmdArg; + break; + } + case 0xC2: // Player Volume + { + Volume = (byte)cmdArg; + break; + } + case 0xC3: // Transpose + { + track.Transpose = (sbyte)cmdArg; + break; + } + case 0xC4: // Pitch Bend + { + track.PitchBend = (sbyte)cmdArg; + break; + } + case 0xC5: // Pitch Bend Range + { + track.PitchBendRange = (byte)cmdArg; + break; + } + case 0xC6: // Priority + { + track.Priority = (byte)(Priority + (byte)cmdArg); + break; + } + case 0xC7: // Mono + { + track.Mono = cmdArg == 1; + break; + } + case 0xC8: // Tie + { + track.Tie = cmdArg == 1; + track.StopAllChannels(); + break; + } + case 0xC9: // Portamento Control + { + int k = cmdArg + track.Transpose; + if (k < 0) + { + k = 0; + } + else if (k > 0x7F) + { + k = 0x7F; + } + track.PortamentoKey = (byte)k; + track.Portamento = true; + break; + } + case 0xCA: // LFO Depth + { + track.LFODepth = (byte)cmdArg; + break; + } + case 0xCB: // LFO Speed + { + track.LFOSpeed = (byte)cmdArg; + break; + } + case 0xCC: // LFO Type + { + track.LFOType = (LFOType)cmdArg; + break; + } + case 0xCD: // LFO Range + { + track.LFORange = (byte)cmdArg; + break; + } + case 0xCE: // Portamento Toggle + { + track.Portamento = cmdArg == 1; + break; + } + case 0xCF: // Portamento Time + { + track.PortamentoTime = (byte)cmdArg; + break; + } + case 0xD0: // Forced Attack + { + track.Attack = (byte)cmdArg; + break; + } + case 0xD1: // Forced Decay + { + track.Decay = (byte)cmdArg; + break; + } + case 0xD2: // Forced Sustain + { + track.Sustain = (byte)cmdArg; + break; + } + case 0xD3: // Forced Release + { + track.Release = (byte)cmdArg; + break; + } + case 0xD4: // Loop Start + { + if (track.CallStackDepth < 3) + { + track.CallStack[track.CallStackDepth] = track.DataOffset; + track.CallStackLoops[track.CallStackDepth] = (byte)cmdArg; + track.CallStackDepth++; + } + break; + } + case 0xD5: // Track Expression + { + track.Expression = (byte)cmdArg; + break; + } + } + } + break; + } + case 0xE0: + { + int cmdArg = ReadArg(ArgType.Short); + if (track.DoCommandWork) + { + switch (cmd) + { + case 0xE0: // LFO Delay + { + track.LFODelay = (ushort)cmdArg; + break; + } + case 0xE1: // Tempo + { + _tempo = (ushort)cmdArg; + break; + } + case 0xE3: // Sweep Pitch + { + track.SweepPitch = (short)cmdArg; + break; + } + } + } + break; + } + case 0xF0: + { + if (track.DoCommandWork) + { + switch (cmd) + { + case 0xFC: // Loop End + { + if (track.CallStackDepth != 0) + { + byte count = track.CallStackLoops[track.CallStackDepth - 1]; + if (count != 0) + { + count--; + track.CallStackLoops[track.CallStackDepth - 1] = count; + if (count == 0) + { + track.CallStackDepth--; + break; + } + } + track.DataOffset = track.CallStack[track.CallStackDepth - 1]; + } + break; + } + case 0xFD: // Return + { + if (track.CallStackDepth != 0) + { + track.CallStackDepth--; + track.DataOffset = track.CallStack[track.CallStackDepth]; + track.CallStackLoops[track.CallStackDepth] = 0; // This is only necessary for SetTicks() to deal with LoopStart (0) + } + break; + } + case 0xFE: // Alloc Tracks + { + // Must be in the beginning of the first track to work + if (track.Index == 0 && track.DataOffset == 1) // == 1 because we read cmd already + { + // Track 1 enabled = bit 1 set, Track 4 enabled = bit 4 set, etc + int trackBits = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8); + for (int i = 0; i < 0x10; i++) + { + if ((trackBits & (1 << i)) != 0) + { + _tracks[i].Allocated = true; + } + } + } + break; + } + case 0xFF: // Finish + { + track.Stopped = true; + break; + } + } + } + break; + } + } + } + if (resetOverride) + { + track.ArgOverrideType = ArgType.None; + } + if (resetCmdWork) + { + track.DoCommandWork = true; + } + } + + private void Tick() + { + _time.Start(); + while (true) + { + PlayerState state = State; + bool playing = state == PlayerState.Playing; + bool recording = state == PlayerState.Recording; + if (!playing && !recording) + { + goto stop; + } + + void MixerProcess() + { + for (int i = 0; i < 0x10; i++) + { + Track track = _tracks[i]; + if (track.Enabled) + { + track.UpdateChannels(); + } + } + _mixer.ChannelTick(); + _mixer.Process(playing, recording); + } + + while (_tempoStack >= 240) + { + _tempoStack -= 240; + bool allDone = true; + for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) + { + Track track = _tracks[trackIndex]; + if (!track.Enabled) + { + continue; + } + track.Tick(); + while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) + { + ExecuteNext(track); + } + if (trackIndex == _longestTrack) + { + if (ElapsedTicks == MaxTicks) + { + if (!track.Stopped) + { + List evs = Events[trackIndex]; + for (int i = 0; i < evs.Count; i++) + { + SongEvent ev = evs[i]; + if (ev.Offset == track.DataOffset) + { + //ElapsedTicks = ev.Ticks[0] - track.Rest; + ElapsedTicks = ev.Ticks.Count == 0 ? 0 : ev.Ticks[0] - track.Rest; // Prevent crashes with songs that don't load all ticks yet (See SetTicks()) + break; + } + } + _elapsedLoops++; + if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) + { + _mixer.BeginFadeOut(); + } + } + } + else + { + ElapsedTicks++; + } + } + if (!track.Stopped || track.Channels.Count != 0) + { + allDone = false; + } + } + if (_mixer.IsFadeDone()) + { + allDone = true; + } + if (allDone) + { + MixerProcess(); + State = PlayerState.Stopped; + SongEnded?.Invoke(); + goto stop; + } + } + _tempoStack += _tempo; + MixerProcess(); + if (playing) + { + _time.Wait(); + } + } + stop: + _time.Stop(); + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs b/VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs new file mode 100644 index 00000000..c0b016d8 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs @@ -0,0 +1,184 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class SBNK + { + public class InstrumentData + { + public class DataParam + { + [BinaryArrayFixedLength(2)] + public ushort[] Info { get; set; } + public byte BaseKey { get; set; } + public byte Attack { get; set; } + public byte Decay { get; set; } + public byte Sustain { get; set; } + public byte Release { get; set; } + public byte Pan { get; set; } + } + + public InstrumentType Type { get; set; } + public byte Padding { get; set; } + public DataParam Param { get; set; } + } + public class Instrument : IBinarySerializable + { + public class DefaultData + { + public InstrumentData.DataParam Param { get; set; } + } + public class DrumSetData : IBinarySerializable + { + public byte MinNote; + public byte MaxNote; + public InstrumentData[] SubInstruments; + + public void Read(EndianBinaryReader er) + { + MinNote = er.ReadByte(); + MaxNote = er.ReadByte(); + SubInstruments = new InstrumentData[MaxNote - MinNote + 1]; + for (int i = 0; i < SubInstruments.Length; i++) + { + SubInstruments[i] = er.ReadObject(); + } + } + public void Write(EndianBinaryWriter ew) + { + throw new NotImplementedException(); + } + } + public class KeySplitData : IBinarySerializable + { + public byte[] KeyRegions; + public InstrumentData[] SubInstruments; + + public void Read(EndianBinaryReader er) + { + KeyRegions = er.ReadBytes(8); + int numSubInstruments = 0; + for (int i = 0; i < 8; i++) + { + if (KeyRegions[i] == 0) + { + break; + } + numSubInstruments++; + } + SubInstruments = new InstrumentData[numSubInstruments]; + for (int i = 0; i < numSubInstruments; i++) + { + SubInstruments[i] = er.ReadObject(); + } + } + public void Write(EndianBinaryWriter ew) + { + throw new NotImplementedException(); + } + } + + public InstrumentType Type; + public ushort DataOffset; + public byte Padding; + + public object Data; + + public void Read(EndianBinaryReader er) + { + Type = er.ReadEnum(); + DataOffset = er.ReadUInt16(); + Padding = er.ReadByte(); + + long p = er.BaseStream.Position; + switch (Type) + { + case InstrumentType.PCM: + case InstrumentType.PSG: + case InstrumentType.Noise: Data = er.ReadObject(DataOffset); break; + case InstrumentType.Drum: Data = er.ReadObject(DataOffset); break; + case InstrumentType.KeySplit: Data = er.ReadObject(DataOffset); break; + default: break; + } + er.BaseStream.Position = p; + } + public void Write(EndianBinaryWriter ew) + { + throw new NotImplementedException(); + } + } + + public FileHeader FileHeader { get; set; } // "SBNK" + [BinaryStringFixedLength(4)] + public string BlockType { get; set; } // "DATA" + public int BlockSize { get; set; } + [BinaryArrayFixedLength(32)] + public byte[] Padding { get; set; } + public int NumInstruments { get; set; } + [BinaryArrayVariableLength(nameof(NumInstruments))] + public Instrument[] Instruments { get; set; } + + [BinaryIgnore] + public SWAR[] SWARs { get; } = new SWAR[4]; + + public SBNK(byte[] bytes) + { + using (var er = new EndianBinaryReader(new MemoryStream(bytes))) + { + er.ReadIntoObject(this); + } + } + + public InstrumentData GetInstrumentData(int voice, int key) + { + if (voice >= NumInstruments) + { + return null; + } + else + { + switch (Instruments[voice].Type) + { + case InstrumentType.PCM: + case InstrumentType.PSG: + case InstrumentType.Noise: + { + var d = (Instrument.DefaultData)Instruments[voice].Data; + // TODO: Better way? + return new InstrumentData + { + Type = Instruments[voice].Type, + Param = d.Param + }; + } + case InstrumentType.Drum: + { + var d = (Instrument.DrumSetData)Instruments[voice].Data; + return key < d.MinNote || key > d.MaxNote ? null : d.SubInstruments[key - d.MinNote]; + } + case InstrumentType.KeySplit: + { + var d = (Instrument.KeySplitData)Instruments[voice].Data; + for (int i = 0; i < 8; i++) + { + if (key <= d.KeyRegions[i]) + { + return d.SubInstruments[i]; + } + } + return null; + } + default: return null; + } + } + } + + public SWAR.SWAV GetSWAV(int swarIndex, int swavIndex) + { + SWAR swar = SWARs[swarIndex]; + return swar == null || swavIndex >= swar.NumWaves ? null : swar.Waves[swavIndex]; + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs b/VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs new file mode 100644 index 00000000..0f4c4c7f --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs @@ -0,0 +1,234 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class SDAT + { + public class SYMB : IBinarySerializable + { + public class Record + { + public int NumEntries; + public int[] EntryOffsets; + + public string[] Entries; + + public Record(EndianBinaryReader er, long baseOffset) + { + NumEntries = er.ReadInt32(); + EntryOffsets = er.ReadInt32s(NumEntries); + + long p = er.BaseStream.Position; + Entries = new string[NumEntries]; + for (int i = 0; i < NumEntries; i++) + { + if (EntryOffsets[i] != 0) + { + Entries[i] = er.ReadStringNullTerminated(baseOffset + EntryOffsets[i]); + } + } + er.BaseStream.Position = p; + } + } + + public string BlockType; // "SYMB" + public int BlockSize; + public int[] RecordOffsets; + public byte[] Padding; + + public Record SequenceSymbols; + //SequenceArchiveSymbols; + public Record BankSymbols; + public Record WaveArchiveSymbols; + //PlayerSymbols; + //GroupSymbols; + //StreamPlayerSymbols; + //StreamSymbols; + + public void Read(EndianBinaryReader er) + { + long baseOffset = er.BaseStream.Position; + BlockType = er.ReadString(4, false); + BlockSize = er.ReadInt32(); + RecordOffsets = er.ReadInt32s(8); + Padding = er.ReadBytes(24); + er.BaseStream.Position = baseOffset + RecordOffsets[0]; + SequenceSymbols = new Record(er, baseOffset); + er.BaseStream.Position = baseOffset + RecordOffsets[2]; + BankSymbols = new Record(er, baseOffset); + er.BaseStream.Position = baseOffset + RecordOffsets[3]; + WaveArchiveSymbols = new Record(er, baseOffset); + er.BaseStream.Position = baseOffset + BlockSize; + } + public void Write(EndianBinaryWriter ew) + { + throw new NotImplementedException(); + } + } + + public class INFO : IBinarySerializable + { + public class Record where T : new() + { + public int NumEntries; + public int[] EntryOffsets; + + public T[] Entries; + + public Record(EndianBinaryReader er, long baseOffset) + { + NumEntries = er.ReadInt32(); + EntryOffsets = er.ReadInt32s(NumEntries); + + long p = er.BaseStream.Position; + Entries = new T[NumEntries]; + for (int i = 0; i < NumEntries; i++) + { + if (EntryOffsets[i] != 0) + { + Entries[i] = er.ReadObject(baseOffset + EntryOffsets[i]); + } + } + er.BaseStream.Position = p; + } + } + + public class SequenceInfo + { + public ushort FileId { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown1 { get; set; } + public ushort Bank { get; set; } + public byte Volume { get; set; } + public byte ChannelPriority { get; set; } + public byte PlayerPriority { get; set; } + public byte PlayerNum { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown2 { get; set; } + } + public class BankInfo + { + public ushort FileId { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown { get; set; } + [BinaryArrayFixedLength(4)] + public ushort[] SWARs { get; set; } + } + public class WaveArchiveInfo + { + public ushort FileId { get; set; } + [BinaryArrayFixedLength(2)] + public byte[] Unknown { get; set; } + } + + public string BlockType; // "INFO" + public int BlockSize; + public int[] InfoOffsets; + + public Record SequenceInfos; + //SequenceArchiveInfos; + public Record BankInfos; + public Record WaveArchiveInfos; + //PlayerInfos; + //GroupInfos; + //StreamPlayerInfos; + //StreamInfos; + + public void Read(EndianBinaryReader er) + { + long baseOffset = er.BaseStream.Position; + BlockType = er.ReadString(4, false); + BlockSize = er.ReadInt32(); + InfoOffsets = er.ReadInt32s(8); + er.ReadBytes(24); + er.BaseStream.Position = baseOffset + InfoOffsets[0]; + SequenceInfos = new Record(er, baseOffset); + er.BaseStream.Position = baseOffset + InfoOffsets[2]; + BankInfos = new Record(er, baseOffset); + er.BaseStream.Position = baseOffset + InfoOffsets[3]; + WaveArchiveInfos = new Record(er, baseOffset); + er.BaseStream.Position = baseOffset; + } + public void Write(EndianBinaryWriter ew) + { + throw new NotImplementedException(); + } + } + + public class FAT + { + public class FATEntry : IBinarySerializable + { + public int DataOffset; + public int DataLength; + public byte[] Padding; + + public byte[] Data; + + public void Read(EndianBinaryReader er) + { + DataOffset = er.ReadInt32(); + DataLength = er.ReadInt32(); + Padding = er.ReadBytes(8); + + long p = er.BaseStream.Position; + Data = er.ReadBytes(DataLength, DataOffset); + er.BaseStream.Position = p; + } + public void Write(EndianBinaryWriter ew) + { + throw new NotImplementedException(); + } + } + + [BinaryStringFixedLength(4)] + public string BlockType { get; set; } // "FAT " + public int BlockSize { get; set; } + public int NumEntries { get; set; } + [BinaryArrayVariableLength(nameof(NumEntries))] + public FATEntry[] Entries { get; set; } + } + + public FileHeader FileHeader; // "SDAT" + public int SYMBOffset; + public int SYMBLength; + public int INFOOffset; + public int INFOLength; + public int FATOffset; + public int FATLength; + public int FILEOffset; + public int FILELength; + public byte[] Padding; + + public SYMB SYMBBlock; + public INFO INFOBlock; + public FAT FATBlock; + //FILEBlock + + public SDAT(byte[] bytes) + { + using (var er = new EndianBinaryReader(new MemoryStream(bytes))) + { + FileHeader = er.ReadObject(); + SYMBOffset = er.ReadInt32(); + SYMBLength = er.ReadInt32(); + INFOOffset = er.ReadInt32(); + INFOLength = er.ReadInt32(); + FATOffset = er.ReadInt32(); + FATLength = er.ReadInt32(); + FILEOffset = er.ReadInt32(); + FILELength = er.ReadInt32(); + Padding = er.ReadBytes(16); + + if (SYMBOffset != 0 && SYMBLength != 0) + { + SYMBBlock = er.ReadObject(SYMBOffset); + } + INFOBlock = er.ReadObject(INFOOffset); + FATBlock = er.ReadObject(FATOffset); + } + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs b/VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs new file mode 100644 index 00000000..3d97b1e1 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs @@ -0,0 +1,28 @@ +using Kermalis.EndianBinaryIO; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class SSEQ + { + public FileHeader FileHeader; // "SSEQ" + public string BlockType; // "DATA" + public int BlockSize; + public int DataOffset; + + public byte[] Data; + + public SSEQ(byte[] bytes) + { + using (var er = new EndianBinaryReader(new MemoryStream(bytes))) + { + FileHeader = er.ReadObject(); + BlockType = er.ReadString(4, false); + BlockSize = er.ReadInt32(); + DataOffset = er.ReadInt32(); + + Data = er.ReadBytes(FileHeader.FileSize - DataOffset, DataOffset); + } + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs b/VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs new file mode 100644 index 00000000..c7a982c8 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs @@ -0,0 +1,65 @@ +using Kermalis.EndianBinaryIO; +using System; +using System.IO; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class SWAR + { + public class SWAV : IBinarySerializable + { + public SWAVFormat Format; + public bool DoesLoop; + public ushort SampleRate; + public ushort Timer; // (NDSUtils.ARM7_CLOCK / SampleRate) + public ushort LoopOffset; + public int Length; + + public byte[] Samples; + + public void Read(EndianBinaryReader er) + { + Format = er.ReadEnum(); + DoesLoop = er.ReadBoolean(); + SampleRate = er.ReadUInt16(); + Timer = er.ReadUInt16(); + LoopOffset = er.ReadUInt16(); + Length = er.ReadInt32(); + + Samples = er.ReadBytes((LoopOffset * 4) + (Length * 4)); + } + public void Write(EndianBinaryWriter ew) + { + throw new NotImplementedException(); + } + } + + public FileHeader FileHeader; // "SWAR" + public string BlockType; // "DATA" + public int BlockSize; + public byte[] Padding; + public int NumWaves; + public int[] WaveOffsets; + + public SWAV[] Waves; + + public SWAR(byte[] bytes) + { + using (var er = new EndianBinaryReader(new MemoryStream(bytes))) + { + FileHeader = er.ReadObject(); + BlockType = er.ReadString(4, false); + BlockSize = er.ReadInt32(); + Padding = er.ReadBytes(32); + NumWaves = er.ReadInt32(); + WaveOffsets = er.ReadInt32s(NumWaves); + + Waves = new SWAV[NumWaves]; + for (int i = 0; i < NumWaves; i++) + { + Waves[i] = er.ReadObject(WaveOffsets[i]); + } + } + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Track.cs b/VG Music Studio.backup/Core/NDS/SDAT/Track.cs new file mode 100644 index 00000000..75802b42 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/Track.cs @@ -0,0 +1,193 @@ +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal class Track + { + public readonly byte Index; + private readonly Player _player; + + public bool Allocated; + public bool Enabled; + public bool Stopped; + public bool Tie; + public bool Mono; + public bool Portamento; + public bool WaitingForNoteToFinishBeforeContinuingXD; // Is this necessary? + public byte Voice; + public byte Priority; + public byte Volume; + public byte Expression; + public byte PitchBendRange; + public byte LFORange; + public byte LFOSpeed; + public byte LFODepth; + public ushort LFODelay; + public ushort LFOPhase; + public int LFOParam; + public ushort LFODelayCount; + public LFOType LFOType; + public sbyte PitchBend; + public sbyte Panpot; + public sbyte Transpose; + public byte Attack; + public byte Decay; + public byte Sustain; + public byte Release; + public byte PortamentoKey; + public byte PortamentoTime; + public short SweepPitch; + public int Rest; + public int[] CallStack = new int[3]; + public byte[] CallStackLoops = new byte[3]; + public byte CallStackDepth; + public int DataOffset; + public bool VariableFlag; // Set by variable commands (0xB0 - 0xBD) + public bool DoCommandWork; + public ArgType ArgOverrideType; + + public readonly List Channels = new List(0x10); + + public int GetPitch() + { + //int lfo = LFOType == LFOType.Pitch ? LFOParam : 0; + int lfo = 0; + return (PitchBend * PitchBendRange / 2) + lfo; + } + public int GetVolume() + { + //int lfo = LFOType == LFOType.Volume ? LFOParam : 0; + int lfo = 0; + return Utils.SustainTable[_player.Volume] + Utils.SustainTable[Volume] + Utils.SustainTable[Expression] + lfo; + } + public sbyte GetPan() + { + //int lfo = LFOType == LFOType.Panpot ? LFOParam : 0; + int lfo = 0; + int p = Panpot + lfo; + if (p < -0x40) + { + p = -0x40; + } + else if (p > 0x3F) + { + p = 0x3F; + } + return (sbyte)p; + } + + public Track(byte i, Player player) + { + Index = i; + _player = player; + } + public void Init() + { + Stopped = Tie = WaitingForNoteToFinishBeforeContinuingXD = Portamento = false; + Allocated = Enabled = Index == 0; + DataOffset = 0; + ArgOverrideType = ArgType.None; + Mono = VariableFlag = DoCommandWork = true; + CallStackDepth = 0; + Voice = LFODepth = 0; + PitchBend = Panpot = Transpose = 0; + LFOPhase = LFODelay = LFODelayCount = 0; + LFORange = 1; + LFOSpeed = 0x10; + Priority = (byte)(_player.Priority + 0x40); + Volume = Expression = 0x7F; + Attack = Decay = Sustain = Release = 0xFF; + PitchBendRange = 2; + PortamentoKey = 60; + PortamentoTime = 0; + SweepPitch = 0; + LFOType = LFOType.Pitch; + Rest = 0; + StopAllChannels(); + } + public void LFOTick() + { + if (Channels.Count != 0) + { + if (LFODelayCount > 0) + { + LFODelayCount--; + LFOPhase = 0; + } + else + { + int param = LFORange * Utils.Sin(LFOPhase >> 8) * LFODepth; + if (LFOType == LFOType.Volume) + { + param = (param * 60) >> 14; + } + else + { + param >>= 8; + } + LFOParam = param; + int counter = LFOPhase + (LFOSpeed << 6); // "<< 6" is "* 0x40" + while (counter >= 0x8000) + { + counter -= 0x8000; + } + LFOPhase = (ushort)counter; + } + } + else + { + LFOPhase = 0; + LFOParam = 0; + LFODelayCount = LFODelay; + } + } + public void Tick() + { + if (Rest > 0) + { + Rest--; + } + if (Channels.Count != 0) + { + // TickNotes: + for (int i = 0; i < Channels.Count; i++) + { + Channel c = Channels[i]; + if (c.NoteDuration > 0) + { + c.NoteDuration--; + } + if (!c.AutoSweep && c.SweepCounter < c.SweepLength) + { + c.SweepCounter++; + } + } + } + else + { + WaitingForNoteToFinishBeforeContinuingXD = false; + } + } + public void UpdateChannels() + { + for (int i = 0; i < Channels.Count; i++) + { + Channel c = Channels[i]; + c.LFOType = LFOType; + c.LFOSpeed = LFOSpeed; + c.LFODepth = LFODepth; + c.LFORange = LFORange; + c.LFODelay = LFODelay; + } + } + + public void StopAllChannels() + { + Channel[] chans = Channels.ToArray(); + for (int i = 0; i < chans.Length; i++) + { + chans[i].Stop(); + } + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Utils.cs b/VG Music Studio.backup/Core/NDS/SDAT/Utils.cs new file mode 100644 index 00000000..289f6012 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/SDAT/Utils.cs @@ -0,0 +1,345 @@ +namespace Kermalis.VGMusicStudio.Core.NDS.SDAT +{ + internal static class Utils + { + public static readonly byte[] AttackTable = new byte[128] + { + 255, 254, 253, 252, 251, 250, 249, 248, + 247, 246, 245, 244, 243, 242, 241, 240, + 239, 238, 237, 236, 235, 234, 233, 232, + 231, 230, 229, 228, 227, 226, 225, 224, + 223, 222, 221, 220, 219, 218, 217, 216, + 215, 214, 213, 212, 211, 210, 209, 208, + 207, 206, 205, 204, 203, 202, 201, 200, + 199, 198, 197, 196, 195, 194, 193, 192, + 191, 190, 189, 188, 187, 186, 185, 184, + 183, 182, 181, 180, 179, 178, 177, 176, + 175, 174, 173, 172, 171, 170, 169, 168, + 167, 166, 165, 164, 163, 162, 161, 160, + 159, 158, 157, 156, 155, 154, 153, 152, + 151, 150, 149, 148, 147, 143, 137, 132, + 127, 123, 116, 109, 100, 92, 84, 73, + 63, 51, 38, 26, 14, 5, 1, 0 + }; + public static readonly ushort[] DecayTable = new ushort[128] + { + 1, 3, 5, 7, 9, 11, 13, 15, + 17, 19, 21, 23, 25, 27, 29, 31, + 33, 35, 37, 39, 41, 43, 45, 47, + 49, 51, 53, 55, 57, 59, 61, 63, + 65, 67, 69, 71, 73, 75, 77, 79, + 81, 83, 85, 87, 89, 91, 93, 95, + 97, 99, 101, 102, 104, 105, 107, 108, + 110, 111, 113, 115, 116, 118, 120, 122, + 124, 126, 128, 130, 132, 135, 137, 140, + 142, 145, 148, 151, 154, 157, 160, 163, + 167, 171, 175, 179, 183, 187, 192, 197, + 202, 208, 213, 219, 226, 233, 240, 248, + 256, 265, 274, 284, 295, 307, 320, 334, + 349, 366, 384, 404, 427, 452, 480, 512, + 549, 591, 640, 698, 768, 853, 960, 1097, + 1280, 1536, 1920, 2560, 3840, 7680, 15360, 65535 + }; + public static readonly int[] SustainTable = new int[128] + { + -92544, -92416, -92288, -83328, -76928, -71936, -67840, -64384, + -61440, -58880, -56576, -54400, -52480, -50688, -49024, -47488, + -46080, -44672, -43392, -42240, -41088, -40064, -39040, -38016, + -36992, -36096, -35328, -34432, -33664, -32896, -32128, -31360, + -30592, -29952, -29312, -28672, -28032, -27392, -26880, -26240, + -25728, -25088, -24576, -24064, -23552, -23040, -22528, -22144, + -21632, -21120, -20736, -20224, -19840, -19456, -19072, -18560, + -18176, -17792, -17408, -17024, -16640, -16256, -16000, -15616, + -15232, -14848, -14592, -14208, -13952, -13568, -13184, -12928, + -12672, -12288, -12032, -11648, -11392, -11136, -10880, -10496, + -10240, -9984, -9728, -9472, -9216, -8960, -8704, -8448, + -8192, -7936, -7680, -7424, -7168, -6912, -6656, -6400, + -6272, -6016, -5760, -5504, -5376, -5120, -4864, -4608, + -4480, -4224, -3968, -3840, -3584, -3456, -3200, -2944, + -2816, -2560, -2432, -2176, -2048, -1792, -1664, -1408, + -1280, -1024, -896, -768, -512, -384, -128, 0 + }; + + private static readonly sbyte[] _sinTable = new sbyte[33] + { + 000, 006, 012, 019, 025, 031, 037, 043, + 049, 054, 060, 065, 071, 076, 081, 085, + 090, 094, 098, 102, 106, 109, 112, 115, + 117, 120, 122, 123, 125, 126, 126, 127, + 127 + }; + public static int Sin(int index) + { + if (index < 0x20) + { + return _sinTable[index]; + } + else if (index < 0x40) + { + return _sinTable[0x20 - (index - 0x20)]; + } + else if (index < 0x60) + { + return -_sinTable[index - 0x40]; + } + else // < 0x80 + { + return -_sinTable[0x20 - (index - 0x60)]; + } + } + + private static readonly ushort[] _pitchTable = new ushort[768] + { + 0, 59, 118, 178, 237, 296, 356, 415, + 475, 535, 594, 654, 714, 773, 833, 893, + 953, 1013, 1073, 1134, 1194, 1254, 1314, 1375, + 1435, 1496, 1556, 1617, 1677, 1738, 1799, 1859, + 1920, 1981, 2042, 2103, 2164, 2225, 2287, 2348, + 2409, 2471, 2532, 2593, 2655, 2716, 2778, 2840, + 2902, 2963, 3025, 3087, 3149, 3211, 3273, 3335, + 3397, 3460, 3522, 3584, 3647, 3709, 3772, 3834, + 3897, 3960, 4022, 4085, 4148, 4211, 4274, 4337, + 4400, 4463, 4526, 4590, 4653, 4716, 4780, 4843, + 4907, 4971, 5034, 5098, 5162, 5226, 5289, 5353, + 5417, 5481, 5546, 5610, 5674, 5738, 5803, 5867, + 5932, 5996, 6061, 6125, 6190, 6255, 6320, 6384, + 6449, 6514, 6579, 6645, 6710, 6775, 6840, 6906, + 6971, 7037, 7102, 7168, 7233, 7299, 7365, 7431, + 7496, 7562, 7628, 7694, 7761, 7827, 7893, 7959, + 8026, 8092, 8159, 8225, 8292, 8358, 8425, 8492, + 8559, 8626, 8693, 8760, 8827, 8894, 8961, 9028, + 9096, 9163, 9230, 9298, 9366, 9433, 9501, 9569, + 9636, 9704, 9772, 9840, 9908, 9976, 10045, 10113, + 10181, 10250, 10318, 10386, 10455, 10524, 10592, 10661, + 10730, 10799, 10868, 10937, 11006, 11075, 11144, 11213, + 11283, 11352, 11421, 11491, 11560, 11630, 11700, 11769, + 11839, 11909, 11979, 12049, 12119, 12189, 12259, 12330, + 12400, 12470, 12541, 12611, 12682, 12752, 12823, 12894, + 12965, 13036, 13106, 13177, 13249, 13320, 13391, 13462, + 13533, 13605, 13676, 13748, 13819, 13891, 13963, 14035, + 14106, 14178, 14250, 14322, 14394, 14467, 14539, 14611, + 14684, 14756, 14829, 14901, 14974, 15046, 15119, 15192, + 15265, 15338, 15411, 15484, 15557, 15630, 15704, 15777, + 15850, 15924, 15997, 16071, 16145, 16218, 16292, 16366, + 16440, 16514, 16588, 16662, 16737, 16811, 16885, 16960, + 17034, 17109, 17183, 17258, 17333, 17408, 17483, 17557, + 17633, 17708, 17783, 17858, 17933, 18009, 18084, 18160, + 18235, 18311, 18387, 18462, 18538, 18614, 18690, 18766, + 18842, 18918, 18995, 19071, 19147, 19224, 19300, 19377, + 19454, 19530, 19607, 19684, 19761, 19838, 19915, 19992, + 20070, 20147, 20224, 20302, 20379, 20457, 20534, 20612, + 20690, 20768, 20846, 20924, 21002, 21080, 21158, 21236, + 21315, 21393, 21472, 21550, 21629, 21708, 21786, 21865, + 21944, 22023, 22102, 22181, 22260, 22340, 22419, 22498, + 22578, 22658, 22737, 22817, 22897, 22977, 23056, 23136, + 23216, 23297, 23377, 23457, 23537, 23618, 23698, 23779, + 23860, 23940, 24021, 24102, 24183, 24264, 24345, 24426, + 24507, 24589, 24670, 24752, 24833, 24915, 24996, 25078, + 25160, 25242, 25324, 25406, 25488, 25570, 25652, 25735, + 25817, 25900, 25982, 26065, 26148, 26230, 26313, 26396, + 26479, 26562, 26645, 26729, 26812, 26895, 26979, 27062, + 27146, 27230, 27313, 27397, 27481, 27565, 27649, 27733, + 27818, 27902, 27986, 28071, 28155, 28240, 28324, 28409, + 28494, 28579, 28664, 28749, 28834, 28919, 29005, 29090, + 29175, 29261, 29346, 29432, 29518, 29604, 29690, 29776, + 29862, 29948, 30034, 30120, 30207, 30293, 30380, 30466, + 30553, 30640, 30727, 30814, 30900, 30988, 31075, 31162, + 31249, 31337, 31424, 31512, 31599, 31687, 31775, 31863, + 31951, 32039, 32127, 32215, 32303, 32392, 32480, 32568, + 32657, 32746, 32834, 32923, 33012, 33101, 33190, 33279, + 33369, 33458, 33547, 33637, 33726, 33816, 33906, 33995, + 34085, 34175, 34265, 34355, 34446, 34536, 34626, 34717, + 34807, 34898, 34988, 35079, 35170, 35261, 35352, 35443, + 35534, 35626, 35717, 35808, 35900, 35991, 36083, 36175, + 36267, 36359, 36451, 36543, 36635, 36727, 36820, 36912, + 37004, 37097, 37190, 37282, 37375, 37468, 37561, 37654, + 37747, 37841, 37934, 38028, 38121, 38215, 38308, 38402, + 38496, 38590, 38684, 38778, 38872, 38966, 39061, 39155, + 39250, 39344, 39439, 39534, 39629, 39724, 39819, 39914, + 40009, 40104, 40200, 40295, 40391, 40486, 40582, 40678, + 40774, 40870, 40966, 41062, 41158, 41255, 41351, 41448, + 41544, 41641, 41738, 41835, 41932, 42029, 42126, 42223, + 42320, 42418, 42515, 42613, 42710, 42808, 42906, 43004, + 43102, 43200, 43298, 43396, 43495, 43593, 43692, 43790, + 43889, 43988, 44087, 44186, 44285, 44384, 44483, 44583, + 44682, 44781, 44881, 44981, 45081, 45180, 45280, 45381, + 45481, 45581, 45681, 45782, 45882, 45983, 46083, 46184, + 46285, 46386, 46487, 46588, 46690, 46791, 46892, 46994, + 47095, 47197, 47299, 47401, 47503, 47605, 47707, 47809, + 47912, 48014, 48117, 48219, 48322, 48425, 48528, 48631, + 48734, 48837, 48940, 49044, 49147, 49251, 49354, 49458, + 49562, 49666, 49770, 49874, 49978, 50082, 50187, 50291, + 50396, 50500, 50605, 50710, 50815, 50920, 51025, 51131, + 51236, 51341, 51447, 51552, 51658, 51764, 51870, 51976, + 52082, 52188, 52295, 52401, 52507, 52614, 52721, 52827, + 52934, 53041, 53148, 53256, 53363, 53470, 53578, 53685, + 53793, 53901, 54008, 54116, 54224, 54333, 54441, 54549, + 54658, 54766, 54875, 54983, 55092, 55201, 55310, 55419, + 55529, 55638, 55747, 55857, 55966, 56076, 56186, 56296, + 56406, 56516, 56626, 56736, 56847, 56957, 57068, 57179, + 57289, 57400, 57511, 57622, 57734, 57845, 57956, 58068, + 58179, 58291, 58403, 58515, 58627, 58739, 58851, 58964, + 59076, 59189, 59301, 59414, 59527, 59640, 59753, 59866, + 59979, 60092, 60206, 60319, 60433, 60547, 60661, 60774, + 60889, 61003, 61117, 61231, 61346, 61460, 61575, 61690, + 61805, 61920, 62035, 62150, 62265, 62381, 62496, 62612, + 62727, 62843, 62959, 63075, 63191, 63308, 63424, 63540, + 63657, 63774, 63890, 64007, 64124, 64241, 64358, 64476, + 64593, 64711, 64828, 64946, 65064, 65182, 65300, 65418 + }; + private static readonly byte[] _volumeTable = new byte[724] + { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, + 4, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, + 5, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 7, + 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 10, 10, 10, + 10, 10, 10, 10, 10, 11, 11, 11, + 11, 11, 11, 11, 11, 12, 12, 12, + 12, 12, 12, 12, 13, 13, 13, 13, + 13, 13, 13, 14, 14, 14, 14, 14, + 14, 15, 15, 15, 15, 15, 16, 16, + 16, 16, 16, 16, 17, 17, 17, 17, + 17, 18, 18, 18, 18, 19, 19, 19, + 19, 19, 20, 20, 20, 20, 21, 21, + 21, 21, 22, 22, 22, 22, 23, 23, + 23, 23, 24, 24, 24, 25, 25, 25, + 25, 26, 26, 26, 27, 27, 27, 28, + 28, 28, 29, 29, 29, 30, 30, 30, + 31, 31, 31, 32, 32, 33, 33, 33, + 34, 34, 35, 35, 35, 36, 36, 37, + 37, 38, 38, 38, 39, 39, 40, 40, + 41, 41, 42, 42, 43, 43, 44, 44, + 45, 45, 46, 46, 47, 47, 48, 48, + 49, 50, 50, 51, 51, 52, 52, 53, + 54, 54, 55, 56, 56, 57, 58, 58, + 59, 60, 60, 61, 62, 62, 63, 64, + 65, 66, 66, 67, 68, 69, 70, 70, + 71, 72, 73, 74, 75, 75, 76, 77, + 78, 79, 80, 81, 82, 83, 84, 85, + 86, 87, 88, 89, 90, 91, 92, 93, + 94, 95, 96, 97, 98, 99, 101, 102, + 103, 104, 105, 106, 108, 109, 110, 111, + 113, 114, 115, 117, 118, 119, 121, 122, + 124, 125, 126, 127 + }; + + public static ushort GetChannelTimer(ushort baseTimer, int pitch) + { + int shift = 0; + pitch = -pitch; + + while (pitch < 0) + { + shift--; + pitch += 0x300; + } + + while (pitch >= 0x300) + { + shift++; + pitch -= 0x300; + } + + ulong timer = (_pitchTable[pitch] + 0x10000uL) * baseTimer; + shift -= 16; + if (shift <= 0) + { + timer >>= -shift; + } + else if (shift < 32) + { + if ((timer & (ulong.MaxValue << (32 - shift))) != 0) + { + return ushort.MaxValue; + } + timer <<= shift; + } + else + { + return ushort.MaxValue; + } + + if (timer < 0x10) + { + return 0x10; + } + if (timer > ushort.MaxValue) + { + timer = ushort.MaxValue; + } + return (ushort)timer; + } + public static byte GetChannelVolume(int vol) + { + int a = vol / 0x80; + if (a < -723) + { + a = -723; + } + else if (a > 0) + { + a = 0; + } + return _volumeTable[a + 723]; + } + } +} diff --git a/VG Music Studio.backup/Core/NDS/Utils.cs b/VG Music Studio.backup/Core/NDS/Utils.cs new file mode 100644 index 00000000..ea2388b6 --- /dev/null +++ b/VG Music Studio.backup/Core/NDS/Utils.cs @@ -0,0 +1,7 @@ +namespace Kermalis.VGMusicStudio.Core.NDS +{ + internal static class Utils + { + public const int ARM7_CLOCK = 16756991; // (33.513982 MHz / 2) == 16.756991 MHz == 16,756,991 Hz + } +} diff --git a/VG Music Studio.backup/Core/Player.cs b/VG Music Studio.backup/Core/Player.cs new file mode 100644 index 00000000..7a6e26f2 --- /dev/null +++ b/VG Music Studio.backup/Core/Player.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Core +{ + internal enum PlayerState : byte + { + Stopped = 0, + Playing, + Paused, + Recording, + ShutDown + } + + internal delegate void SongEndedEvent(); + + internal interface IPlayer : IDisposable + { + List[] Events { get; } + long MaxTicks { get; } + long ElapsedTicks { get; } + bool ShouldFadeOut { get; set; } + long NumLoops { get; set; } + + PlayerState State { get; } + event SongEndedEvent SongEnded; + + void LoadSong(long index); + void SetCurrentPosition(long ticks); + void Play(); + void Pause(); + void Stop(); + void Record(string fileName); + void GetSongState(UI.SongInfoControl.SongInfo info); + } +} diff --git a/VG Music Studio.backup/Core/SongEvent.cs b/VG Music Studio.backup/Core/SongEvent.cs new file mode 100644 index 00000000..7e518a3b --- /dev/null +++ b/VG Music Studio.backup/Core/SongEvent.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Core +{ + internal interface ICommand + { + Color Color { get; } + string Label { get; } + string Arguments { get; } + } + internal class SongEvent + { + public long Offset { get; } + public List Ticks { get; } = new List(); + public ICommand Command { get; } + + public SongEvent(long offset, ICommand command) + { + Offset = offset; + Command = command; + } + } +} diff --git a/VG Music Studio.backup/Core/VGMSDebug.cs b/VG Music Studio.backup/Core/VGMSDebug.cs new file mode 100644 index 00000000..91bc0919 --- /dev/null +++ b/VG Music Studio.backup/Core/VGMSDebug.cs @@ -0,0 +1,112 @@ +using Kermalis.EndianBinaryIO; +using Sanford.Multimedia.Midi; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Kermalis.VGMusicStudio.Core +{ +#if DEBUG + internal static class VGMSDebug + { + public static void MIDIVolumeMerger(string f1, string f2) + { + var midi1 = new Sequence(f1); + var midi2 = new Sequence(f2); + var baby = new Sequence(midi1.Division); + + for (int i = 0; i < midi1.Count; i++) + { + Track midi1Track = midi1[i]; + Track midi2Track = midi2[i]; + var babyTrack = new Track(); + baby.Add(babyTrack); + + for (int j = 0; j < midi1Track.Count; j++) + { + MidiEvent e1 = midi1Track.GetMidiEvent(j); + if (e1.MidiMessage is ChannelMessage cm1 && cm1.Command == ChannelCommand.Controller && cm1.Data1 == (int)ControllerType.Volume) + { + MidiEvent e2 = midi2Track.GetMidiEvent(j); + var cm2 = (ChannelMessage)e2.MidiMessage; + babyTrack.Insert(e1.AbsoluteTicks, new ChannelMessage(ChannelCommand.Controller, cm1.MidiChannel, (int)ControllerType.Volume, Math.Max(cm1.Data2, cm2.Data2))); + } + else + { + babyTrack.Insert(e1.AbsoluteTicks, e1.MidiMessage); + } + } + } + + baby.Save(f1); + baby.Save(f2); + } + + public static void EventScan(List songs, bool showIndexes) + { + Console.WriteLine($"{nameof(EventScan)} started."); + var scans = new Dictionary>(); + foreach (Config.Song song in songs) + { + try + { + Engine.Instance.Player.LoadSong(song.Index); + } + catch (Exception ex) + { + Console.WriteLine("Exception loading {0} - {1}", showIndexes ? $"song {song.Index}" : $"\"{song.Name}\"", ex.Message); + continue; + } + if (Engine.Instance.Player.Events != null) + { + foreach (string cmd in Engine.Instance.Player.Events.Where(ev => ev != null).SelectMany(ev => ev).Select(ev => ev.Command.Label).Distinct()) + { + if (scans.ContainsKey(cmd)) + { + scans[cmd].Add(song); + } + else + { + scans.Add(cmd, new List() { song }); + } + } + } + } + foreach (KeyValuePair> kvp in scans.OrderBy(k => k.Key)) + { + Console.WriteLine("{0} ({1})", kvp.Key, showIndexes ? string.Join(", ", kvp.Value.Select(s => s.Index)) : string.Join(", ", kvp.Value.Select(s => s.Name))); + } + Console.WriteLine($"{nameof(EventScan)} ended."); + } + + public static void GBAGameCodeScan(string path) + { + Console.WriteLine($"{nameof(GBAGameCodeScan)} started."); + var scans = new List(); + foreach (string file in Directory.GetFiles(path, "*.gba", SearchOption.AllDirectories)) + { + try + { + using (var reader = new EndianBinaryReader(File.OpenRead(file))) + { + string gameCode = reader.ReadString(3, false, 0xAC); + char regionCode = reader.ReadChar(0xAF); + byte version = reader.ReadByte(0xBC); + scans.Add(string.Format("Code: {0}\tRegion: {1}\tVersion: {2}\tFile: {3}", gameCode, regionCode, version, file)); + } + } + catch (Exception ex) + { + Console.WriteLine("Exception loading \"{0}\" - {1}", file, ex.Message); + } + } + foreach (string s in scans.OrderBy(s => s)) + { + Console.WriteLine(s); + } + Console.WriteLine($"{nameof(GBAGameCodeScan)} ended."); + } + } +#endif +} diff --git a/VG Music Studio/Dependencies/DLS2.dll b/VG Music Studio.backup/Dependencies/DLS2.dll similarity index 100% rename from VG Music Studio/Dependencies/DLS2.dll rename to VG Music Studio.backup/Dependencies/DLS2.dll diff --git a/VG Music Studio/Dependencies/Sanford.Multimedia.Midi.dll b/VG Music Studio.backup/Dependencies/Sanford.Multimedia.Midi.dll similarity index 100% rename from VG Music Studio/Dependencies/Sanford.Multimedia.Midi.dll rename to VG Music Studio.backup/Dependencies/Sanford.Multimedia.Midi.dll diff --git a/VG Music Studio/Dependencies/SoundFont2.dll b/VG Music Studio.backup/Dependencies/SoundFont2.dll similarity index 100% rename from VG Music Studio/Dependencies/SoundFont2.dll rename to VG Music Studio.backup/Dependencies/SoundFont2.dll diff --git a/VG Music Studio.backup/MP2K.yaml b/VG Music Studio.backup/MP2K.yaml new file mode 100644 index 00000000..15660cec --- /dev/null +++ b/VG Music Studio.backup/MP2K.yaml @@ -0,0 +1,1443 @@ +A2UJ_00: + Name: "Mother 1 + 2 (Japan)" + SongTableOffsets: 0x10B530 + SongTableSizes: 451 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +A3UJ_00: + Name: "Mother 3 (Japan)" + SongTableOffsets: 0x120E94 + SongTableSizes: 1968 + SampleRate: 4 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +A4AE_00: + Name: "The Simpson's Road Rage (USA)" + SongTableOffsets: 0x125FD8 + SongTableSizes: 112 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 11 + HasGoldenSunSynths: False + HasPokemonCompression: False +A4AX_00: + Name: "The Simpson's Road Rage (Europe)" + SongTableOffsets: 0x1281D0 + SongTableSizes: 113 + Copy: "A4AE_00" +A7KE_00: + Name: "Kirby: Nightmare in Dream Land (USA)" + SongTableOffsets: 0x60B460 + SongTableSizes: 579 + SampleRate: 4 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False + Playlists: + Sound Test (Music): + 26: "0 - Title Screen" + 40: "1 - File Select" + 0: "2 - Vegetable Valley" + 21: "3 - Ice Cream Island" + 11: "4 - Butter Building" + 17: "5 - Grape Garden" + 22: "6 - Yogurt Yard" + 12: "7 - Orange Ocean" + 18: "8 - Rainbow Resort" + 15: "9 - Fortified Fronts" + 2: "10 - Sky Stages" + 37: "11 - Mini-Boss Fight" + 30: "12 - Boss Fight" + 1: "13 - Boss Defeated!" + 14: "14 - Victory Dance 1" + 13: "15 - Victory Dance 2" + 39: "16 - Arena" + 25: "17 - Museum" + 38: "18 - King Dedede's Theme" + 20: "19 - Green Greens Theme" + 27: "20 - Fountain of Dreams" + 31: "21 - Returning the Star Rod!" + 32: "22 - Nightmare's Power Orb" + 33: "23 - Nightmare's Pursuit" + 34: "24 - Fighting the Nightmare!" + 24: "25 - The End!" + 3: "26 - Down!" + 4: "27 - Level 1: Vegetable Valley" + 5: "28 - Level 2: Ice Cream Island" + 6: "29 - Level 3: Butter Building" + 7: "30 - Level 4: Grape Garden" + 8: "31 - Level 5: Yogurt Yard" + 9: "32 - Level 6: Orange Ocean" + 10: "33 - Level 7: Rainbow Resort" + 35: "34 - Minigame: Quick Draw" + 46: "35 - Minigame: Bomb Rally" + 42: "36 - Minigame: Kirby's Air Grind" + 43: "37 - Minigame: Kirby's Air Grind (Goal!)" + 44: "38 - Minigame: Kirby's Air Grind (Results)" + 23: "39 - Minigame Result: Bad!" + 28: "40 - Minigame Result: Good!" + 29: "41 - Minigame Result: Perfect!" + 19: "42 - Invincible Candy" + 16: "43 - Game Over" + Other Music: + 47: "Vegetable Valley (No Intro)" + 53: "Ice Cream Island (No Intro)" + 48: "Butter Building (No Intro)" + 50: "Grape Garden (No Intro)" + 54: "Yogurt Yard (No Intro)" + 49: "Orange Ocean (No Intro)" + 51: "Rainbow Resort (No Intro)" + 55: "Fortified Fronts (No Intro)" + 52: "Green Greens (No Intro)" + 41: "Building of Battles (Outside)" + 56: "Nightmare Appears!" +A7KJ_00: + Name: "Kirby: Nightmare in Dream Land (Japan)" + SongTableOffsets: 0x60F36C + Copy: "A7KE_00" +A7KP_00: + Name: "Kirby: Nightmare in Dream Land (Europe)" + SongTableOffsets: 0x843B50 + Copy: "A7KE_00" +A88E_00: + Name: "Mario & Luigi - Superstar Saga (USA)" + SongTableOffsets: 0xFB5DA4 + SongTableSizes: 323 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +A88J_00: + Name: "Mario & Luigi - Superstar Saga (Japan)" + SongTableOffsets: 0xFB5E00 + Copy: "A88E_00" +A88P_00: + Name: "Mario & Luigi - Superstar Saga (Europe)" + Copy: "A88E_00" +AE7E_00: + Name: "Fire Emblem: The Blazing Blade (USA)" + SongTableOffsets: 0x69D6D8 + SongTableSizes: 1001 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 13 + HasGoldenSunSynths: False + HasPokemonCompression: False +AE7J_00: + Name: "Fire Emblem: The Blazing Blade (Japan)" + SongTableOffsets: 0x6EA8C8 + Copy: "AE7E_00" +AE7X_00: + Name: "Fire Emblem: The Blazing Blade (Europe)" + SongTableOffsets: 0x6805F4 + Copy: "AE7E_00" +AE7Y_00: + SongTableOffsets: 0x67F1F4 + Copy: "AE7X_00" +AEFJ_00: + Name: "Fire Emblem: The Binding Blade (Japan)" + SongTableOffsets: 0x3994D8 + SongTableSizes: 621 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 13 + HasGoldenSunSynths: False + HasPokemonCompression: False +AFXE_00: + Name: "Final Fantasy Tactics Advance (USA)" + SongTableOffsets: 0x14BB48 + SongTableSizes: 640 + SampleRate: 4 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False + Playlists: + Music (OST Order): + 42: "Main Theme" + 24: "Snow Dancing in the Schoolyard" + 33: "Companions That Surpassed Their Tribe" + 20: "Magic Beast Farm" + 17: "Crystal" + 25: "Undeniable Anxiety" + 36: "Amber Valley" + 6: "Bell of Victory (Fanfare)" + 15: "At the Bar" + 12: "Different World Ivalice" + 10: "Engange (Fanfare)" + 0: "Gathering Allies" + 11: "Walking in Ivalice" + 4: "Wind of Hope (Fanfare)" + 14: "Teach Me, Mont Blanc" + 9: "Wounded Comrades (Fanfare)" + 39: "Undefeated Heart" + 2: "Gained Fruit (Fanfare)" + 28: "Marche" + 37: "Painful Battle" + 5: "Notice of Retreat (Fanfare)" + 7: "Sleep of Defeat (Fanfare)" + 21: "Prison" + 40: "Surpassing The Wall" + 3: "Exhausted Ones (Fanfare)" + 29: "Mewt" + 35: "Battle Of Hope" + 8: "Level Up (Fanfare)" + 22: "Law Card" + 30: "Ritz" + 19: "Mysterious Shop" + 34: "The Road We Both Aim For" + 16: "Wind of Liberation (Fanfare)" + 27: "Confusion" + 23: "Judge (Fanfare)" + 38: "Beyond The Wasteland" + 26: "The World Starting To Move" + 41: "Unavoidable Destiny" + 45: "Incarnation" + 31: "Vanishing World" + 32: "A Place We Should Return To" + 47: "Fulfilled Dream Segment" + Music (Bonus): + 13: "Main Theme (Beta Version)" + 48: "St Ivalice Turns Into Ivalice" + 49: "Save Menu Theme" +AFXJ_00: + Name: "Final Fantasy Tactics Advance (Japan)" + SongTableOffsets: 0x1401A8 + Copy: "AFXE_00" +AFXP_00: + Name: "Final Fantasy Tactics Advance (Europe)" + SongTableOffsets: 0x14F540 + Copy: "AFXE_00" +AGFD_00: + Name: "Golden Sun: The Lost Age (Germany)" + Copy: "AGFE_00" +AGFE_00: + Name: "Golden Sun: The Lost Age (USA)" + SongTableOffsets: 0x1C4530 + SongTableSizes: 800 + SampleRate: 8 + ReverbType: "Camelot2" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: True + HasPokemonCompression: False +AGFF_00: + Name: "Golden Sun: The Lost Age (France)" + Copy: "AGFE_00" +AGFI_00: + Name: "Golden Sun: The Lost Age (Italy)" + Copy: "AGFE_00" +AGFJ_00: + Name: "Golden Sun: The Lost Age (Japan)" + Copy: "AGFE_00" +AGFS_00: + Name: "Golden Sun: The Lost Age (Spain)" + Copy: "AGFE_00" +AGLJ_00: + Name: "Tomato Adventure (Japan)" + SongTableOffsets: 0x4B11A4 + SongTableSizes: 244 + SampleRate: 4 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +AGSD_00: + Name: "Golden Sun (Germany)" + SongTableOffsets: 0xFD484 + Copy: "AGSE_00" +AGSE_00: + Name: "Golden Sun (USA)" + SongTableOffsets: 0xFC684 + SongTableSizes: 312 + SampleRate: 6 + ReverbType: "Camelot1" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: True + HasPokemonCompression: False +AGSF_00: + Name: "Golden Sun (France)" + SongTableOffsets: 0xFEE84 + Copy: "AGSE_00" +AGSI_00: + Name: "Golden Sun (Italy)" + Copy: "AGSE_00" +AGSJ_00: + Name: "Golden Sun (Japan)" + SongTableOffsets: 0xF3684 + Copy: "AGSE_00" +AGSS_00: + Name: "Golden Sun (Spain)" + SongTableOffsets: 0xFEE84 + Copy: "AGSE_00" +AINJ_00: + Name: "Initial D: Another Stage (Japan)" + SongTableOffsets: 0x648B38 + SongTableSizes: 88 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 13 + HasGoldenSunSynths: False + HasPokemonCompression: False +AMAC_00: + Name: "Super Mario Advance (China)" + SongTableOffsets: 0xF64C4 + Copy: "AMZE_00" +AMAE_00: + Name: "Super Mario Advance (Europe)" + SongTableOffsets: 0xF4368 + Copy: "AMZE_00" +AMAJ_00: + Name: "Super Mario Advance (Japan)" + SongTableOffsets: 0xF3F18 + Copy: "AMZE_00" +AMKE_00: + Name: "Mario Kart: Super Circuit (USA)" + SongTableOffsets: 0x102498 + SongTableSizes: 403 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False + Playlists: + Music: + 2: "Main Menu" + 3: "Race Intro (Grand Prix)" + 4: "Race Intro (Time Trial / Quick Race)" + 5: "1st Place" + 6: "2nd-4th Place" + 7: "5th Place and Under" + 10: "Starman" + 11: "Lightning" + 12: "Ghost" + 13: "Double Ghost" + 14: "1st Place Results" + 15: "2nd-4th Place Results" + 16: "5th Place and Under Results" + 17: "Quick Race Results" + 18: "Battle Mode" + 19: "Final Lap" + 20: "20" + 21: "Shy Guy Beach / Cheep-Cheep Island" + 22: "Bowser's Castles" + 23: "Cheese Land" + 24: "Riverside Park" + 25: "Mario/Peach/Luigi Circuit" + 26: "Sunset Wilds" + 27: "Boo Lake / Broken Pier" + 28: "Rainbow Road" + 29: "Yoshi Desert" + 30: "Ribbon Road" + 31: "Snow Land" + 32: "Lakeside Park" + 33: "Battle Mode Win" + 34: "Battle Mode Lose" + 35: "Sky Garden" + 36: "Rainbow Road (Intro)" + 37: "Ghost Data Trading" + 40: "Connection Screen" + 41: "SNES Mario Circuit" + 42: "SNES Donut Plains" + 43: "SNES Choco Island" + 44: "SNES Vanilla Lake" + 45: "SNES Ghost Valley" + 46: "SNES Rainbow Road" + 47: "SNES Bowser's Castle" + 48: "SNES Koopa Beach" + 51: "Night Opening" + 52: "Title Screen" + 53: "Opening" + 54: "Credits" + 55: "Award Ceremony (Intro)" + 56: "Award Ceremony" + 57: "Award Ceremony Lose" + 59: "59" + 60: "60" +AMKJ_00: + Name: "Mario Kart: Super Circuit (Japan)" + SongTableOffsets: 0x1245C8 + Copy: "AMKE_00" +AMKP_00: + Name: "Mario Kart: Super Circuit (Europe)" + Copy: "AMKE_00" +AMTC_00: + Name: "Metroid Fusion (China)" + SongTableOffsets: 0xAB0E4 + Copy: "AMTE_00" +AMTE_00: + Name: "Metroid Fusion (USA)" + SongTableOffsets: 0xA8D3C + SongTableSizes: 745 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +AMTJ_00: + Name: "Metroid Fusion (Japan)" + SongTableOffsets: 0xAB0A0 + Copy: "AMTE_00" +AMTP_00: + Name: "Metroid Fusion (Europe)" + SongTableOffsets: 0xA9398 + Copy: "AMTE_00" +AMZE_00: + Name: "Super Mario Advance (USA)" + SongTableOffsets: 0xF40D4 + SongTableSizes: 447 + SampleRate: 2 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +AWAC_00: + Name: "Wario Land 4 (China)" + SongTableOffsets: 0x98CB8 + Copy: "AWAE_00" +AWAE_00: + Name: "Wario Land 4 (USA)" + SongTableOffsets: 0x98028 + SongTableSizes: 819 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False + Playlists: + Music: + 635: "Intro" + 636: "Work It!" + 638: "Enter the Pyramid" + 639: "Map" + 672: "Hall of Hieroglyphs" + 651: "Palm Tree Paradise" + 652: "Palm Tree Paradise (Caves)" + 653: "Palm Tree Paradise (Back From Caves)" + 654: "Wildflower Fields" + 655: "Mystic Lake (Lakeside)" + 656: "Mystic Lake (Inside Jungle)" + 657: "Mystic Lake (Back to Lakeside)" + 658: "Monsoon Jungle" + 659: "The Curious Factory" + 660: "The Toxic Landfill" + 662: "40 Below Fridge" + 661: "Pinball Zone" + 663: "Toy Block Tower" + 664: "The Big Board" + 665: "Doodle Woods" + 666: "Domino Row" + 667: "Crescent Moon Village" + 668: "Arabian Night" + 670: "Fiery Cavern" + 669: "Hotel Horror" + 671: "Golden Passage" + 617: "Wario's Workout" + 673: "Puzzle Room" + 674: "Puzzle Room (2)" + 217: "Hurry Up Time Limit" + 681: "Hurry Up!!!" + 450: "Only a Few Seconds Left!!!" + 452: "Coin Loss..." + 642: "Inside the Void" + 342: "Four Pieces" + 594: "High Score" + 448: "Door to Next Stage Opens" + 457: "Door to Next Stage Opens (2)" + 421: "Stage Already Clear" + 447: "Stage Already Clear (2)" + 475: "Can't Enter Boss Room" + 425: "Boss Room Opens" + 473: "Enter Boss Room" + 474: "Track 37" + 640: "Boss Room" + 682: "Item Shop" + 493: "Boss's Presence" + 683: "Here Comes the Mystery Man" + 684: "Here Comes the Mystery Man (Shorter Version)" + 508: "Bugle" + 511: "Large Lips" + 512: "Large Lips (Longer Version)" + 513: "Random Damage Sound" + 124: "Boss is Ready" + 207: "Boss is Ready (Shorter Version)" + 687: "Boss" + 429: "Treasures (Part 1)" + 430: "Treasures (Part 2)" + 431: "Treasures (Part 3)" + 422: "Victory" + 432: "Victory (2)" + 701: "Mini Game Shop" + 709: "Wario's Homerun Derby" + 710: "Homerun!" + 715: "Cheerleaders (Part 1)" + 716: "Cheerleaders (Part 2)" + 702: "The Wario Hop Begins" + 707: "The Wario Hop Begins (No Loop)" + 703: "The Wario Hop 1" + 704: "The Wario Hop 2" + 705: "The Wario Hop 3" + 706: "The Wario Hop 4" + 708: "Wario's Roulette" + 711: "Failure 1" + 712: "Failure 2" + 713: "Failure 3" + 714: "Failure 4" + 593: "Mini Game High Score" + 191: "Golden Diva Appears" + 183: "Golden Diva is Ready" + 688: "Golden Diva" + 689: "Golden Diva's Lip is Left" + 489: "All of my Treasures!" + 800: "Escape from the Pyramid" + 801: "Track 68" + 802: "Thank You Wario" + 803: "Japanese Ending Song" + 806: "Japanese Ending Song (2)" + 809: "Japanese Ending Song (3)" + 812: "Japanese Ending Song (4)" + 815: "English Ending Song" + 816: "English Ending Song (2)" + 817: "English Ending Song (3)" + 818: "English Engind Song (4)" + 814: "The Big Board (Ending Mix)" + 813: "Mystic Lake (Ending Mix)" + 811: "Wildflower Fields (Ending Mix)" + 810: "Hall of Hieroglyphs (Ending Mix)" + 808: "Doodle Woods (Ending Mix)" + 807: "Crescent Moon Village (Ending Mix)" + 805: "Monsoon Jungle (Ending Mix)" + 804: "Toy Blick Tower (Ending Mix)" + 618: "Sound Room" + 619: "About That Shepherd" + 620: "Things That Never Change" + 621: "Tomorrow's Blood Pressure" + 622: "Beyond the Headrush" + 623: "Driftwood & The Island Dog" + 624: "The Judge's Feet" + 625: "The Moon's Lamppost" + 626: "Soft Shell" + 627: "So Sleepy" + 628: "The Short Futon" + 629: "Avocado Song" + 630: "Mr. Fly" + 631: "Yesterday's Words" + 632: "The Errand" + 633: "You and Your Shoes" + 634: "Mr. Ether and Planaria" + 727: "Medamayaki (Karaoke)" + 728: "Medamayaki (Karaoke) (2)" + 729: "Medamayaki (Voice)" +AWAJ_00: + Name: "Wario Land 4 (Japan)" + Copy: "AWAE_00" +AXPD_00: + Name: "Pokémon Sapphire Version (Germany)" + SongTableOffsets: 0x463394 + Copy: "AXVD_00" +AXPD_01: + Copy: "AXPD_00" +AXPE_00: + Name: "Pokémon Sapphire Version (USA)" + SongTableOffsets: 0x4554E8 + Copy: "AXVE_00" +AXPE_01: + SongTableOffsets: 0x455500 + Copy: "AXPE_00" +AXPE_02: + Copy: "AXPE_01" +AXPF_00: + Name: "Pokémon Sapphire Version (France)" + SongTableOffsets: 0x45E094 + Copy: "AXVF_00" +AXPF_01: + Copy: "AXPF_00" +AXPI_00: + Name: "Pokémon Sapphire Version (Italy)" + SongTableOffsets: 0x4574B4 + Copy: "AXVI_00" +AXPI_01: + Copy: "AXPI_00" +AXPJ_00: + Name: "Pokémon Sapphire Version (Japan)" + SongTableOffsets: 0x416ECC + Copy: "AXVJ_00" +AXPS_00: + Name: "Pokémon Sapphire Version (Spain)" + SongTableOffsets: 0x45A660 + Copy: "AXVS_00" +AXPS_01: + Copy: "AXPS_00" +AXVD_00: + Name: "Pokémon Ruby Version (Germany)" + SongTableOffsets: 0x463428 + Copy: "AXVE_00" +AXVD_01: + Copy: "AXVD_00" +AXVE_00: + Name: "Pokémon Ruby Version (USA)" + SongTableOffsets: 0x45548C + SongTableSizes: 468 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 12 + HasGoldenSunSynths: False + HasPokemonCompression: True + Playlists: + Disc 1: + 414: "Opening Movie: Setting out on a Journey in the Hoenn Region" + 442: "Opening Movie: Double Battles" + 413: "Title Screen" + 374: "Introductions" + 405: "Littleroot Town" + 383: "Birch Pokémon Lab" + 415: "May" + 410: "H-Help Me!" + 457: "Battle! (Wild Pokémon)" + 353: "Victory! (Wild Pokémon)" + 359: "Route 101" + 363: "Oldale Town" + 400: "Pokémon Center" + 368: "Pokémon Healed" + 380: "Trainers' Eyes Meet (Youngster)" + 407: "Trainers' Eyes Meet (Lass)" + 459: "Battle! (Trainer Battle)" + 412: "Victory! (Trainer Battle)" + 367: "Level Up!" + 362: "Petalburg City" + 420: "Hurry Along" + 401: "Route 104" + 366: "Petalburg Woods" + 441: "Team Magma Appears!" + 458: "Battle! (Team Aqua / Team Magma)" + 424: "Victory! (Team Aqua / Team Magma)" + 399: "Rustboro City" + 435: "Trainers' School" + 431: "Crossing the Sea" + 427: "Dewford Town" + 379: "Trainers' Eyes Meet (Tuber♀)" + 433: "Slateport City" + 375: "Oceanic Museum" + 360: "Route 110" + 403: "Cycling" + 426: "Game Corner" + 390: "Win" + 391: "Lose" + 392: "Reel Time" + 389: "Jackpot" + 398: "Verdanturf Town" + 418: "Route 113" + 449: "Twins" + 437: "Fallarbor Town" + 425: "Cable Car" + 406: "Mt. Chimney" + 451: "Trainers' Eyes Meet (Hiker)" + 409: "Route 111" + 364: "Pokémon Gym" + 460: "Battle! (Gym Leader)" + 354: "Victory! (Gym Leader)" + 369: "Obtained a Badge!" + 372: "Obtained a TM!" + 365: "Surf" + Disc 2: + 402: "Route 119" + 382: "Fortree City" + 361: "Route 120" + 453: "Interviewers" + 428: "Safari Zone" + 397: "Trainers' Eyes Meet (Gentleman)" + 408: "Lilycove City" + 373: "Museum" + 378: "Move Deleted" + 421: "Brendan" + 464: "Battle! (Brendan/May)" + 376: "Evolution (Intro)" + 377: "Evolution" + 371: "Congratulations! Your Pokémon Evolved!" + 404: "Poké Mart" + 432: "Mt. Pyre" + 416: "Trainers' Eyes Meet (Psychic)" + 423: "Trainers' Eyes Meet (Hex Maniac)" + 434: "Mt. Pyre Exterior" + 430: "Hideout" + 370: "Obtained an Item!" + 419: "Team Aqua Appears!" + 466: "Battle! (Team Aqua/Team Magma Leaders)" + 388: "The Super-Ancient Pokémon Awaken!" + 444: "Drought" + 443: "Heavy Rain" + 411: "Dive" + 445: "Sootopolis City" + 386: "Cave of Origin" + 463: "Battle! (Super-Ancient Pokémon)" + 385: "Trainers' Eyes Meet (Swimmer♀)" + 422: "Evergrande City" + 387: "Obtained a Berry!" + 452: "Contest Lobby" + 440: "Pokémon Contest!" + 446: "Results Announcement" + 439: "Contest Winner" + 438: "Sealed Chamber" + 462: "Battle! (Regirock/Regice/Registeel)" + 448: "The Trick House" + 381: "Abandoned Ship" + 384: "Battle Tower" + 429: "Victory Road" + 417: "Trainers' Eyes Meet (Cooltrainer)" + 450: "The Elite Four Appear!" + 465: "Battle! (Elite Four)" + 454: "Champion Steven" + 461: "Battle! (Steven)" + 355: "Victory! (Steven)" + 447: "Room of Glory" + 436: "The Hall of Fame" + 455: "Ending Theme" + 456: "The End" + Other Music: + 350: "Unused - TETSUJI" + 351: "Unused - Route 38" + 352: "Victory! (Wild Pokémon) (No Intro)" + 356: "Unused - Pokémon Center (2)" + 357: "Unused - Viridian City" + 358: "Unused - Battle! (Entei/Raikou/Suicune)" + 393: "Pokémon Contest! (Multiplayer - Player 1)" + 394: "Pokémon Contest! (Multiplayer - Player 2)" + 395: "Pokémon Contest! (Multiplayer - Player 3)" + 396: "Pokémon Contest! (Multiplayer - Player 4)" + 467: "Unused - Team Rocket Appears!" +AXVE_01: + SongTableOffsets: 0x4554A0 + Copy: "AXVE_00" +AXVE_02: + Copy: "AXVE_01" +AXVF_00: + Name: "Pokémon Ruby Version (France)" + SongTableOffsets: 0x45E564 + Copy: "AXVE_00" +AXVF_01: + Copy: "AXVF_00" +AXVI_00: + Name: "Pokémon Ruby Version (Italy)" + SongTableOffsets: 0x457810 + Copy: "AXVE_00" +AXVI_01: + Copy: "AXVI_00" +AXVJ_00: + Name: "Pokémon Ruby Version (Japan)" + SongTableOffsets: 0x416EE4 + Copy: "AXVE_00" +AXVS_00: + Name: "Pokémon Ruby Version (Spain)" + SongTableOffsets: 0x45A924 + Copy: "AXVE_00" +AXVS_01: + Copy: "AXVS_00" +AZLE_00: + Name: "The Legend of Zelda: A Link to the Past and Four Swords (USA)" + SongTableOffsets: 0x3C3BBC + SongTableSizes: 545 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +AZLJ_00: + Name: "The Legend of Zelda: A Link to the Past and Four Swords (Japan)" + SongTableOffsets: 0x3E7A54 + Copy: "AZLE_00" +AZLP_00: + Name: "The Legend of Zelda: A Link to the Past and Four Swords (Europe)" + SongTableOffsets: 0x43F8AC + Copy: "AZLE_00" +B24E_00: + Name: "Pokémon Mystery Dungeon: Red Rescue Team (USA)" + SongTableOffsets: 0x1E866BC + SongTableSizes: 940 + SampleRate: 5 + ReverbType: "Normal" + Reverb: 0 + Volume: 14 + HasGoldenSunSynths: False + HasPokemonCompression: False + Playlists: + Disc 1: + 40: "Intro" + 43: "Title" + 8: "File Select" + 4: "Personality Test" + 101: "Awakening" + 10: "There's Trouble!" + 125: "Tiny Woods" + 114: "At the End of the Road" + 103: "Happiness" + 1: "Rescue Team Base" + 46: "Insert Title" + 14: "Thunderwave Cave" + 12: "Dream" + 7: "Pokémon Plaza" + 107: "Friend Area ~ Field" + 105: "Friend Area ~ Steppe" + 121: "Friend Area ~ Forest" + 116: "Friend Area ~ Wilds" + 2: "Friend Area ~ Swamp" + 16: "Friend Area ~ Pond" + 120: "Mt. Steel" + 11: "Versus Boss" + 112: "Friend Area ~ Lab" + 110: "Makuhita Dojo" + 15: "Sinister Woods" + 9: "Danger" + 113: "Silent Chasm" + 111: "Mt. Thunder" + 126: "Mt. Thunder Peak" + 20: "Great Canyon" + 6: "The Legend of Ninetales" + 24: "Run Away" + Disc 2: + 102: "Lapis Cave" + 38: "The Mountain of Fire" + 25: "Mt. Blaze" + 17: "Kecleon Shop" + 18: "Stop! Thief!" + 33: "Mt. Blaze Peak" + 36: "Escape Through the Snow" + 104: "Frosty Forest" + 39: "Frosty Grotto" + 5: "Benevolent Spirit" + 115: "Mt. Freeze" + 123: "Mt. Freeze Peak" + 108: "Magma Cavern" + 128: "Monster House!" + 124: "Magma Cavern Pit" + 19: "World Calamity" + 29: "Dream Eater" + 22: "Sky Tower" + 23: "Sky Tower Summit" + 26: "Rayquaza's Domain" + 32: "Versus Legendary" + 37: "The Other Side" + 42: "Farewell" + 44: "Staff Roll" + 45: "Time of Reunion" + 106: "Friend Area ~ Oceanic" + 28: "Friend Area ~ Rainbow Peak" + 3: "Friend Area ~ Caves" + 35: "Friend Area ~ Cryptic Cave" + 118: "Friend Area ~ Southern Island" + 122: "Friend Area ~ Final Island" + 21: "Stormy Sea" + 13: "Buried Relic" + 117: "Friend Area ~ Legendary Island" + 30: "Friend Area ~ Deep-Sea Current" + 127: "Friend Area ~ Healing Forest" + 31: "Friend Area ~ Seafloor Cave" + 34: "Friend Area ~ Volcanic Pit" + 27: "Friend Area ~ Stratos Lookout" + 119: "Friend Area ~ Enclosed Island" + 100: "Unused 1" + 109: "Unused 2" + Other Music: + 41: "41" + 51: "Exploration Failed" + 52: "Exploration Complete!" + Dungeon Music: + 125: "Tiny Woods" + 14: "Thunderwave Cave" + 120: "Mt. Steel" + 110: "Makuhita Dojo" + 15: "Sinister Woods" + 113: "Silent Chasm" + 111: "Mt. Thunder" + 126: "Mt. Thunder Peak" + 20: "Great Canyon" + 102: "Lapis Cave" + 25: "Mt. Blaze" + 33: "Mt. Blaze Peak" + 104: "Frosty Forest" + 39: "Frosty Grotto" + 115: "Mt. Freeze" + 123: "Mt. Freeze Peak" + 108: "Magma Cavern" + 124: "Magma Cavern Pit" + 22: "Sky Tower" + 23: "Sky Tower Summit" + 21: "Stormy Sea" + 13: "Buried Relic" +B24J_00: + Name: "Pokémon Mystery Dungeon: Red Rescue Team (Japan)" + SongTableOffsets: 0x1DC66BC + Copy: "B24E_00" +B24P_00: + Name: "Pokémon Mystery Dungeon: Red Rescue Team (Europe)" + Copy: "B24E_00" +BE8E_00: + Name: "Fire Emblem: The Sacred Stones (USA)" + SongTableOffsets: 0x224470 + SongTableSizes: 1000 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 13 + HasGoldenSunSynths: False + HasPokemonCompression: False +BE8J_00: + Name: "Fire Emblem: The Sacred Stones (Japan)" + SongTableOffsets: 0x214120 + Copy: "BE8E_00" +BE8P_00: + Name: "Fire Emblem: The Sacred Stones (Europe)" + SongTableOffsets: 0x42FFB0 + Copy: "BE8E_00" +BMXC_00: + Name: "Metroid: Zero Mission (China)" + SongTableOffsets: 0xA8AAC + Copy: "BMXE_00" +BMXE_00: + Name: "Metroid: Zero Mission (USA)" + SongTableOffsets: 0x8F2C0 + SongTableSizes: 708 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +BMXJ_00: + Name: "Metroid: Zero Mission (Japan)" + SongTableOffsets: 0x8F31C + Copy: "BMXE_00" +BMXP_00: + Name: "Metroid: Zero Mission (Europe)" + SongTableOffsets: 0x8FF4C + Copy: "BMXE_00" +BPED_00: + Name: "Pokémon Emerald Version (Germany)" + SongTableOffsets: 0x6C5BDC + Copy: "BPEE_00" +BPEE_00: + Name: "Pokémon Emerald Version (USA)" + SongTableOffsets: 0x6B49F0 + SongTableSizes: 610 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 12 + HasGoldenSunSynths: False + HasPokemonCompression: True + Playlists: + Music: + 350: "Unused - TETSUJI" + 351: "Unused - Route 38" + 352: "Victory! (Wild Pokémon) (No Intro)" + 353: "Victory! (Wild Pokémon)" + 354: "Victory! (Gym Leader)" + 355: "Victory! (Wallace)" + 356: "Unused - Pokémon Center (2)" + 357: "Unused - Viridian City" + 358: "Unused - Battle! (Entei/Raikou/Suicune)" + 359: "Route 101" + 360: "Route 110" + 361: "Route 120" + 362: "Petalburg City" + 363: "Oldale Town" + 364: "Pokémon Gym" + 365: "Surf" + 366: "Petalburg Woods" + 367: "Level Up!" + 368: "Pokémon Healed" + 369: "Obtained a Badge!" + 370: "Obtained an Item!" + 371: "Congratulations! Your Pokémon Evolved!" + 372: "Obtained a TM!" + 373: "Museum" + 374: "Introductions" + 375: "Oceanic Museum" + 376: "Evolution (Intro)" + 377: "Evolution" + 378: "Move Deleted" + 379: "Trainers' Eyes Meet (Tuber♀)" + 380: "Trainers' Eyes Meet (Youngster)" + 381: "Abandoned Ship" + 382: "Fortree City" + 383: "Birch Pokémon Lab" + 384: "Battle Tower (RS)" + 385: "Trainers' Eyes Meet (Swimmer♀)" + 386: "Cave of Origin" + 387: "Obtained a Berry!" + 388: "The Super-Ancient Pokémon Awaken!" + 389: "Jackpot" + 390: "Win" + 391: "Lose" + 392: "Reel Time" + 393: "Pokémon Contest! (Multiplayer - Player 1)" + 394: "Pokémon Contest! (Multiplayer - Player 2)" + 395: "Pokémon Contest! (Multiplayer - Player 3)" + 396: "Pokémon Contest! (Multiplayer - Player 4)" + 397: "Trainers' Eyes Meet (Gentleman)" + 398: "Verdanturf Town" + 399: "Rustboro City" + 400: "Pokémon Center" + 401: "Route 104" + 402: "Route 119" + 403: "Cycling" + 404: "Poké Mart" + 405: "Littleroot Town" + 406: "Mt. Chimney" + 407: "Trainers' Eyes Meet (Lass)" + 408: "Lilycove City" + 409: "Route 111" + 410: "H-Help Me!" + 411: "Dive" + 412: "Victory! (Trainer Battle)" + 413: "Title Screen" + 414: "Opening Movie: Setting out on a Journey in the Hoenn Region" + 415: "May" + 416: "Trainers' Eyes Meet (Psychic)" + 417: "Trainers' Eyes Meet (Cooltrainer)" + 418: "Route 113" + 419: "Team Aqua Appears!" + 420: "Hurry Along" + 421: "Brendan" + 422: "Evergrande City" + 423: "Trainers' Eyes Meet (Hex Maniac)" + 424: "Victory! (Team Aqua / Team Magma)" + 425: "Cable Car" + 426: "Game Corner" + 427: "Dewford Town" + 428: "Safari Zone" + 429: "Victory Road" + 430: "Hideout" + 431: "Crossing the Sea" + 432: "Mt. Pyre" + 433: "Slateport City" + 434: "Mt. Pyre Exterior" + 435: "Trainers' School" + 436: "The Hall of Fame" + 437: "Fallarbor Town" + 438: "Sealed Chamber" + 439: "Contest Winner" + 440: "Pokémon Contest!" + 441: "Team Magma Appears!" + 442: "Opening Movie: Double Battles" + 443: "Heavy Rain" + 444: "The Drought" + 445: "Sootopolis City" + 446: "Results Announcement" + 447: "Room of Glory" + 448: "The Trick House" + 449: "Twins" + 450: "The Elite Four Appear!" + 451: "Trainers' Eyes Meet (Hiker)" + 452: "Contest Lobby" + 453: "Interviewers" + 454: "Champion Wallace" + 455: "Ending Theme" + 456: "The End" + 457: "Battle Frontier" + 458: "Battle Arena" + 459: "Obtained a Battle Point!" + 460: "Registered Trainer!" + 461: "Battle Pyramid" + 462: "Battle Pyramid Summit" + 463: "Battle Palace" + 464: "Rayquaza Enters!" + 465: "Battle Tower" + 466: "Obtained a Frontier Symbol!" + 467: "Battle Dome" + 468: "Battle Pike" + 469: "Battle Factory" + 470: "Battle! (Rayquaza)" + 471: "Battle! (Frontier Brain)" + 472: "Battle! (Mew)" + 473: "Battle Dome Lobby" + 474: "Battle! (Wild Pokémon)" + 475: "Battle! (Team Aqua / Team Magma)" + 476: "Battle! (Trainer Battle)" + 477: "Battle! (Gym Leader)" + 478: "Battle! (Champion Wallace)" + 479: "Battle! (Regirock/Regice/Registeel)" + 480: "Battle! (Groudon/Kyogre)" + 481: "Battle! (Brendan/May/Steven)" + 482: "Battle! (Elite Four)" + 483: "Battle! (Team Aqua/Team Magma Leaders)" + 484: "Guide (FRLG)" + 485: "Rocket Game Corner (FRLG)" + 486: "Rocket Hideout (FRLG)" + 487: "Pokémon Gym (FRLG)" + 488: "Jigglypuff's Song (FRLG)" + 489: "Opening Movie (FRLG)" + 490: "Title Screen (FRLG)" + 491: "Cinnabar Island Theme (FRLG)" + 492: "Lavender Town Theme (FRLG)" + 493: "Pokémon Healed (FRLG)" + 494: "Cycling (FRLG)" + 495: "A Trainer Appears (Bad Guy Version) (FRLG)" + 496: "A Trainer Appears (Girl Version) (FRLG)" + 497: "A Trainer Appears (Boy Version) (FRLG)" + 498: "Hall of Fame (FRLG)" + 499: "Viridian Forest (FRLG)" + 500: "Navel Rock" + 501: "Pokémon Mansion (FRLG)" + 502: "Ending Theme (FRLG)" + 503: "Road to Viridian City: Leaving Pallet Town (FRLG)" + 504: "Welcome to the World of Pokémon! (FRLG)" + 505: "Road to Cerulean City: Leaving Mt. Moon (FRLG)" + 506: "Road to Fuchsia City: Leaving Lavender Town (FRLG)" + 507: "The Final Road (FRLG)" + 508: "Battle! (Gym Leader Battle) (FRLG)" + 509: "Battle! (Trainer Battle) (FRLG)" + 510: "Battle! (Wild Pokémon) (FRLG)" + 511: "Final Battle! (Rival) (FRLG)" + 512: "Pallet Town Theme (FRLG)" + 513: "Professor Oak's Laboratory (FRLG)" + 514: "Professor Oak (FRLG)" + 515: "Pokémon Center (FRLG)" + 516: "The S.S. Anne (FRLG)" + 517: "The Sea (FRLG)" + 518: "Pokémon Tower (FRLG)" + 519: "Silph Co. (FRLG)" + 520: "Fuchsia City Theme (FRLG)" + 521: "Celadon City Theme (FRLG)" + 522: "Victory! (Trainer Battle) (FRLG)" + 523: "Victory! (Wild Pokémon) (FRLG)" + 524: "Victory! (Gym Leader Battle) (FRLG)" + 525: "Vermillion City Theme (FRLG)" + 526: "Pewter City Theme (FRLG)" + 527: "A Rival Appears (FRLG)" + 528: "A Rival Appears (No Intro) (FRLG)" + 529: "Fanfare: Professor Oak's Evaluation (FRLG)" + 530: "Fanfare: Pokémon Obtained (FRLG)" + 531: "Fanfare: Pokémon Caught" + 532: "Pokémon Printer (FRLG)" + 533: "Game Freak Logo (FRLG)" + 534: "Fanfare: Pokémon Caught (No Intro) (FRLG)" + 535: "Game Tutorial (1) (FRLG)" + 536: "Game Tutorial (2) (FRLG)" + 537: "Game Tutorial (3) (FRLG)" + 538: "Pokémon Jump (FRLG)" + 539: "The Union Room (FRLG)" + 540: "Pokémon Net Center (FRLG)" + 541: "Mystery Gift (FRLG)" + 542: "Dodrio Berry Picking (FRLG)" + 543: "Mt. Ember (FRLG)" + 544: "Teachy TV Lesson (FRLG)" + 545: "Sevii Islands (FRLG)" + 546: "Tanoby Chambers (FRLG)" + 547: "Sevii Islands: One, Two & Three Islands (FRLG)" + 548: "Sevii Islands: Four & Five Islands (FRLG)" + 549: "Sevii Islands: Six & Seven Islands (FRLG)" + 550: "The Poké Flute (FRLG)" + 551: "Battle! (Deoxys)" + 552: "Battle! (Mewtwo) (FRLG)" + 553: "Battle! (Ho-Oh/Lugia)" + 554: "Tense Battle! (FRLG)" + 555: "Deoxys Appears" + 556: "Trainer Tower (FRLG)" + 557: "Epilogue (FRLG)" + 558: "Teachy TV Menu (FRLG)" + Battle Music: + 358: "Unused - Battle! (Entei/Raikou/Suicune)" + 470: "Battle! (Rayquaza)" + 471: "Battle! (Frontier Brain)" + 472: "Battle! (Mew)" + 474: "Battle! (Wild Pokémon)" + 475: "Battle! (Team Aqua / Team Magma)" + 476: "Battle! (Trainer Battle)" + 477: "Battle! (Gym Leader)" + 478: "Battle! (Champion Wallace)" + 479: "Battle! (Regirock/Regice/Registeel)" + 481: "Battle! (Brendan/May/Steven)" + 482: "Battle! (Elite Four)" + 483: "Battle! (Team Aqua/Team Magma Leaders)" + 508: "Battle! (Gym Leader Battle) (FRLG)" + 509: "Battle! (Trainer Battle) (FRLG)" + 510: "Battle! (Wild Pokémon) (FRLG)" + 511: "Final Battle! (Rival) (FRLG)" + 551: "Battle! (Deoxys)" + 552: "Battle! (Mewtwo) (FRLG)" + 553: "Battle! (Ho-Oh/Lugia)" +BPEF_00: + Name: "Pokémon Emerald Version (France)" + SongTableOffsets: 0x6B890C + Copy: "BPEE_00" +BPEI_00: + Name: "Pokémon Emerald Version (Italy)" + SongTableOffsets: 0x6B1004 + Copy: "BPEE_00" +BPEJ_00: + Name: "Pokémon Emerald Version (Japan)" + SongTableOffsets: 0x63C2AC + Copy: "BPEE_00" +BPES_00: + Name: "Pokémon Emerald Version (Spain)" + SongTableOffsets: 0x6B6FA4 + Copy: "BPEE_00" +BPGD_00: + Name: "Pokémon Leaf Green Version (Germany)" + SongTableOffsets: 0x4A0A5C + Copy: "BPRD_00" +BPGE_00: + Name: "Pokémon Leaf Green Version (USA)" + SongTableOffsets: 0x4A2BA8 + Copy: "BPRE_00" +BPGE_01: + SongTableOffsets: 0x4A2C18 + Copy: "BPGE_00" +BPGF_00: + Name: "Pokémon Leaf Green Version (France)" + SongTableOffsets: 0x4980BC + Copy: "BPRF_00" +BPGI_00: + Name: "Pokémon Leaf Green Version (Italy)" + SongTableOffsets: 0x4964B8 + Copy: "BPRI_00" +BPGJ_00: + Name: "Pokémon Leaf Green Version (Japan)" + SongTableOffsets: 0x467D50 + Copy: "BPRJ_00" +BPGS_00: + Name: "Pokémon Leaf Green Version (Spain)" + SongTableOffsets: 0x4992C0 + Copy: "BPRS_00" +BPPE_00: + Name: "Pokémon Pinball: Ruby and Sapphire (USA)" + SongTableOffsets: 0x534E04 + SongTableSizes: 333 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 14 + HasGoldenSunSynths: False + HasPokemonCompression: False +BPPJ_00: + Name: "Pokémon Pinball: Ruby & Sapphire (Japan)" + SongTableOffsets: 0x51B9BC + Copy: "BPPE_00" +BPPJ_01: + SongTableOffsets: 0x51B708 + Copy: "BPPJ_00" +BPPP_00: + Name: "Pokémon Pinball: Ruby & Sapphire (Europe)" + SongTableOffsets: 0x7C67A4 + Copy: "BPPE_00" +BPRD_00: + Name: "Pokémon Fire Red Version (Germany)" + SongTableOffsets: 0x4A18F0 + Copy: "BPRE_00" +BPRE_00: + Name: "Pokémon Fire Red Version (USA)" + SongTableOffsets: 0x4A32CC + SongTableSizes: 347 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 12 + HasGoldenSunSynths: False + HasPokemonCompression: True + Playlists: + Disc 1: + 321: "Game Freak Logo" + 277: "Opening Movie" + 278: "Title Screen" + 323: "Game Tutorial (1)" + 324: "Game Tutorial (2)" + 325: "Game Tutorial (3)" + 292: "Welcome to the World of Pokémon!" + 300: "Pallet Town Theme" + 302: "Professor Oak" + 301: "Professor Oak's Laboratory" + 318: "Fanfare: Pokémon Obtained" + 315: "A Rival Appears" + 297: "Battle! (Trainer Battle)" + 310: "Victory! (Trainer Battle)" + 291: "Road to Viridian City: Leaving Pallet Town" + 257: "Fanfare: Item Obtained (Version 1)" + 314: "Pewter City Theme" + 346: "Teachy TV Menu" + 287: "Viridian Forest" + 298: "Battle! (Wild Pokémon)" + 311: "Victory! (Wild Pokémon)" + 285: "A Trainer Appears (Boy Version)" + 303: "Pokémon Center" + 276: "Jigglypuff's Song" + 256: "Pokémon Healed" + 272: "Guide" + 275: "Pokémon Gym" + 342: "Tense Battle!" + 296: "Battle! (Gym Leader Battle)" + 312: "Victory! (Gym Leader Battle)" + 260: "Fanfare: Badge Obtained" + 263: "Evolution (Intro)" + 264: "Evolution" + 259: "Fanfare: Evolution" + 293: "Road to Cerulean City: Leaving Mt. Moon" + 284: "A Trainer Appears (Girl Version)" + 288: "Caves of Mt. Moon" + 313: "Vermilion City Theme" + 304: "The S.S. Anne" + 282: "Cycling" + 280: "Lavender Town Theme" + 306: "Pokémon Tower" + 309: "Celadon City Theme" + 273: "Rocket Game Corner" + 268: "Fanfare: Winning" + 269: "Fanfare: Jackpot" + 320: "Pokémon Printer" + 274: "Rocket Hideout" + 283: "A Trainer Appears (Bad Guy Version)" + 307: "Silph Co." + 294: "Road to Fuchsia City: Leaving Lavender Town" + 338: "The Poké Flute" + 308: "Fuchsia City Theme" + 270: "Move Deleted" + 305: "The Sea" + 341: "Battle! (Legendary Pokémon)" + 319: "Fanfare: Pokémon Caught" + 279: "Cinnabar Island Theme" + 289: "Pokémon Mansion" + 328: "Pokémon Net Center" + 317: "Fanfare: Professor Oak's Evaluation" + 336: "Sevii Islands: Four & Five Islands" + 326: "Pokémon Jump" + 330: "Dodrio Berry Picking" + 271: "Too Bad..." + 333: "Sevii Islands" + 337: "Sevii Islands: Six & Seven Islands" + 327: "The Union Room" + 329: "Mystery Gift" + 258: "Fanfare: Item Obtained (Version 2)" + 340: "Battle! (Mewtwo)" + 295: "The Final Road" + 299: "Final Battle! (Rival)" + 345: "Epilogue" + 286: "Hall of Fame" + 290: "Ending Theme" + Disc 2: + 343: "Deoxys Appears" + 339: "Battle! (Deoxys)" + Other Music: + 261: "Unused - Obtained a TM! (RS)" + 262: "Unused - Obtained a Berry! (RS)" + 265: "Battle! (Link Battle) (1)" + 266: "Battle! (Link Battle) (2)" + 267: "Unused - Trainers' School (RS)" + 281: "Unused - Pokémon Healed (2)" + 316: "A Rival Appears (No Intro)" + 322: "Fanfare: Pokémon Caught (No Intro)" + 331: "Mt. Ember" + 332: "Teachy TV Lesson" + 334: "Tanoby Chambers" + 335: "Sevii Islands: One, Two & Three Islands" + 344: "Trainer Tower" +BPRE_01: + SongTableOffsets: 0x4A332C + Copy: "BPRE_00" +BPRF_00: + Name: "Pokémon Fire Red Version (France)" + SongTableOffsets: 0x499394 + Copy: "BPRE_00" +BPRI_00: + Name: "Pokémon Fire Red Version (Italy)" + SongTableOffsets: 0x496D20 + Copy: "BPRE_00" +BPRJ_00: + Name: "Pokémon Fire Red Version (Japan)" + SongTableOffsets: 0x467F0C + Copy: "BPRE_00" +BPRJ_01: + SongTableOffsets: 0x463708 + Copy: "BPRJ_00" +BPRS_00: + Name: "Pokémon Fire Red Version (Spain)" + SongTableOffsets: 0x499BC8 + Copy: "BPRE_00" +BZME_00: + Name: "The Legend of Zelda: The Minish Cap (USA)" + SongTableOffsets: 0xA11DBC + SongTableSizes: 546 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +BZMJ_00: + Name: "The Legend of Zelda: The Minish Cap (Japan)" + SongTableOffsets: 0x9F3D3C + Copy: "BZME_00" +BZMP_00: + Name: "The Legend of Zelda: The Minish Cap (Europe)" + SongTableOffsets: 0xB1D414 + Copy: "BZME_00" +U32E_00: + Name: "Boktai 2 - Solar Boy Django (USA)" + SongTableOffsets: 0x25EEA8 + SongTableSizes: 1000 + SampleRate: 6 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +U32P_00: + Name: "Boktai 2 - Solar Boy Django (Europe)" + SongTableOffsets: 0x264758 + Copy: "U32E_00" +U3IE_00: + Name: "Boktai - The Sun is in Your Hand (USA)" + SongTableOffsets: 0x1EC904 + SongTableSizes: 906 + SampleRate: 6 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +U3IP_00: + Name: "Boktai - The Sun is in Your Hand (Europe)" + SongTableOffsets: 0x1E7604 + Copy: "U3IE_00" +U33J_00: + Name: "Boktai 3 - Sabata's Counterattack (Japan)" + SongTableOffsets: 0x26456C + SongTableSizes: 1483 + SampleRate: 6 + ReverbType: "Normal" + Reverb: 0 + Volume: 15 + HasGoldenSunSynths: False + HasPokemonCompression: False +V49E_00: + Name: "Drill Dozer (USA)" + SongTableOffsets: 0x5079B8 + SongTableSizes: 387 + SampleRate: 3 + ReverbType: "Normal" + Reverb: 0 + Volume: 10 + HasGoldenSunSynths: False + HasPokemonCompression: False \ No newline at end of file diff --git a/VG Music Studio.backup/MPlayDef.s b/VG Music Studio.backup/MPlayDef.s new file mode 100644 index 00000000..a014a0a4 --- /dev/null +++ b/VG Music Studio.backup/MPlayDef.s @@ -0,0 +1,465 @@ +@*** +@ +@ MusicPlayDef.s (MPlayDef.s) ver1.05 +@ +@ Copyright (C) 1999-2001 NINTENDO Co.,Ltd. +@**************************************************************@ + +@*** +@ MML (without running status) +@******************************************************@ + + .equ W00, 0x80 @ WAIT + .equ W01, W00+1 @ + .equ W02, W00+2 @ + .equ W03, W00+3 @ + .equ W04, W00+4 @ + .equ W05, W00+5 @ + .equ W06, W00+6 @ + .equ W07, W00+7 @ + .equ W08, W00+8 @ + .equ W09, W00+9 @ + .equ W10, W00+10 @ + .equ W11, W00+11 @ + .equ W12, W00+12 @ + .equ W13, W00+13 @ + .equ W14, W00+14 @ + .equ W15, W00+15 @ + .equ W16, W00+16 @ + .equ W17, W00+17 @ + .equ W18, W00+18 @ + .equ W19, W00+19 @ + .equ W20, W00+20 @ + .equ W21, W00+21 @ + .equ W22, W00+22 @ + .equ W23, W00+23 @ + .equ W24, W00+24 @ + .equ W28, W00+25 @ + .equ W30, W00+26 @ + .equ W32, W00+27 @ + .equ W36, W00+28 @ + .equ W40, W00+29 @ + .equ W42, W00+30 @ + .equ W44, W00+31 @ + .equ W48, W00+32 @ + .equ W52, W00+33 @ + .equ W54, W00+34 @ + .equ W56, W00+35 @ + .equ W60, W00+36 @ + .equ W64, W00+37 @ + .equ W66, W00+38 @ + .equ W68, W00+39 @ + .equ W72, W00+40 @ + .equ W76, W00+41 @ + .equ W78, W00+42 @ + .equ W80, W00+43 @ + .equ W84, W00+44 @ + .equ W88, W00+45 @ + .equ W90, W00+46 @ + .equ W92, W00+47 @ + .equ W96, W00+48 @ + + .equ FINE, 0xb1 @ fine + .equ GOTO, 0xb2 @ goto + .equ PATT, 0xb3 @ pattern play + .equ PEND, 0xb4 @ pattern end + .equ REPT, 0xb5 @ repeat + .equ MEMACC, 0xb9 @ memacc op adr dat ***lib + .equ PRIO, 0xba @ priority + .equ TEMPO, 0xbb @ tempo (BPM/2) + .equ KEYSH, 0xbc @ key shift + +@*** +@ MML (within running status) +@******************************************************@ + + .equ VOICE, 0xbd @ voice # + .equ VOL, 0xbe @ volume + .equ PAN, 0xbf @ panpot (c_v+??) + .equ BEND, 0xc0 @ pitch bend (c_v+??) + .equ BENDR, 0xc1 @ bend range + .equ LFOS, 0xc2 @ LFO speed + .equ LFODL, 0xc3 @ LFO delay + .equ MOD, 0xc4 @ modulation depth + .equ MODT, 0xc5 @ modulation type + .equ TUNE, 0xc8 @ micro tuning (c_v+??) + + .equ XCMD, 0xcd @ extend command ***lib + .equ xIECV, 0x08 @ imi.echo vol ***lib + .equ xIECL, 0x09 @ imi.echo len ***lib + + .equ EOT, 0xce @ End of Tie + .equ TIE, 0xcf @ + .equ N01, TIE+1 @ NOTE + .equ N02, N01+1 @ + .equ N03, N01+2 @ + .equ N04, N01+3 @ + .equ N05, N01+4 @ + .equ N06, N01+5 @ + .equ N07, N01+6 @ + .equ N08, N01+7 @ + .equ N09, N01+8 @ + .equ N10, N01+9 @ + .equ N11, N01+10 @ + .equ N12, N01+11 @ + .equ N13, N01+12 @ + .equ N14, N01+13 @ + .equ N15, N01+14 @ + .equ N16, N01+15 @ + .equ N17, N01+16 @ + .equ N18, N01+17 @ + .equ N19, N01+18 @ + .equ N20, N01+19 @ + .equ N21, N01+20 @ + .equ N22, N01+21 @ + .equ N23, N01+22 @ + .equ N24, N01+23 @ + .equ N28, N01+24 @ + .equ N30, N01+25 @ + .equ N32, N01+26 @ + .equ N36, N01+27 @ + .equ N40, N01+28 @ + .equ N42, N01+29 @ + .equ N44, N01+30 @ + .equ N48, N01+31 @ + .equ N52, N01+32 @ + .equ N54, N01+33 @ + .equ N56, N01+34 @ + .equ N60, N01+35 @ + .equ N64, N01+36 @ + .equ N66, N01+37 @ + .equ N68, N01+38 @ + .equ N72, N01+39 @ + .equ N76, N01+40 @ + .equ N78, N01+41 @ + .equ N80, N01+42 @ + .equ N84, N01+43 @ + .equ N88, N01+44 @ + .equ N90, N01+45 @ + .equ N92, N01+46 @ + .equ N96, N01+47 @ + +@*** +@ Max value of operators +@******************************************************@ + + .equ mxv, 0x7F @ + +@*** +@ center value of PAN, BEND, TUNE +@******************************************************@ + + .equ c_v, 0x40 @ -64 ~ +63 + +@*** +@ parameter of N??, TIE, EOT +@******************************************************@ + + .equ CnM2, 0 @ + .equ CsM2, 1 @ + .equ DnM2, 2 @ + .equ DsM2, 3 @ + .equ EnM2, 4 @ + .equ FnM2, 5 @ + .equ FsM2, 6 @ + .equ GnM2, 7 @ + .equ GsM2, 8 @ + .equ AnM2, 9 @ + .equ AsM2, 10 @ + .equ BnM2, 11 @ + .equ CnM1, 12 @ + .equ CsM1, 13 @ + .equ DnM1, 14 @ + .equ DsM1, 15 @ + .equ EnM1, 16 @ + .equ FnM1, 17 @ + .equ FsM1, 18 @ + .equ GnM1, 19 @ + .equ GsM1, 20 @ + .equ AnM1, 21 @ + .equ AsM1, 22 @ + .equ BnM1, 23 @ + .equ Cn0, 24 @ + .equ Cs0, 25 @ + .equ Dn0, 26 @ + .equ Ds0, 27 @ + .equ En0, 28 @ + .equ Fn0, 29 @ + .equ Fs0, 30 @ + .equ Gn0, 31 @ + .equ Gs0, 32 @ + .equ An0, 33 @ + .equ As0, 34 @ + .equ Bn0, 35 @ + .equ Cn1, 36 @ + .equ Cs1, 37 @ + .equ Dn1, 38 @ + .equ Ds1, 39 @ + .equ En1, 40 @ + .equ Fn1, 41 @ + .equ Fs1, 42 @ + .equ Gn1, 43 @ + .equ Gs1, 44 @ + .equ An1, 45 @ + .equ As1, 46 @ + .equ Bn1, 47 @ + .equ Cn2, 48 @ + .equ Cs2, 49 @ + .equ Dn2, 50 @ + .equ Ds2, 51 @ + .equ En2, 52 @ + .equ Fn2, 53 @ + .equ Fs2, 54 @ + .equ Gn2, 55 @ + .equ Gs2, 56 @ + .equ An2, 57 @ + .equ As2, 58 @ + .equ Bn2, 59 @ + .equ Cn3, 60 @ + .equ Cs3, 61 @ + .equ Dn3, 62 @ + .equ Ds3, 63 @ + .equ En3, 64 @ + .equ Fn3, 65 @ + .equ Fs3, 66 @ + .equ Gn3, 67 @ + .equ Gs3, 68 @ + .equ An3, 69 @ 440Hz + .equ As3, 70 @ + .equ Bn3, 71 @ + .equ Cn4, 72 @ + .equ Cs4, 73 @ + .equ Dn4, 74 @ + .equ Ds4, 75 @ + .equ En4, 76 @ + .equ Fn4, 77 @ + .equ Fs4, 78 @ + .equ Gn4, 79 @ + .equ Gs4, 80 @ + .equ An4, 81 @ + .equ As4, 82 @ + .equ Bn4, 83 @ + .equ Cn5, 84 @ + .equ Cs5, 85 @ + .equ Dn5, 86 @ + .equ Ds5, 87 @ + .equ En5, 88 @ + .equ Fn5, 89 @ + .equ Fs5, 90 @ + .equ Gn5, 91 @ + .equ Gs5, 92 @ + .equ An5, 93 @ + .equ As5, 94 @ + .equ Bn5, 95 @ + .equ Cn6, 96 @ + .equ Cs6, 97 @ + .equ Dn6, 98 @ + .equ Ds6, 99 @ + .equ En6, 100 @ + .equ Fn6, 101 @ + .equ Fs6, 102 @ + .equ Gn6, 103 @ + .equ Gs6, 104 @ + .equ An6, 105 @ + .equ As6, 106 @ + .equ Bn6, 107 @ + .equ Cn7, 108 @ + .equ Cs7, 109 @ + .equ Dn7, 110 @ + .equ Ds7, 111 @ + .equ En7, 112 @ + .equ Fn7, 113 @ + .equ Fs7, 114 @ + .equ Gn7, 115 @ + .equ Gs7, 116 @ + .equ An7, 117 @ + .equ As7, 118 @ + .equ Bn7, 119 @ + .equ Cn8, 120 @ + .equ Cs8, 121 @ + .equ Dn8, 122 @ + .equ Ds8, 123 @ + .equ En8, 124 @ + .equ Fn8, 125 @ + .equ Fs8, 126 @ + .equ Gn8, 127 @ + +@*** +@ parameter of velocity +@******************************************************@ + + .equ v000, 0 @ + .equ v001, 1 @ + .equ v002, 2 @ + .equ v003, 3 @ + .equ v004, 4 @ + .equ v005, 5 @ + .equ v006, 6 @ + .equ v007, 7 @ + .equ v008, 8 @ + .equ v009, 9 @ + .equ v010, 10 @ + .equ v011, 11 @ + .equ v012, 12 @ + .equ v013, 13 @ + .equ v014, 14 @ + .equ v015, 15 @ + .equ v016, 16 @ + .equ v017, 17 @ + .equ v018, 18 @ + .equ v019, 19 @ + .equ v020, 20 @ + .equ v021, 21 @ + .equ v022, 22 @ + .equ v023, 23 @ + .equ v024, 24 @ + .equ v025, 25 @ + .equ v026, 26 @ + .equ v027, 27 @ + .equ v028, 28 @ + .equ v029, 29 @ + .equ v030, 30 @ + .equ v031, 31 @ + .equ v032, 32 @ + .equ v033, 33 @ + .equ v034, 34 @ + .equ v035, 35 @ + .equ v036, 36 @ + .equ v037, 37 @ + .equ v038, 38 @ + .equ v039, 39 @ + .equ v040, 40 @ + .equ v041, 41 @ + .equ v042, 42 @ + .equ v043, 43 @ + .equ v044, 44 @ + .equ v045, 45 @ + .equ v046, 46 @ + .equ v047, 47 @ + .equ v048, 48 @ + .equ v049, 49 @ + .equ v050, 50 @ + .equ v051, 51 @ + .equ v052, 52 @ + .equ v053, 53 @ + .equ v054, 54 @ + .equ v055, 55 @ + .equ v056, 56 @ + .equ v057, 57 @ + .equ v058, 58 @ + .equ v059, 59 @ + .equ v060, 60 @ + .equ v061, 61 @ + .equ v062, 62 @ + .equ v063, 63 @ + .equ v064, 64 @ + .equ v065, 65 @ + .equ v066, 66 @ + .equ v067, 67 @ + .equ v068, 68 @ + .equ v069, 79 @ + .equ v070, 70 @ + .equ v071, 71 @ + .equ v072, 72 @ + .equ v073, 73 @ + .equ v074, 74 @ + .equ v075, 75 @ + .equ v076, 76 @ + .equ v077, 77 @ + .equ v078, 78 @ + .equ v079, 79 @ + .equ v080, 80 @ + .equ v081, 81 @ + .equ v082, 82 @ + .equ v083, 83 @ + .equ v084, 84 @ + .equ v085, 85 @ + .equ v086, 86 @ + .equ v087, 87 @ + .equ v088, 88 @ + .equ v089, 89 @ + .equ v090, 90 @ + .equ v091, 91 @ + .equ v092, 92 @ + .equ v093, 93 @ + .equ v094, 94 @ + .equ v095, 95 @ + .equ v096, 96 @ + .equ v097, 97 @ + .equ v098, 98 @ + .equ v099, 99 @ + .equ v100, 100 @ + .equ v101, 101 @ + .equ v102, 102 @ + .equ v103, 103 @ + .equ v104, 104 @ + .equ v105, 105 @ + .equ v106, 106 @ + .equ v107, 107 @ + .equ v108, 108 @ + .equ v109, 109 @ + .equ v110, 110 @ + .equ v111, 111 @ + .equ v112, 112 @ + .equ v113, 113 @ + .equ v114, 114 @ + .equ v115, 115 @ + .equ v116, 116 @ + .equ v117, 117 @ + .equ v118, 118 @ + .equ v119, 119 @ + .equ v120, 120 @ + .equ v121, 121 @ + .equ v122, 122 @ + .equ v123, 123 @ + .equ v124, 124 @ + .equ v125, 125 @ + .equ v126, 126 @ + .equ v127, 127 @ + +@*** +@ parameter of gate+ +@******************************************************@ + + .equ gtp1, 1 @ + .equ gtp2, 2 @ + .equ gtp3, 3 @ + +@*** +@ parameter of MODT, BRET +@******************************************************@ + + .equ mod_vib,0 @ vibrate + .equ mod_tre,1 @ tremolo + .equ mod_pan,2 @ auto-panpot + +@*** +@ parameter of MEMACC +@******************************************************@ + + .equ mem_set,0 @ + .equ mem_add,1 @ + .equ mem_sub,2 @ + .equ mem_mem_set,3 @ + .equ mem_mem_add,4 @ + .equ mem_mem_sub,5 @ + .equ mem_beq,6 @ + .equ mem_bne,7 @ + .equ mem_bhi,8 @ + .equ mem_bhs,9 @ + .equ mem_bls,10 @ + .equ mem_blo,11 @ + .equ mem_mem_beq,12 @ + .equ mem_mem_bne,13 @ + .equ mem_mem_bhi,14 @ + .equ mem_mem_bhs,15 @ + .equ mem_mem_bls,16 @ + .equ mem_mem_blo,17 @ + +@*** +@ etc. +@******************************************************@ + + .equ reverb_set,0x80 @ SOUND_MODE_REVERB_SET + .equ PAM, PAN @ + diff --git a/VG Music Studio.backup/Program.cs b/VG Music Studio.backup/Program.cs new file mode 100644 index 00000000..e2e25293 --- /dev/null +++ b/VG Music Studio.backup/Program.cs @@ -0,0 +1,30 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.UI; +using System; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio +{ + internal static class Program + { + [STAThread] + private static void Main() + { +#if DEBUG + //Debug.GBAGameCodeScan(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games"); +#endif + try + { + GlobalConfig.Init(); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorGlobalConfig); + return; + } + Application.EnableVisualStyles(); + Application.Run(MainForm.Instance); + } + } +} diff --git a/VG Music Studio/Properties/AssemblyInfo.cs b/VG Music Studio.backup/Properties/AssemblyInfo.cs similarity index 100% rename from VG Music Studio/Properties/AssemblyInfo.cs rename to VG Music Studio.backup/Properties/AssemblyInfo.cs diff --git a/VG Music Studio.backup/Properties/Icon.ico b/VG Music Studio.backup/Properties/Icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f9a64706d4c81795fbb20a1228f606492531f002 GIT binary patch literal 3238 zcmcImO=ufO6nR^y~dE4H?XLbYB@8$%9qQ&7ppSu+?K3?&C&)+E%#h+Z8BbSOld z_F&%vJrsJ24x+Gvf)2iTFLH2hhTd|GN-jOhmqrKI)9=lUeypgyW#H+}d-MKw=FK-E z67&(3%Q?-zpCwu+BF{7OUo%9%LsuwRSrGklg(#OZ@=eH3uM*vaPZ{(bh~xd=SXo}C z+h0{^<<VzAJp%kky2_}C?_{fOHO!yie5Nj z(sZ2nR*FZ(jnF|kk#V~kd^)ez$=sXOr|MjR(c#l~2U%AHM?HpA9o zax`*C)ym{!P&(ck*H>zlJnMZgY>nwtwOVCY>-}_`w1){6YPG%E&|zy_KM3TfTSH{&4Wc zqHwV9N_p&@tofW+SHwQf!7~Yc<3##?F4TIl4ZZ8YT%I~7QbK>2z*bc8!&)gfPVut_ zdp>7Tg@e)SMVX7)e2TuM_d!$-uJyt!IH&-6tu~&*sa69gr}Z4%+OseBxp3v84h}sB91d3o z)_hXG#jR|IErYDU z=iOTX=X7jj0hrx@?1fDoKop+I4S?=qnz0YC7r1ic_(U$w;k`qf?8W`G?a z!&TJBvW2}h{+|3gwCm4Dog}c5zF480fOkVLx*7C*}FM!IR58UTt|7M(Gd$4)pXzQ?Tmu|nHx%g8w3ch@A#`or8Jzv;+%pH55 z^@ZA(%Ri%A*}bs;xO2_tMnmoU?(q%ZOBy_P6!xo?8$H7yvmP4~)j)zgmoasAA@f*L{S>Kw zhSYfpR3~+zG@fxz5A;FL8MhHBh_1JqJV}*FL(jsBPkd+#kC**9oMhoi=cR{}5bp7b z*C1Tr4V;G9=D$Y(fdI!41j5BeIu5QfICdX@L^QYiF6^5=yjqCg#kO@;@otr1$%Hq8 dDf$G2MvuP6x~JbGv%>=WhIdQhO;c}({sqamkn{im literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Icon16.png b/VG Music Studio.backup/Properties/Icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..acacf012d3debe19507e5ec994bdd1a563f26455 GIT binary patch literal 359 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt3( zJ+r@loG*ZCZg{#lhFJLbPITmJG7xax$=4~pxHEl%@`MA&v@4kOlONgcDm!j_N1|+M z_@A>=wz*9@?sIz2eTLre|MYK_?VnO(e_Nx`hHtTct4g<2$K9RW7j7@espy`n%cbch z#yX|xhBlWNoBwg=7b~waaMtKtXjnD(BvZzk9j=T{ik=OkJKoHCZq^dF-!!k`$@_wc z&-c!LT08&pl-7Xj&;EUQ|5}$#p+Hglf$4eA1`fs(3I{ExpO|y+`}F&ahx2NAwY}EO y<$Ku1Gf4%+*qC_0E2Ps@a#CxX?zhUme7mPEsqN0y%WeVv$l&Sf=d#Wzp$P!<-HfXM literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Icon24.png b/VG Music Studio.backup/Properties/Icon24.png new file mode 100644 index 0000000000000000000000000000000000000000..449d8e39fc42179ba431ecef81ff8f1f3f25ec22 GIT binary patch literal 476 zcmV<20VDp2P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y9E;jv63FrU-0ZmCnK~zXfrINv|!Y~j8(+<+VAdr3yPy!NM zXn_i728lj$=0+b}5I4gZUbJ!6jsunko3(dd61fQ>e9de&e@*_LaEmn{-)(ncEW|C= zpa6yBf(C^ekgZqi2mxAqLA}OWY=DOi88nYECKn2EB_(V?DeUKf|K%b|m{Yl6-<1HH z7S(LCYPk^hvSM99dl*>j>C>Jxh}u9dv6b5f&9+Y{M7_NLE4NY)&Xz)OXur4N$vm1@ z+wJjiL)?IzTqCU$aJgQ?X0y+RXx$ZrBpRQ?U7c@C0CL_U_a-9#!~>^uxF3#P2!pi( z5DR@G;7y7Fk}Cj^Fc1O5%8`~nk)`+au>E^qHE7#5qW@{Y*yn*5VC8xPkEu@&@ke52 zB%avHYo8vv1#+NSLQjlTH)v7-(4d=PuVE@=u93r_pk`3O6k&>66u*eygyspZ1Dv|o S#%M$U0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y9E;jv63FrU-0mVr~K~z{r#gsu#!!Q&?d!0&b5J;SX9-&gZ z;R>99%T(eXS+nas*r4u0$Lb`LpK)TFN?3Z>d?$aN-NL5t`@6E;K3*rk=di_A5Pm!y z`x*&bY~^1C5W0KaC4m|VTWo;>QDRNR7F$pON^rd-GApzY!*e)^$uF&x3V(;77J<@qT!#;jqEA^|+)(0$hwbE(8Elnns?o)x}G*o>#w9%~YK z#)jvDT?1XaULYrez1a&T!44-tauO4P7#<&9g4bUv!TvJ=ZCXQWbp1YmUoNW>JRqMN zHe6$c!|hSv>wI36SQWs`wZd_GX|l)brvTXR--ZAMH4O*DMk<~@6Ig2q#_-H_Xt)X> zn75F-iC7zpw{YG+YHh*L4XE z2P(j;ZxV>%hllaM*b)%X(?g~-93%l3j(JTWhI3sdfFBI34UeCF?did=U@&Gh2f1K9 zIm5Yb761qbY{oD}PYn=`P}ct>0MI~B1Y)Sx8Hl+y9M~cy__@YV?zIF)Y!UE8D#K6` i-)X@XBliv+={LWVau*j&XqjOE0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y9E;jv63FrU-0@q1IK~!i%)s;(b)IbzPvz@=Ulgwz8Z$%O}M^(|9s26EyQAP zkiWmXA6kTn#oqka0g%4=d@}}U5h50QQUe?f5sN)Z0gi--#a>8WcmT$GLd0S(Y5)(R zF>|&AiN#)|0N??{+90vmgE{$2TnZA4J=hyS%!N2B^x&wxNFgSJUbG4THN@ngUZem= zLV1X@!&;L>KYjc#N^q~eHN1HM3A6|i4_$(Vl{!bL?+5GY`IH2)~CMqceHLx5ayq4Bxr6CuSm#i_4P?;-I^fGo84ru zxWhhmT+fSi=}UYmK;>nh zlarvHm){xy=6e5{L0-^zdEi1ya3yh(p1xio z`xGZ~Yq%)yAzX;-XaHc}rU8;X90}m^HNX*hm;^}Tq6UD6hldlj=cK3aftmzJ(&Fyj z-#^qOcZsE^PyL8Ii~*9gK%RSx04@(l^y>$x0H6@uB>*m%&^(a5gsHt+06<;=gbOBoE+paB z@(_!l1C!hwF3q|s%)O;N#G+6L2O-HhwI=b_!j)KbJcOiN6kJ{bdy9)$tmsJsa!4j& wr|!{{EjABN%T4^6cbP8a*t5dZ)H07*qoM6N<$g3Q5&p8x;= literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Icon528.png b/VG Music Studio.backup/Properties/Icon528.png new file mode 100644 index 0000000000000000000000000000000000000000..75601de2ef2e68898be2f817784b750d79ffbeb2 GIT binary patch literal 10133 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 PydL+1rG@ihln?(8zw=kQ literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Next.png b/VG Music Studio.backup/Properties/Next.png new file mode 100644 index 0000000000000000000000000000000000000000..dc839b50f18c9d65802cc276b8eaa1c7eeabce56 GIT binary patch literal 4026 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%0D$qRH; zyXtyZUEit5-_fJ&^J>Zk+;?M@Ps28yCFwT*4$p8uFn$-gRj>RKK}M5_qTQppzwt`b z9N0fuD%6E11`d4!NrL7hyD!T#fu^dJ^oKGfnFLdH(4~cTpQv7b+197fUcTZq*S13w z{uG@;u^0{`sMJFl!}NjZ&}TWsoe6RF0kWn|CFsC^`M}UPlCjMLT|@Av73bi9Piv5! Mwd1L(lYUqK2WetO7XSbN literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Pause.ico b/VG Music Studio.backup/Properties/Pause.ico new file mode 100644 index 0000000000000000000000000000000000000000..30b143620803667ca703b20759d56c50743d8b20 GIT binary patch literal 118062 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 literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Pause.png b/VG Music Studio.backup/Properties/Pause.png new file mode 100644 index 0000000000000000000000000000000000000000..9a736deda0138a1282a6d9c36f6f0b9190b9839c GIT binary patch literal 1450 zcmeAS@N?(olHy`uVBq!ia0vp^b3mAb4M=wFIOGeY7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0h7I;J!GcfQS24TkI`72U@f)XXJ5hcO-X(i=}MX3x0iJ5sNdU>fO3MP7n zdWOkbbG#WCSfxE(978JN-d=KCC!8+ens{PDYRg`OQ(ZHZZ!n*laW%EgMIk8em;3dg zov&YL=GmXJwNS0yq!E&PXHK}kzyB}$e?MMVmMmtQ&Lk4)YMxb~=`b;LsBu@ya`jwT zyY4}dwTSq_=-7ix%QW79Qg*j(7?&a-#1@!_X4 zQ}5;4<=H*?95>(Wy?MS_cHHyN&jVso%%`WXNne+~{{4dY`>sAUUtiwv{>7Ps%CwpK zv%j`a|5mp7+4h=$GF{zoo_*^1>ho(|^;)=!^Rwk==l|Su{CiE^-9P&xKTkTi7HHD- z_q&cGn`3YOn*aZk&vO5teSUv-Jy)LX?z-!?diyK)GS|4Lx1BaVEivcd*%P!P?!NdW z?^^%8r{|z7@4HX$m+qHtI4#ItUvcaK&pS8lbe`L>2Yqbc6L#2a?l~xV2dH(35WRZ; z1HY7?KOu4F;sd?^aldvm?vN?`(4Nk7?m=D&b~-JE=UqktKh2}z?1`Fte{R&Aer~(( z`RCWq!dbV^i{1ZPZu6oEZlKI zx$WM?nGb`k1;iKO%OP8x3k7#w6^-EvND#fd;^0yS*RYGdQb2CX5aX83i~GyIsW5h$ Uqzzvruqb8lboFyt=akR{092E?-2eap literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Play.ico b/VG Music Studio.backup/Properties/Play.ico new file mode 100644 index 0000000000000000000000000000000000000000..e8be3d8757c7a4f9b418a3650f1cd6c69278d3db GIT binary patch literal 140062 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 F;r}DLY(@Y8 literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Play.png b/VG Music Studio.backup/Properties/Play.png new file mode 100644 index 0000000000000000000000000000000000000000..f386fd325480bec804b628fe9fb3016a349159db GIT binary patch literal 2298 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 literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Previous.ico b/VG Music Studio.backup/Properties/Previous.ico new file mode 100644 index 0000000000000000000000000000000000000000..fe2ef73e77e4f7b2ae6c6c6f1701a2b5ffdaa4bc GIT binary patch literal 140062 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 literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Previous.png b/VG Music Studio.backup/Properties/Previous.png new file mode 100644 index 0000000000000000000000000000000000000000..63496267cc5cabedf50440f1a3b095a96bef2eb6 GIT binary patch literal 3708 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

-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 y2S{H_$3dd9qdC6QL6S$6aNP{V$@my literal 0 HcmV?d00001 diff --git a/VG Music Studio.backup/Properties/Resources.Designer.cs b/VG Music Studio.backup/Properties/Resources.Designer.cs new file mode 100644 index 00000000..1aa1551a --- /dev/null +++ b/VG Music Studio.backup/Properties/Resources.Designer.cs @@ -0,0 +1,133 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Kermalis.VGMusicStudio.Properties { + using System; + + + ///

+ /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Icon { + get { + object obj = ResourceManager.GetObject("Icon", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconNext { + get { + object obj = ResourceManager.GetObject("IconNext", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconPause { + get { + object obj = ResourceManager.GetObject("IconPause", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconPlay { + get { + object obj = ResourceManager.GetObject("IconPlay", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap IconPlaylist { + get { + object obj = ResourceManager.GetObject("IconPlaylist", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconPrevious { + get { + object obj = ResourceManager.GetObject("IconPrevious", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap IconSong { + get { + object obj = ResourceManager.GetObject("IconSong", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/VG Music Studio.backup/Properties/Resources.resx b/VG Music Studio.backup/Properties/Resources.resx new file mode 100644 index 00000000..ad871cdc --- /dev/null +++ b/VG Music Studio.backup/Properties/Resources.resx @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Properties\Icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Next.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Pause.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Play.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Properties\Playlist.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Previous.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Properties\Song.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/VG Music Studio.backup/Properties/Settings.Designer.cs b/VG Music Studio.backup/Properties/Settings.Designer.cs new file mode 100644 index 00000000..875ce0f2 --- /dev/null +++ b/VG Music Studio.backup/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Kermalis.VGMusicStudio.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/VG Music Studio.backup/Properties/Settings.settings b/VG Music Studio.backup/Properties/Settings.settings new file mode 100644 index 00000000..39645652 --- /dev/null +++ b/VG Music Studio.backup/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/VG Music Studio.backup/Properties/Song.png b/VG Music Studio.backup/Properties/Song.png new file mode 100644 index 0000000000000000000000000000000000000000..62a132e5d49752cc1cccbe330aeb2d0148cbbbe5 GIT binary patch literal 2328 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fo{p?&#~tz_78O`%fY(0|PTd zfKP}kP<#}OhQNpofz1YAIzSnvB*-uL|HuXm!Qa_cfeC@Lz$3Dlfr0N32s4Umcr^e8 zB}-f*N`mv#O3D+9QW**oGxJLH@={9_O!N%&j0`_2WKIPZFtwg8jv*CsZzo2UNgML8 z)c*Ijo)pUWc+W*0mSvt&vG%eDopq-6pMMm=5Gls3a5{-GB~qtBr<-*`>PCi5I${pf zjxwFtM3!V<(vSKBCs^(FJ^U`f#`pY}dWf4rp4s;O3(Oi47w`Ndy{alr&>)XLh&SDQ z#p#U<*y)*W2kJlE{Ch_00r$SWQ&+OuJp6roM%)4Bd3O$TG3AABXlsxbU8D5CjP*6g zj?BxgEO!S!pGKNV$N@@~(PhBHgA zi%i(Dm32av#S-aTM?elJ|KdE~;565Xs5?v^rZIvIZh_ZCCv4dXQliANHYtK(Wt(); z1??7)fFj3PC6EAj(*>PFx0r;sZ3h~_-m);(el<78`+T5{*Yb13&VRW2PCftBY#-Hx z)pZ>F(pUVgX80ap=34$xGN8sObv`F+OxS_d`Wkx|Y&$)LX&3K@-_omgpV3@!nPLAw zr^@M`tTCn^YR=pLYmD0s41!&ayct_hZ($PRxsWlxlC_2R!n1}>RuQl2$xr&hXKx+F2TK_3_8pUAIw$eq^lBZY2LX)nvFVbGyV-s$ zX)k5j6MkU+?+DKW+nD}cIUU6$_u(gZmf$qu3B90j+3gtHM?Lyy!43W&mzlYEUlqNR zJQFy3&(BAp*NP7(=-ohPwmj#pOOgVT*ped}#h^^R(r|P7q#2Hq{47KA- zC+aK|xb8k_Vc%RYaqA>72Z&$mceb|_bNF`XSK$kFb&DBqg%>Q8;a0Ft{LPY~0o34k z!;Vqv6i}t`HvfjUBAtdeEhtLT#TdU_PG(Hee9j7m!dhW5<%;T#*D5wrKJTRFl(EI}sMiX +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Kermalis.VGMusicStudio.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to {0} key. + /// + internal static string ConfigKeySubkey { + get { + return ResourceManager.GetString("ConfigKeySubkey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Would you like to stop playing the current playlist?. + /// + internal static string EndPlaylistBody { + get { + return ResourceManager.GetString("EndPlaylistBody", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid command in track {0} at 0x{1:X}: 0x{2:X}. + /// + internal static string ErrorAlphaDreamDSEMP2KSDATInvalidCommand { + get { + return ResourceManager.GetString("ErrorAlphaDreamDSEMP2KSDATInvalidCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot copy invalid game code "{0}". + /// + internal static string ErrorAlphaDreamMP2KCopyInvalidGameCode { + get { + return ResourceManager.GetString("ErrorAlphaDreamMP2KCopyInvalidGameCode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game code "{0}" is missing.. + /// + internal static string ErrorAlphaDreamMP2KMissingGameCode { + get { + return ResourceManager.GetString("ErrorAlphaDreamMP2KMissingGameCode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error parsing game code "{0}" in "{1}"{2}. + /// + internal static string ErrorAlphaDreamMP2KParseGameCode { + get { + return ResourceManager.GetString("ErrorAlphaDreamMP2KParseGameCode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Playlist "{0}" has song {1} defined more than once between decimal and hexadecimal.. + /// + internal static string ErrorAlphaDreamMP2KSongRepeated { + get { + return ResourceManager.GetString("ErrorAlphaDreamMP2KSongRepeated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" count must be the same as "{1}" count.. + /// + internal static string ErrorAlphaDreamMP2KSongTableCounts { + get { + return ResourceManager.GetString("ErrorAlphaDreamMP2KSongTableCounts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" must be True or False.. + /// + internal static string ErrorBoolParse { + get { + return ResourceManager.GetString("ErrorBoolParse", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Color {0} has an invalid key.. + /// + internal static string ErrorConfigColorInvalidKey { + get { + return ResourceManager.GetString("ErrorConfigColorInvalidKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Color {0} is not defined.. + /// + internal static string ErrorConfigColorMissing { + get { + return ResourceManager.GetString("ErrorConfigColorMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Color {0} is defined more than once between decimal and hexadecimal.. + /// + internal static string ErrorConfigColorRepeated { + get { + return ResourceManager.GetString("ErrorConfigColorRepeated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" is invalid.. + /// + internal static string ErrorConfigKeyInvalid { + get { + return ResourceManager.GetString("ErrorConfigKeyInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" is missing.. + /// + internal static string ErrorConfigKeyMissing { + get { + return ResourceManager.GetString("ErrorConfigKeyMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" must have at least one entry.. + /// + internal static string ErrorConfigKeyNoEntries { + get { + return ResourceManager.GetString("ErrorConfigKeyNoEntries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown header version: 0x{0:X}. + /// + internal static string ErrorDSEInvalidHeaderVersion { + get { + return ResourceManager.GetString("ErrorDSEInvalidHeaderVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid key in track {0} at 0x{1:X}: {2}. + /// + internal static string ErrorDSEInvalidKey { + get { + return ResourceManager.GetString("ErrorDSEInvalidKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There are no "bgm(NNNN).smd" files.. + /// + internal static string ErrorDSENoSequences { + get { + return ResourceManager.GetString("ErrorDSENoSequences", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading Global Config. + /// + internal static string ErrorGlobalConfig { + get { + return ResourceManager.GetString("ErrorGlobalConfig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading Song {0}. + /// + internal static string ErrorLoadSong { + get { + return ResourceManager.GetString("ErrorLoadSong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid running status command in track {0} at 0x{1:X}: 0x{2:X}. + /// + internal static string ErrorMP2KInvalidRunningStatusCommand { + get { + return ResourceManager.GetString("ErrorMP2KInvalidRunningStatusCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Too many nested call events in track {0}. + /// + internal static string ErrorMP2KSDATNestedCalls { + get { + return ResourceManager.GetString("ErrorMP2KSDATNestedCalls", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading GBA ROM (AlphaDream). + /// + internal static string ErrorOpenAlphaDream { + get { + return ResourceManager.GetString("ErrorOpenAlphaDream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading DSE Folder. + /// + internal static string ErrorOpenDSE { + get { + return ResourceManager.GetString("ErrorOpenDSE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading GBA ROM (MP2K). + /// + internal static string ErrorOpenMP2K { + get { + return ResourceManager.GetString("ErrorOpenMP2K", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading SDAT File. + /// + internal static string ErrorOpenSDAT { + get { + return ResourceManager.GetString("ErrorOpenSDAT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error parsing "{0}"{1}. + /// + internal static string ErrorParseConfig { + get { + return ResourceManager.GetString("ErrorParseConfig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Exporting DLS. + /// + internal static string ErrorSaveDLS { + get { + return ResourceManager.GetString("ErrorSaveDLS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Exporting MIDI. + /// + internal static string ErrorSaveMIDI { + get { + return ResourceManager.GetString("ErrorSaveMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Exporting SF2. + /// + internal static string ErrorSaveSF2 { + get { + return ResourceManager.GetString("ErrorSaveSF2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Exporting WAV. + /// + internal static string ErrorSaveWAV { + get { + return ResourceManager.GetString("ErrorSaveWAV", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This SDAT archive has no sequences.. + /// + internal static string ErrorSDATNoSequences { + get { + return ResourceManager.GetString("ErrorSDATNoSequences", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" is not an integer value.. + /// + internal static string ErrorValueParse { + get { + return ResourceManager.GetString("ErrorValueParse", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" must be between {1} and {2}.. + /// + internal static string ErrorValueParseRanged { + get { + return ResourceManager.GetString("ErrorValueParseRanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GBA Files. + /// + internal static string FilterOpenGBA { + get { + return ResourceManager.GetString("FilterOpenGBA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SDAT Files. + /// + internal static string FilterOpenSDAT { + get { + return ResourceManager.GetString("FilterOpenSDAT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DLS Files. + /// + internal static string FilterSaveDLS { + get { + return ResourceManager.GetString("FilterSaveDLS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MIDI Files. + /// + internal static string FilterSaveMIDI { + get { + return ResourceManager.GetString("FilterSaveMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SF2 Files. + /// + internal static string FilterSaveSF2 { + get { + return ResourceManager.GetString("FilterSaveSF2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WAV Files. + /// + internal static string FilterSaveWAV { + get { + return ResourceManager.GetString("FilterSaveWAV", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data. + /// + internal static string MenuData { + get { + return ResourceManager.GetString("MenuData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to End Current Playlist. + /// + internal static string MenuEndPlaylist { + get { + return ResourceManager.GetString("MenuEndPlaylist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File. + /// + internal static string MenuFile { + get { + return ResourceManager.GetString("MenuFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open GBA ROM (AlphaDream). + /// + internal static string MenuOpenAlphaDream { + get { + return ResourceManager.GetString("MenuOpenAlphaDream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open DSE Folder. + /// + internal static string MenuOpenDSE { + get { + return ResourceManager.GetString("MenuOpenDSE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open GBA ROM (MP2K). + /// + internal static string MenuOpenMP2K { + get { + return ResourceManager.GetString("MenuOpenMP2K", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open SDAT File. + /// + internal static string MenuOpenSDAT { + get { + return ResourceManager.GetString("MenuOpenSDAT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Playlist. + /// + internal static string MenuPlaylist { + get { + return ResourceManager.GetString("MenuPlaylist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export VoiceTable as DLS. + /// + internal static string MenuSaveDLS { + get { + return ResourceManager.GetString("MenuSaveDLS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export Song as MIDI. + /// + internal static string MenuSaveMIDI { + get { + return ResourceManager.GetString("MenuSaveMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export VoiceTable as SF2. + /// + internal static string MenuSaveSF2 { + get { + return ResourceManager.GetString("MenuSaveSF2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export Song as WAV. + /// + internal static string MenuSaveWAV { + get { + return ResourceManager.GetString("MenuSaveWAV", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to C;C#;D;D#;E;F;F#;G;G#;A;A#;B. + /// + internal static string Notes { + get { + return ResourceManager.GetString("Notes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Next Song. + /// + internal static string PlayerNextSong { + get { + return ResourceManager.GetString("PlayerNextSong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Notes. + /// + internal static string PlayerNotes { + get { + return ResourceManager.GetString("PlayerNotes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pause. + /// + internal static string PlayerPause { + get { + return ResourceManager.GetString("PlayerPause", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Play. + /// + internal static string PlayerPlay { + get { + return ResourceManager.GetString("PlayerPlay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Position. + /// + internal static string PlayerPosition { + get { + return ResourceManager.GetString("PlayerPosition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Previous Song. + /// + internal static string PlayerPreviousSong { + get { + return ResourceManager.GetString("PlayerPreviousSong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rest. + /// + internal static string PlayerRest { + get { + return ResourceManager.GetString("PlayerRest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop. + /// + internal static string PlayerStop { + get { + return ResourceManager.GetString("PlayerStop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tempo. + /// + internal static string PlayerTempo { + get { + return ResourceManager.GetString("PlayerTempo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type. + /// + internal static string PlayerType { + get { + return ResourceManager.GetString("PlayerType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unpause. + /// + internal static string PlayerUnpause { + get { + return ResourceManager.GetString("PlayerUnpause", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Music. + /// + internal static string PlaylistMusic { + get { + return ResourceManager.GetString("PlaylistMusic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Would you like to play the following playlist?{0}. + /// + internal static string PlayPlaylistBody { + get { + return ResourceManager.GetString("PlayPlaylistBody", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to VoiceTable saved to {0}.. + /// + internal static string SuccessSaveDLS { + get { + return ResourceManager.GetString("SuccessSaveDLS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MIDI saved to {0}.. + /// + internal static string SuccessSaveMIDI { + get { + return ResourceManager.GetString("SuccessSaveMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to VoiceTable saved to {0}.. + /// + internal static string SuccessSaveSF2 { + get { + return ResourceManager.GetString("SuccessSaveSF2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WAV saved to {0}.. + /// + internal static string SuccessSaveWAV { + get { + return ResourceManager.GetString("SuccessSaveWAV", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Arguments. + /// + internal static string TrackViewerArguments { + get { + return ResourceManager.GetString("TrackViewerArguments", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event. + /// + internal static string TrackViewerEvent { + get { + return ResourceManager.GetString("TrackViewerEvent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Offset. + /// + internal static string TrackViewerOffset { + get { + return ResourceManager.GetString("TrackViewerOffset", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ticks. + /// + internal static string TrackViewerTicks { + get { + return ResourceManager.GetString("TrackViewerTicks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Track Viewer. + /// + internal static string TrackViewerTitle { + get { + return ResourceManager.GetString("TrackViewerTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Track {0}. + /// + internal static string TrackViewerTrackX { + get { + return ResourceManager.GetString("TrackViewerTrackX", resourceCulture); + } + } + } +} diff --git a/VG Music Studio.backup/Properties/Strings.es.resx b/VG Music Studio.backup/Properties/Strings.es.resx new file mode 100644 index 00000000..d98bb266 --- /dev/null +++ b/VG Music Studio.backup/Properties/Strings.es.resx @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Quisiera detener la Lista de Reproducción actual? + + + Error al Cargar Canción {0} + + + Error al Abrir Carpeta DSE + + + Error al Abrir GBA ROM (AlphaDream) + + + Error al Abrir GBA ROM (MP2K) + + + Error al Abrir Archivo SDAT + + + Error al Exportar MIDI + + + Archivo GBA + + + Archivo SDAT + + + Archivos MIDI + + + Datos + + + Detener Lista de Reproducción Actual + + + Archivo + + + Abrir Carpeta DSE + + + Abrir GBA ROM (AlphaDream) + + + Abrir GBA ROM (MP2K) + + + Abrir Archivo SDAT + + + Playlist + + + Exportar Canción como MIDI + + + Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si + + + Siguiente Canción + + + Notas + + + Pausar + + + Reproducir + + + Posición + + + Canción Anterior + + + Retraso + + + Detener + + + Tempo + + + Tipo + + + Resumir + + + Música + + + Quisiera reproducir la siguiente Lista de Reproducción? {0} + + + MIDI guardado en {0}. + + + Argumento + + + Evento + + + Offset + + + Ticks + + + Visor de Eventos + + + Pista {0} + + + {0} clave + + + "{0}" debe ser Verdadero o Falso. + + + El color {0} tiene una clave inválida. + + + El color {0} no está definido. + + + El color {0} está definido más de una vez entre decimal y hexadecimal. + + + "{0}" es inválido. + + + "{0}" no se encuentra. + + + "{0}" debe tener al menos una entrada. + + + Versión del encabezado desconocida: 0x{0:X} + + + Clave inválida en la pista {0} en 0x{1:X}: {2} + + + Comando inválido en la pista {0} en 0x{1:X}: 0x{2:X} + + + No hay ningún archivo "bgm(NNNN).smd". + + + Error al Cargar Configuración Global + + + No se puede copiar, el código del juego "{0}" es inválido. + + + No se encuentra el código del juego "{0}". + + + Error analizando el código del juego "{0}" en "{1}"{2} + + + La Lista de Reproducción "{0}" tiene a la canción {1} definida más de una vez entre decimal y hexadecimal. + + + El número de "{0}" debe ser igual al de "{1}". + + + Comando de estado en ejecución inválido en la pista {0} en 0x{1:X}: 0x{2:X} + + + Demasiadas llamadas a eventos anidadas en la pista {0} + + + Error analizando "{0}"{1} + + + Este archivo SDAT no tiene secuencias. + + + "{0}" no es un valor entero. + + + "{0}" debe estar entre {1} y {2}. + + + Error al Exportar WAV + + + Archivos WAV + + + Exportar Canción como WAV + + + WAV guardado en {0}. + + + Error al Exportar SF2 + + + Archivos SF2 + + + Exportar VoiceTable como SF2 + + + SF2 guardado en {0}. + + + Error al Exportar DLS + + + Archivos DLS + + + Exportar VoiceTable como DLS + + + DLS guardado en {0}. + + \ No newline at end of file diff --git a/VG Music Studio.backup/Properties/Strings.it.resx b/VG Music Studio.backup/Properties/Strings.it.resx new file mode 100644 index 00000000..935f17e8 --- /dev/null +++ b/VG Music Studio.backup/Properties/Strings.it.resx @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Errore durante il caricamento del Brano {0} + + + Errore durante il caricamento della ROM GBA (AlphaDream) + + + Errore Durante L'Esportazione in MIDI + + + File GBA + + + File MIDI + + + Dati + + + File + + + Apri ROM GBA (MP2K) + + + Esporta Brano in MIDI + + + Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si + + + Pausa + + + Note + + + Pausa + + + Play + + + Posizione + + + Stop + + + Tempo + + + Tipo + + + Riprendi + + + MIDI salvato in {0}. + + + Desideri riprodurre la seguente playlist?{0} + + + Brano Sucessivo + + + Brano Precedente + + + Desideri interrompere la riproduzione della playlist attuale? + + + Errore durante il caricamento della Cartella DSE + + + Errore durante il caricamento della ROM GBA (MP2K) + + + Errore durante il caricamento del File SDAT + + + File SDAT + + + Termina la Playlist Attuale + + + Apri Cartella DSE + + + Apri ROM GBA (AlphaDream) + + + Apri File SDAT + + + Playlist + + + Musica + + + Argomenti + + + Evento + + + Posizione + + + Tick + + + Visualizzatore Traccia + + + Traccia {0} + + + La Chiave {0} + + + "{0}" deve essere Vero o Falso. + + + Il colore {0} non ha una chiave valida. + + + Il colore {0} non è definito. + + + Il colore {0} è definito più di una volta tra decimale ed esadecimale. + + + "{0}" non è valido. + + + "{0}" è mancante. + + + "{0}" deve possedere almeno una voce. + + + Versione dell'header sconosciuta: 0x{0:X} + + + Tasto del pianoforte non valido nella traccia {0} a 0x{1:X}: {2} + + + Comando non valido nella traccia {0} a 0x{1:X}: 0x{2:X} + + + Non sono presenti file "bgm(NNNN).smd". + + + Errore nel Caricamento della Configurazione Globale + + + Non è possibile copiare il Codice Gioco invalido "{0}" + + + Il Codice Gioco "{0}" non è presente. + + + Errore nell'analisi del Codice Gioco "{0}" in "{1}"{2} + + + La Playlist "{0}" presenta il brano {1} più volte definito tra decimale e esadecimale. + + + Il numero di "{0}" deve essere uguale a quello di "{1}". + + + Comando di stato in esecuzione non valido nella traccia {0} a 0x{1:X}: 0x{2:X} + + + Troppi eventi di chiamata nidificati nella traccia {0} + + + Errore nell'analisi di "{0}"{1} + + + Questo archivio SDAT non ha sequenze. + + + "{0}" non è un valore intero. + + + "{0}" deve essere compreso tra {1} e {2}. + + + Errore Durante L'Esportazione in WAV + + + File WAV + + + Esporta Brano in WAV + + + WAV salvato in {0}. + + + Errore Durante L'Esportazione in SF2 + + + File SF2 + + + Esporta VoiceTable In SF2 + + + VoiceTable salvata in {0}. + + + Errore Durante L'Esportazione in DLS + + + File DLS + + + Esporta VoiceTable In DLS + + + VoiceTable salvata in {0}. + + \ No newline at end of file diff --git a/VG Music Studio.backup/Properties/Strings.resx b/VG Music Studio.backup/Properties/Strings.resx new file mode 100644 index 00000000..8e4ae4a1 --- /dev/null +++ b/VG Music Studio.backup/Properties/Strings.resx @@ -0,0 +1,376 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Error Loading Song {0} + {0} is the song number. + + + Error Loading GBA ROM (AlphaDream) + + + Error Exporting MIDI + + + GBA Files + + + MIDI Files + + + Data + + + File + + + Open GBA ROM (MP2K) + + + Export Song as MIDI + + + C;C#;D;D#;E;F;F#;G;G#;A;A#;B + + + Rest + + + Notes + + + Pause + + + Play + + + Position + + + Stop + + + Tempo + + + Type + + + Unpause + + + MIDI saved to {0}. + {0} is the file name. + + + Would you like to play the following playlist?{0} + {0} is a newline character followed by the playlist name. + + + Next Song + + + Previous Song + + + Would you like to stop playing the current playlist? + + + Error Loading DSE Folder + + + Error Loading GBA ROM (MP2K) + + + Error Loading SDAT File + + + SDAT Files + + + End Current Playlist + + + Open DSE Folder + + + Open GBA ROM (AlphaDream) + + + Open SDAT File + + + Playlist + + + Music + + + Arguments + + + Event + + + Offset + + + Ticks + + + Track Viewer + + + Track {0} + {0} is the track number. + + + {0} key + {0} is the parent key name. + + + "{0}" must be True or False. + {0} is the value name. + + + Color {0} has an invalid key. + {0} is the color number. + + + Color {0} is not defined. + {0} is the color number. + + + Color {0} is defined more than once between decimal and hexadecimal. + {0} is the color number. + + + "{0}" is invalid. + {0} is the invalid key. + + + "{0}" is missing. + {0} is the missing key. + + + "{0}" must have at least one entry. + {0} is the key. + + + Unknown header version: 0x{0:X} + {0} is the header version. + + + Invalid key in track {0} at 0x{1:X}: {2} + {0} is the track number, {1} is the offset, {2} is the key. + + + Invalid command in track {0} at 0x{1:X}: 0x{2:X} + {0} is the track number, {1} is the offset, {2} is the command. + + + There are no "bgm(NNNN).smd" files. + + + Error Loading Global Config + + + Cannot copy invalid game code "{0}" + {0} is the invalid game code. + + + Game code "{0}" is missing. + {0} is the game code. + + + Error parsing game code "{0}" in "{1}"{2} + {0} is the game code, {1} is the config filename, {2} is a newline character followed by the error message. + + + Playlist "{0}" has song {1} defined more than once between decimal and hexadecimal. + {0} is the playlist name, {1} is the song index. + + + "{0}" count must be the same as "{1}" count. + {0} is key 1, {1} is key 2. + + + Invalid running status command in track {0} at 0x{1:X}: 0x{2:X} + {0} is the track number, {1} is the offset, {2} is the command. + + + Too many nested call events in track {0} + {0} is the track number. + + + Error parsing "{0}"{1} + {0} is the config filename, {1} is a newline character followed by the error message. + + + This SDAT archive has no sequences. + + + "{0}" is not an integer value. + {0} is the value name. + + + "{0}" must be between {1} and {2}. + {0} is the value name, {1} is the minimum allowed value, {2} is the maximum allowed value. + + + Error Exporting WAV + + + WAV Files + + + Export Song as WAV + + + WAV saved to {0}. + {0} is the file name. + + + Error Exporting SF2 + + + SF2 Files + + + Export VoiceTable as SF2 + + + VoiceTable saved to {0}. + {0} is the file name. + + + Error Exporting DLS + + + DLS Files + + + Export VoiceTable as DLS + + + VoiceTable saved to {0}. + {0} is the file name. + + \ No newline at end of file diff --git a/VG Music Studio.backup/UI/ColorSlider.cs b/VG Music Studio.backup/UI/ColorSlider.cs new file mode 100644 index 00000000..b23df9ce --- /dev/null +++ b/VG Music Studio.backup/UI/ColorSlider.cs @@ -0,0 +1,485 @@ +#region License + +/* Copyright (c) 2017 Fabrice Lacharme + * This code is inspired from Michal Brylka + * https://www.codeproject.com/Articles/17395/Owner-drawn-trackbar-slider + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory(""), ToolboxBitmap(typeof(TrackBar))] + internal class ColorSlider : Control + { + private const int thumbSize = 14; + private Rectangle thumbRect; + + private long _value = 0L; + public long Value + { + get => _value; + set + { + if (value >= _minimum && value <= _maximum) + { + _value = value; + ValueChanged?.Invoke(this, new EventArgs()); + Invalidate(); + } + else + { + throw new ArgumentOutOfRangeException(nameof(Value), $"{nameof(Value)} must be between {nameof(Minimum)} and {nameof(Maximum)}."); + } + } + } + private long _minimum = 0L; + public long Minimum + { + get => _minimum; + set + { + if (value <= _maximum) + { + _minimum = value; + if (_value < _minimum) + { + _value = _minimum; + ValueChanged?.Invoke(this, new EventArgs()); + } + Invalidate(); + } + else + { + throw new ArgumentOutOfRangeException(nameof(Minimum), $"{nameof(Minimum)} cannot be higher than {nameof(Maximum)}."); + } + } + } + private long _maximum = 10L; + public long Maximum + { + get => _maximum; + set + { + if (value >= _minimum) + { + _maximum = value; + if (_value > _maximum) + { + _value = _maximum; + ValueChanged?.Invoke(this, new EventArgs()); + } + Invalidate(); + } + else + { + throw new ArgumentOutOfRangeException(nameof(Maximum), $"{nameof(Maximum)} cannot be lower than {nameof(Minimum)}."); + } + } + } + private long _smallChange = 1L; + public long SmallChange + { + get => _smallChange; + set + { + if (value >= 0) + { + _smallChange = value; + } + else + { + throw new ArgumentOutOfRangeException(nameof(SmallChange), $"{nameof(SmallChange)} must be greater than or equal to 0."); + } + } + } + private long _largeChange = 5L; + public long LargeChange + { + get => _largeChange; + set + { + if (value >= 0) + { + _largeChange = value; + } + else + { + throw new ArgumentOutOfRangeException(nameof(LargeChange), $"{nameof(LargeChange)} must be greater than or equal to 0."); + } + } + } + private bool _acceptKeys = true; + public bool AcceptKeys + { + get => _acceptKeys; + set + { + _acceptKeys = value; + SetStyle(ControlStyles.Selectable, value); + } + } + + public event EventHandler ValueChanged; + + private readonly Color _thumbOuterColor = Color.White; + private readonly Color _thumbInnerColor = Color.White; + private readonly Color _thumbPenColor = Color.FromArgb(125, 125, 125); + private readonly Color _barInnerColor = Theme.BackColorMouseOver; + private readonly Color _elapsedPenColorTop = Theme.ForeColor; + private readonly Color _elapsedPenColorBottom = Theme.ForeColor; + private readonly Color _barPenColorTop = Color.FromArgb(85, 90, 104); + private readonly Color _barPenColorBottom = Color.FromArgb(117, 124, 140); + private readonly Color _elapsedInnerColor = Theme.BorderColor; + private readonly Color _tickColor = Color.White; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + pen.Dispose(); + } + base.Dispose(disposing); + } + public ColorSlider() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.Selectable | + ControlStyles.SupportsTransparentBackColor | ControlStyles.UserMouse | + ControlStyles.UserPaint, true); + Size = new Size(200, 48); + } + + protected override void OnPaint(PaintEventArgs e) + { + if (!Enabled) + { + Color[] c = DesaturateColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, + _barInnerColor, + _elapsedPenColorTop, _elapsedPenColorBottom, + _barPenColorTop, _barPenColorBottom, + _elapsedInnerColor); + Draw(e, + c[0], c[1], c[2], + c[3], + c[4], c[5], + c[6], c[7], + c[8]); + } + else + { + if (mouseInRegion) + { + Color[] c = LightenColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, + _barInnerColor, + _elapsedPenColorTop, _elapsedPenColorBottom, + _barPenColorTop, _barPenColorBottom, + _elapsedInnerColor); + Draw(e, + c[0], c[1], c[2], + c[3], + c[4], c[5], + c[6], c[7], + c[8]); + } + else + { + Draw(e, + _thumbOuterColor, _thumbInnerColor, _thumbPenColor, + _barInnerColor, + _elapsedPenColorTop, _elapsedPenColorBottom, + _barPenColorTop, _barPenColorBottom, + _elapsedInnerColor); + } + } + } + private readonly Pen pen = new Pen(Color.Transparent); + private void Draw(PaintEventArgs e, + Color thumbOuterColorPaint, Color thumbInnerColorPaint, Color thumbPenColorPaint, + Color barInnerColorPaint, + Color elapsedTopPenColorPaint, Color elapsedBottomPenColorPaint, + Color barTopPenColorPaint, Color barBottomPenColorPaint, + Color elapsedInnerColorPaint) + { + if (Focused) + { + ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Color.FromArgb(50, elapsedTopPenColorPaint), ButtonBorderStyle.Dashed); + } + + long a = _maximum - _minimum; + long x = a == 0 ? 0 : (_value - _minimum) * (ClientRectangle.Width - thumbSize) / a; + thumbRect = new Rectangle((int)x, ClientRectangle.Y + (ClientRectangle.Height / 2) - (thumbSize / 2), thumbSize, thumbSize); + Rectangle barRect = ClientRectangle; + barRect.Inflate(-1, -barRect.Height / 3); + Rectangle elapsedRect = barRect; + elapsedRect.Width = thumbRect.Left + (thumbSize / 2); + + pen.Color = barInnerColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + (barRect.Height / 2)); + pen.Color = elapsedInnerColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + (barRect.Height / 2)); + pen.Color = elapsedTopPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2)); + pen.Color = elapsedBottomPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y + 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2)); + pen.Color = barTopPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2)); + pen.Color = barBottomPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); + pen.Color = barTopPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X, barRect.Y + (barRect.Height / 2) + 1); + pen.Color = barBottomPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); + + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + Color newthumbOuterColorPaint = thumbOuterColorPaint, + newthumbInnerColorPaint = thumbInnerColorPaint; + if (busyMouse) + { + newthumbOuterColorPaint = Color.FromArgb(175, thumbOuterColorPaint); + newthumbInnerColorPaint = Color.FromArgb(175, thumbInnerColorPaint); + } + using (GraphicsPath thumbPath = CreateRoundRectPath(thumbRect, thumbSize)) + { + using (var lgbThumb = new LinearGradientBrush(thumbRect, newthumbOuterColorPaint, newthumbInnerColorPaint, LinearGradientMode.Vertical) { WrapMode = WrapMode.TileFlipXY }) + { + e.Graphics.FillPath(lgbThumb, thumbPath); + } + Color newThumbPenColor = thumbPenColorPaint; + if (busyMouse || mouseInThumbRegion) + { + newThumbPenColor = ControlPaint.Dark(newThumbPenColor); + } + pen.Color = newThumbPenColor; + e.Graphics.DrawPath(pen, thumbPath); + } + + const int numTicks = 1 + (10 * (5 + 1)); + int interval = 0; + int start = thumbRect.Width / 2; + int w = barRect.Width - thumbRect.Width; + int idx = 0; + pen.Color = _tickColor; + for (int i = 0; i <= 10; i++) + { + e.Graphics.DrawLine(pen, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height - 5); + if (i < 10) + { + for (int j = 0; j <= 5; j++) + { + idx++; + interval = idx * w / (numTicks - 1); + } + } + } + } + + private bool mouseInRegion = false; + private bool mouseInThumbRegion = false; + private bool busyMouse = false; + private void SetValueFromPoint(Point p) + { + int x = p.X; + int margin = thumbSize / 2; + x -= margin; + _value = (long)((x * ((_maximum - _minimum) / (ClientSize.Width - (2f * margin)))) + _minimum); + if (_value < _minimum) + { + _value = _minimum; + } + else if (_value > _maximum) + { + _value = _maximum; + } + ValueChanged?.Invoke(this, new EventArgs()); + } + protected override void OnEnabledChanged(EventArgs e) + { + base.OnEnabledChanged(e); + Invalidate(); + } + protected override void OnMouseEnter(EventArgs e) + { + base.OnMouseEnter(e); + mouseInRegion = true; + Invalidate(); + } + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + mouseInRegion = false; + mouseInThumbRegion = false; + Invalidate(); + } + protected override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); + busyMouse = (MouseButtons & MouseButtons.Left) != MouseButtons.None; + if (busyMouse) + { + SetValueFromPoint(e.Location); + } + Invalidate(); + } + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); + if (busyMouse) + { + SetValueFromPoint(e.Location); + } + Invalidate(); + } + protected override void OnMouseUp(MouseEventArgs e) + { + base.OnMouseUp(e); + mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); + bool old = busyMouse; + busyMouse = old && e.Button == MouseButtons.Left ? false : old; + Invalidate(); + } + protected override void OnGotFocus(EventArgs e) + { + base.OnGotFocus(e); + Invalidate(); + } + protected override void OnLostFocus(EventArgs e) + { + base.OnLostFocus(e); + Invalidate(); + } + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (_acceptKeys && !busyMouse) + { + switch (e.KeyCode) + { + case Keys.Down: + case Keys.Left: + { + long newVal = _value - _smallChange; + if (newVal < _minimum) + { + newVal = _minimum; + } + Value = newVal; + break; + } + case Keys.Up: + case Keys.Right: + { + long newVal = _value + _smallChange; + if (newVal > _maximum) + { + newVal = _maximum; + } + Value = newVal; + break; + } + case Keys.Home: + { + Value = _minimum; + break; + } + case Keys.End: + { + Value = _maximum; + break; + } + case Keys.PageDown: + { + long newVal = _value - _largeChange; + if (newVal < _minimum) + { + newVal = _minimum; + } + Value = newVal; + break; + } + case Keys.PageUp: + { + long newVal = _value + _largeChange; + if (newVal > _maximum) + { + newVal = _maximum; + } + Value = newVal; + break; + } + } + } + } + protected override bool ProcessDialogKey(Keys keyData) + { + return !_acceptKeys || keyData == Keys.Tab || ModifierKeys == Keys.Shift ? base.ProcessDialogKey(keyData) : false; + } + + private static GraphicsPath CreateRoundRectPath(Rectangle rect, int size) + { + var gp = new GraphicsPath(); + gp.AddLine(rect.Left + (size / 2), rect.Top, rect.Right - (size / 2), rect.Top); + gp.AddArc(rect.Right - size, rect.Top, size, size, 270, 90); + + gp.AddLine(rect.Right, rect.Top + (size / 2), rect.Right, rect.Bottom - (size / 2)); + gp.AddArc(rect.Right - size, rect.Bottom - size, size, size, 0, 90); + + gp.AddLine(rect.Right - (size / 2), rect.Bottom, rect.Left + (size / 2), rect.Bottom); + gp.AddArc(rect.Left, rect.Bottom - size, size, size, 90, 90); + + gp.AddLine(rect.Left, rect.Bottom - (size / 2), rect.Left, rect.Top + (size / 2)); + gp.AddArc(rect.Left, rect.Top, size, size, 180, 90); + return gp; + } + private static Color[] DesaturateColors(params Color[] colors) + { + var ret = new Color[colors.Length]; + for (int i = 0; i < colors.Length; i++) + { + int gray = (int)((colors[i].R * 0.3) + (colors[i].G * 0.6) + (colors[i].B * 0.1)); + ret[i] = Color.FromArgb((-0x010101 * (255 - gray)) - 1); + } + return ret; + } + private static Color[] LightenColors(params Color[] colors) + { + var ret = new Color[colors.Length]; + for (int i = 0; i < colors.Length; i++) + { + ret[i] = ControlPaint.Light(colors[i]); + } + return ret; + } + private static bool IsPointInRect(Point p, Rectangle rect) + { + return p.X > rect.Left & p.X < rect.Right & p.Y > rect.Top & p.Y < rect.Bottom; + } + } +} diff --git a/VG Music Studio.backup/UI/FlexibleMessageBox.cs b/VG Music Studio.backup/UI/FlexibleMessageBox.cs new file mode 100644 index 00000000..30d29ad1 --- /dev/null +++ b/VG Music Studio.backup/UI/FlexibleMessageBox.cs @@ -0,0 +1,697 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + /* FlexibleMessageBox – A flexible replacement for the .NET MessageBox + * + * Author: Jörg Reichert (public@jreichert.de) + * Contributors: Thanks to: David Hall, Roink + * Version: 1.3 + * Published at: http://www.codeproject.com/Articles/601900/FlexibleMessageBox + * + ************************************************************************************************************ + * Features: + * - It can be simply used instead of MessageBox since all important static "Show"-Functions are supported + * - It is small, only one source file, which could be added easily to each solution + * - It can be resized and the content is correctly word-wrapped + * - It tries to auto-size the width to show the longest text row + * - It never exceeds the current desktop working area + * - It displays a vertical scrollbar when needed + * - It does support hyperlinks in text + * + * Because the interface is identical to MessageBox, you can add this single source file to your project + * and use the FlexibleMessageBox almost everywhere you use a standard MessageBox. + * The goal was NOT to produce as many features as possible but to provide a simple replacement to fit my + * own needs. Feel free to add additional features on your own, but please left my credits in this class. + * + ************************************************************************************************************ + * Usage examples: + * + * FlexibleMessageBox.Show("Just a text"); + * + * FlexibleMessageBox.Show("A text", + * "A caption"); + * + * FlexibleMessageBox.Show("Some text with a link: www.google.com", + * "Some caption", + * MessageBoxButtons.AbortRetryIgnore, + * MessageBoxIcon.Information, + * MessageBoxDefaultButton.Button2); + * + * var dialogResult = FlexibleMessageBox.Show("Do you know the answer to life the universe and everything?", + * "One short question", + * MessageBoxButtons.YesNo); + * + ************************************************************************************************************ + * THE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS", WITHOUT WARRANTY + * OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHOR BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THIS + * SOFTWARE. + * + ************************************************************************************************************ + * History: + * Version 1.3 - 19.Dezember 2014 + * - Added refactoring function GetButtonText() + * - Used CurrentUICulture instead of InstalledUICulture + * - Added more button localizations. Supported languages are now: ENGLISH, GERMAN, SPANISH, ITALIAN + * - Added standard MessageBox handling for "copy to clipboard" with + and + + * - Tab handling is now corrected (only tabbing over the visible buttons) + * - Added standard MessageBox handling for ALT-Keyboard shortcuts + * - SetDialogSizes: Refactored completely: Corrected sizing and added caption driven sizing + * + * Version 1.2 - 10.August 2013 + * - Do not ShowInTaskbar anymore (original MessageBox is also hidden in taskbar) + * - Added handling for Escape-Button + * - Adapted top right close button (red X) to behave like MessageBox (but hidden instead of deactivated) + * + * Version 1.1 - 14.June 2013 + * - Some Refactoring + * - Added internal form class + * - Added missing code comments, etc. + * + * Version 1.0 - 15.April 2013 + * - Initial Version + */ + + internal class FlexibleMessageBox + { + #region Public statics + + /// + /// Defines the maximum width for all FlexibleMessageBox instances in percent of the working area. + /// + /// Allowed values are 0.2 - 1.0 where: + /// 0.2 means: The FlexibleMessageBox can be at most half as wide as the working area. + /// 1.0 means: The FlexibleMessageBox can be as wide as the working area. + /// + /// Default is: 70% of the working area width. + /// + public static double MAX_WIDTH_FACTOR = 0.7; + + /// + /// Defines the maximum height for all FlexibleMessageBox instances in percent of the working area. + /// + /// Allowed values are 0.2 - 1.0 where: + /// 0.2 means: The FlexibleMessageBox can be at most half as high as the working area. + /// 1.0 means: The FlexibleMessageBox can be as high as the working area. + /// + /// Default is: 90% of the working area height. + /// + public static double MAX_HEIGHT_FACTOR = 0.9; + + /// + /// Defines the font for all FlexibleMessageBox instances. + /// + /// Default is: Theme.Font + /// + public static Font FONT = Theme.Font; + + #endregion + + #region Public show functions + + public static DialogResult Show(string text) + { + return FlexibleMessageBoxForm.Show(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text) + { + return FlexibleMessageBoxForm.Show(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption) + { + return FlexibleMessageBoxForm.Show(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(Exception ex, string caption) + { + return FlexibleMessageBoxForm.Show(null, string.Format("Error Details:{1}{1}{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace), caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text, string caption) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons) + { + return FlexibleMessageBoxForm.Show(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) + { + return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + { + return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, defaultButton); + } + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, defaultButton); + } + + #endregion + + #region Internal form class + + class FlexibleMessageBoxForm : ThemedForm + { + IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + void InitializeComponent() + { + components = new Container(); + button1 = new ThemedButton(); + richTextBoxMessage = new ThemedRichTextBox(); + FlexibleMessageBoxFormBindingSource = new BindingSource(components); + panel1 = new ThemedPanel(); + pictureBoxForIcon = new PictureBox(); + button2 = new ThemedButton(); + button3 = new ThemedButton(); + ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).BeginInit(); + panel1.SuspendLayout(); + ((ISupportInitialize)(pictureBoxForIcon)).BeginInit(); + SuspendLayout(); + // + // button1 + // + button1.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + button1.AutoSize = true; + button1.DialogResult = DialogResult.OK; + button1.Location = new Point(11, 67); + button1.MinimumSize = new Size(0, 24); + button1.Name = "button1"; + button1.Size = new Size(75, 24); + button1.TabIndex = 2; + button1.Text = "OK"; + button1.UseVisualStyleBackColor = true; + button1.Visible = false; + // + // richTextBoxMessage + // + richTextBoxMessage.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) + | AnchorStyles.Left) + | AnchorStyles.Right); + richTextBoxMessage.BorderStyle = BorderStyle.None; + richTextBoxMessage.DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "MessageText", true, DataSourceUpdateMode.OnPropertyChanged)); + richTextBoxMessage.Font = new Font(Theme.Font.FontFamily, 9); + richTextBoxMessage.Location = new Point(50, 26); + richTextBoxMessage.Margin = new Padding(0); + richTextBoxMessage.Name = "richTextBoxMessage"; + richTextBoxMessage.ReadOnly = true; + richTextBoxMessage.ScrollBars = RichTextBoxScrollBars.Vertical; + richTextBoxMessage.Size = new Size(200, 20); + richTextBoxMessage.TabIndex = 0; + richTextBoxMessage.TabStop = false; + richTextBoxMessage.Text = ""; + richTextBoxMessage.LinkClicked += new LinkClickedEventHandler(LinkClicked); + // + // panel1 + // + panel1.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) + | AnchorStyles.Left) + | AnchorStyles.Right); + panel1.Controls.Add(pictureBoxForIcon); + panel1.Controls.Add(richTextBoxMessage); + panel1.Location = new Point(-3, -4); + panel1.Name = "panel1"; + panel1.Size = new Size(268, 59); + panel1.TabIndex = 1; + // + // pictureBoxForIcon + // + pictureBoxForIcon.BackColor = Color.Transparent; + pictureBoxForIcon.Location = new Point(15, 19); + pictureBoxForIcon.Name = "pictureBoxForIcon"; + pictureBoxForIcon.Size = new Size(32, 32); + pictureBoxForIcon.TabIndex = 8; + pictureBoxForIcon.TabStop = false; + // + // button2 + // + button2.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); + button2.DialogResult = DialogResult.OK; + button2.Location = new Point(92, 67); + button2.MinimumSize = new Size(0, 24); + button2.Name = "button2"; + button2.Size = new Size(75, 24); + button2.TabIndex = 3; + button2.Text = "OK"; + button2.UseVisualStyleBackColor = true; + button2.Visible = false; + // + // button3 + // + button3.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); + button3.AutoSize = true; + button3.DialogResult = DialogResult.OK; + button3.Location = new Point(173, 67); + button3.MinimumSize = new Size(0, 24); + button3.Name = "button3"; + button3.Size = new Size(75, 24); + button3.TabIndex = 0; + button3.Text = "OK"; + button3.UseVisualStyleBackColor = true; + button3.Visible = false; + // + // FlexibleMessageBoxForm + // + AutoScaleDimensions = new SizeF(6F, 13F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(260, 102); + Controls.Add(button3); + Controls.Add(button2); + Controls.Add(panel1); + Controls.Add(button1); + DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "CaptionText", true)); + Icon = Properties.Resources.Icon; + MaximizeBox = false; + MinimizeBox = false; + MinimumSize = new Size(276, 140); + Name = "FlexibleMessageBoxForm"; + SizeGripStyle = SizeGripStyle.Show; + StartPosition = FormStartPosition.CenterParent; + Text = ""; + Shown += new EventHandler(FlexibleMessageBoxForm_Shown); + ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).EndInit(); + panel1.ResumeLayout(false); + ((ISupportInitialize)(pictureBoxForIcon)).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + ThemedButton button1, button2, button3; + private BindingSource FlexibleMessageBoxFormBindingSource; + ThemedRichTextBox richTextBoxMessage; + ThemedPanel panel1; + private PictureBox pictureBoxForIcon; + + #region Private constants + + //These separators are used for the "copy to clipboard" standard operation, triggered by Ctrl + C (behavior and clipboard format is like in a standard MessageBox) + static readonly String STANDARD_MESSAGEBOX_SEPARATOR_LINES = "---------------------------\n"; + static readonly String STANDARD_MESSAGEBOX_SEPARATOR_SPACES = " "; + + //These are the possible buttons (in a standard MessageBox) + private enum ButtonID { OK = 0, CANCEL, YES, NO, ABORT, RETRY, IGNORE }; + + //These are the buttons texts for different languages. + //If you want to add a new language, add it here and in the GetButtonText-Function + private enum TwoLetterISOLanguageID { en, de, es, it }; + static readonly String[] BUTTON_TEXTS_ENGLISH_EN = { "OK", "Cancel", "&Yes", "&No", "&Abort", "&Retry", "&Ignore" }; //Note: This is also the fallback language + static readonly String[] BUTTON_TEXTS_GERMAN_DE = { "OK", "Abbrechen", "&Ja", "&Nein", "&Abbrechen", "&Wiederholen", "&Ignorieren" }; + static readonly String[] BUTTON_TEXTS_SPANISH_ES = { "Aceptar", "Cancelar", "&Sí", "&No", "&Abortar", "&Reintentar", "&Ignorar" }; + static readonly String[] BUTTON_TEXTS_ITALIAN_IT = { "OK", "Annulla", "&Sì", "&No", "&Interrompi", "&Riprova", "&Ignora" }; + + #endregion + + #region Private members + + MessageBoxDefaultButton defaultButton; + int visibleButtonsCount; + readonly TwoLetterISOLanguageID languageID = TwoLetterISOLanguageID.en; + + #endregion + + #region Private constructor + + private FlexibleMessageBoxForm() + { + InitializeComponent(); + + //Try to evaluate the language. If this fails, the fallback language English will be used + Enum.TryParse(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, out languageID); + + KeyPreview = true; + KeyUp += FlexibleMessageBoxForm_KeyUp; + } + + #endregion + + #region Private helper functions + + static string[] GetStringRows(string message) + { + if (string.IsNullOrEmpty(message)) + { + return null; + } + + var messageRows = message.Split(new char[] { '\n' }, StringSplitOptions.None); + return messageRows; + } + + string GetButtonText(ButtonID buttonID) + { + var buttonTextArrayIndex = Convert.ToInt32(buttonID); + + switch (languageID) + { + case TwoLetterISOLanguageID.de: return BUTTON_TEXTS_GERMAN_DE[buttonTextArrayIndex]; + case TwoLetterISOLanguageID.es: return BUTTON_TEXTS_SPANISH_ES[buttonTextArrayIndex]; + case TwoLetterISOLanguageID.it: return BUTTON_TEXTS_ITALIAN_IT[buttonTextArrayIndex]; + + default: return BUTTON_TEXTS_ENGLISH_EN[buttonTextArrayIndex]; + } + } + + static double GetCorrectedWorkingAreaFactor(double workingAreaFactor) + { + const double MIN_FACTOR = 0.2; + const double MAX_FACTOR = 1.0; + + if (workingAreaFactor < MIN_FACTOR) + { + return MIN_FACTOR; + } + + if (workingAreaFactor > MAX_FACTOR) + { + return MAX_FACTOR; + } + + return workingAreaFactor; + } + + static void SetDialogStartPosition(FlexibleMessageBoxForm flexibleMessageBoxForm, IWin32Window owner) + { + //If no owner given: Center on current screen + if (owner == null) + { + var screen = Screen.FromPoint(Cursor.Position); + flexibleMessageBoxForm.StartPosition = FormStartPosition.Manual; + flexibleMessageBoxForm.Left = screen.Bounds.Left + screen.Bounds.Width / 2 - flexibleMessageBoxForm.Width / 2; + flexibleMessageBoxForm.Top = screen.Bounds.Top + screen.Bounds.Height / 2 - flexibleMessageBoxForm.Height / 2; + } + } + + static void SetDialogSizes(FlexibleMessageBoxForm flexibleMessageBoxForm, string text, string caption) + { + //First set the bounds for the maximum dialog size + flexibleMessageBoxForm.MaximumSize = new Size(Convert.ToInt32(SystemInformation.WorkingArea.Width * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_WIDTH_FACTOR)), + Convert.ToInt32(SystemInformation.WorkingArea.Height * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_HEIGHT_FACTOR))); + + //Get rows. Exit if there are no rows to render... + var stringRows = GetStringRows(text); + if (stringRows == null) + { + return; + } + + //Calculate whole text height + var textHeight = TextRenderer.MeasureText(text, FONT).Height; + + //Calculate width for longest text line + const int SCROLLBAR_WIDTH_OFFSET = 15; + var longestTextRowWidth = stringRows.Max(textForRow => TextRenderer.MeasureText(textForRow, FONT).Width); + var captionWidth = TextRenderer.MeasureText(caption, SystemFonts.CaptionFont).Width; + var textWidth = Math.Max(longestTextRowWidth + SCROLLBAR_WIDTH_OFFSET, captionWidth); + + //Calculate margins + var marginWidth = flexibleMessageBoxForm.Width - flexibleMessageBoxForm.richTextBoxMessage.Width; + var marginHeight = flexibleMessageBoxForm.Height - flexibleMessageBoxForm.richTextBoxMessage.Height; + + //Set calculated dialog size (if the calculated values exceed the maximums, they were cut by windows forms automatically) + flexibleMessageBoxForm.Size = new Size(textWidth + marginWidth, + textHeight + marginHeight); + } + + static void SetDialogIcon(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxIcon icon) + { + switch (icon) + { + case MessageBoxIcon.Information: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Information.ToBitmap(); + break; + case MessageBoxIcon.Warning: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Warning.ToBitmap(); + break; + case MessageBoxIcon.Error: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Error.ToBitmap(); + break; + case MessageBoxIcon.Question: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Question.ToBitmap(); + break; + default: + //When no icon is used: Correct placement and width of rich text box. + flexibleMessageBoxForm.pictureBoxForIcon.Visible = false; + flexibleMessageBoxForm.richTextBoxMessage.Left -= flexibleMessageBoxForm.pictureBoxForIcon.Width; + flexibleMessageBoxForm.richTextBoxMessage.Width += flexibleMessageBoxForm.pictureBoxForIcon.Width; + break; + } + } + + static void SetDialogButtons(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxButtons buttons, MessageBoxDefaultButton defaultButton) + { + //Set the buttons visibilities and texts + switch (buttons) + { + case MessageBoxButtons.AbortRetryIgnore: + flexibleMessageBoxForm.visibleButtonsCount = 3; + + flexibleMessageBoxForm.button1.Visible = true; + flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.ABORT); + flexibleMessageBoxForm.button1.DialogResult = DialogResult.Abort; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.IGNORE); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Ignore; + + flexibleMessageBoxForm.ControlBox = false; + break; + + case MessageBoxButtons.OKCancel: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.OK; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case MessageBoxButtons.RetryCancel: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case MessageBoxButtons.YesNo: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.Yes; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.No; + + flexibleMessageBoxForm.ControlBox = false; + break; + + case MessageBoxButtons.YesNoCancel: + flexibleMessageBoxForm.visibleButtonsCount = 3; + + flexibleMessageBoxForm.button1.Visible = true; + flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); + flexibleMessageBoxForm.button1.DialogResult = DialogResult.Yes; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.No; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case MessageBoxButtons.OK: + default: + flexibleMessageBoxForm.visibleButtonsCount = 1; + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.OK; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + } + + //Set default button (used in FlexibleMessageBoxForm_Shown) + flexibleMessageBoxForm.defaultButton = defaultButton; + } + + #endregion + + #region Private event handlers + + void FlexibleMessageBoxForm_Shown(object sender, EventArgs e) + { + int buttonIndexToFocus = 1; + Button buttonToFocus; + + //Set the default button... + switch (defaultButton) + { + case MessageBoxDefaultButton.Button1: + default: + buttonIndexToFocus = 1; + break; + case MessageBoxDefaultButton.Button2: + buttonIndexToFocus = 2; + break; + case MessageBoxDefaultButton.Button3: + buttonIndexToFocus = 3; + break; + } + + if (buttonIndexToFocus > visibleButtonsCount) + { + buttonIndexToFocus = visibleButtonsCount; + } + + if (buttonIndexToFocus == 3) + { + buttonToFocus = button3; + } + else if (buttonIndexToFocus == 2) + { + buttonToFocus = button2; + } + else + { + buttonToFocus = button1; + } + + buttonToFocus.Focus(); + } + + void LinkClicked(object sender, LinkClickedEventArgs e) + { + try + { + Cursor.Current = Cursors.WaitCursor; + Process.Start(e.LinkText); + } + catch (Exception) + { + //Let the caller of FlexibleMessageBoxForm decide what to do with this exception... + throw; + } + finally + { + Cursor.Current = Cursors.Default; + } + } + + void FlexibleMessageBoxForm_KeyUp(object sender, KeyEventArgs e) + { + //Handle standard key strikes for clipboard copy: "Ctrl + C" and "Ctrl + Insert" + if (e.Control && (e.KeyCode == Keys.C || e.KeyCode == Keys.Insert)) + { + var buttonsTextLine = (button1.Visible ? button1.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) + + (button2.Visible ? button2.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) + + (button3.Visible ? button3.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty); + + //Build same clipboard text like the standard .Net MessageBox + var textForClipboard = STANDARD_MESSAGEBOX_SEPARATOR_LINES + + Text + Environment.NewLine + + STANDARD_MESSAGEBOX_SEPARATOR_LINES + + richTextBoxMessage.Text + Environment.NewLine + + STANDARD_MESSAGEBOX_SEPARATOR_LINES + + buttonsTextLine.Replace("&", string.Empty) + Environment.NewLine + + STANDARD_MESSAGEBOX_SEPARATOR_LINES; + + //Set text in clipboard + Clipboard.SetText(textForClipboard); + } + } + + #endregion + + #region Properties (only used for binding) + + public string CaptionText { get; set; } + public string MessageText { get; set; } + + #endregion + + #region Public show function + + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + { + //Create a new instance of the FlexibleMessageBox form + var flexibleMessageBoxForm = new FlexibleMessageBoxForm + { + ShowInTaskbar = false, + + //Bind the caption and the message text + CaptionText = caption, + MessageText = text + }; + flexibleMessageBoxForm.FlexibleMessageBoxFormBindingSource.DataSource = flexibleMessageBoxForm; + + //Set the buttons visibilities and texts. Also set a default button. + SetDialogButtons(flexibleMessageBoxForm, buttons, defaultButton); + + //Set the dialogs icon. When no icon is used: Correct placement and width of rich text box. + SetDialogIcon(flexibleMessageBoxForm, icon); + + //Set the font for all controls + flexibleMessageBoxForm.Font = FONT; + flexibleMessageBoxForm.richTextBoxMessage.Font = FONT; + + //Calculate the dialogs start size (Try to auto-size width to show longest text row). Also set the maximum dialog size. + SetDialogSizes(flexibleMessageBoxForm, text, caption); + + //Set the dialogs start position when given. Otherwise center the dialog on the current screen. + SetDialogStartPosition(flexibleMessageBoxForm, owner); + + //Show the dialog + return flexibleMessageBoxForm.ShowDialog(owner); + } + + #endregion + } //class FlexibleMessageBoxForm + + #endregion + } +} diff --git a/VG Music Studio.backup/UI/ImageComboBox.cs b/VG Music Studio.backup/UI/ImageComboBox.cs new file mode 100644 index 00000000..c928af92 --- /dev/null +++ b/VG Music Studio.backup/UI/ImageComboBox.cs @@ -0,0 +1,62 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + internal class ImageComboBox : ComboBox + { + private const int _imgSize = 15; + private bool _open = false; + + public ImageComboBox() + { + DrawMode = DrawMode.OwnerDrawFixed; + DropDownStyle = ComboBoxStyle.DropDown; + } + + protected override void OnDrawItem(DrawItemEventArgs e) + { + e.DrawBackground(); + e.DrawFocusRectangle(); + + if (e.Index >= 0) + { + ImageComboBoxItem item = Items[e.Index] as ImageComboBoxItem ?? throw new InvalidCastException($"Item was not of type \"{nameof(ImageComboBoxItem)}\""); + int indent = _open ? item.IndentLevel : 0; + e.Graphics.DrawImage(item.Image, e.Bounds.Left + (indent * _imgSize), e.Bounds.Top, _imgSize, _imgSize); + e.Graphics.DrawString(item.ToString(), e.Font, new SolidBrush(e.ForeColor), e.Bounds.Left + (indent * _imgSize) + _imgSize, e.Bounds.Top); + } + + base.OnDrawItem(e); + } + protected override void OnDropDown(EventArgs e) + { + _open = true; + base.OnDropDown(e); + } + protected override void OnDropDownClosed(EventArgs e) + { + _open = false; + base.OnDropDownClosed(e); + } + } + internal class ImageComboBoxItem + { + public object Item { get; } + public Image Image { get; } + public int IndentLevel { get; } + + public ImageComboBoxItem(object item, Image image, int indentLevel) + { + Item = item; + Image = image; + IndentLevel = indentLevel; + } + + public override string ToString() + { + return Item.ToString(); + } + } +} diff --git a/VG Music Studio.backup/UI/MainForm.cs b/VG Music Studio.backup/UI/MainForm.cs new file mode 100644 index 00000000..761394fa --- /dev/null +++ b/VG Music Studio.backup/UI/MainForm.cs @@ -0,0 +1,836 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using Microsoft.WindowsAPICodePack.Dialogs; +using Microsoft.WindowsAPICodePack.Taskbar; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory("")] + internal class MainForm : ThemedForm + { + private const int _intendedWidth = 675; + private const int _intendedHeight = 675 + 1 + 125 + 24; + + public static MainForm Instance { get; } = new MainForm(); + + public readonly bool[] PianoTracks = new bool[SongInfoControl.SongInfo.MaxTracks]; + + private bool _playlistPlaying; + private Config.Playlist _curPlaylist; + private long _curSong = -1; + private readonly List _playedSongs = new List(); + private readonly List _remainingSongs = new List(); + + private TrackViewer _trackViewer; + + #region Controls + + private readonly MenuStrip _mainMenu; + private readonly ToolStripMenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, + _dataItem, _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem, + _playlistItem, _endPlaylistItem; + private readonly Timer _timer; + private readonly ThemedNumeric _songNumerical; + private readonly ThemedButton _playButton, _pauseButton, _stopButton; + private readonly SplitContainer _splitContainer; + private readonly PianoControl _piano; + private readonly ColorSlider _volumeBar, _positionBar; + private readonly SongInfoControl _songInfo; + private readonly ImageComboBox _songsComboBox; + private readonly ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; + + #endregion + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _timer.Dispose(); + } + base.Dispose(disposing); + } + private MainForm() + { + for (int i = 0; i < PianoTracks.Length; i++) + { + PianoTracks[i] = true; + } + + // File Menu + _openDSEItem = new ToolStripMenuItem { Text = Strings.MenuOpenDSE }; + _openDSEItem.Click += OpenDSE; + _openAlphaDreamItem = new ToolStripMenuItem { Text = Strings.MenuOpenAlphaDream }; + _openAlphaDreamItem.Click += OpenAlphaDream; + _openMP2KItem = new ToolStripMenuItem { Text = Strings.MenuOpenMP2K }; + _openMP2KItem.Click += OpenMP2K; + _openSDATItem = new ToolStripMenuItem { Text = Strings.MenuOpenSDAT }; + _openSDATItem.Click += OpenSDAT; + _fileItem = new ToolStripMenuItem { Text = Strings.MenuFile }; + _fileItem.DropDownItems.AddRange(new ToolStripItem[] { _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem }); + + // Data Menu + _trackViewerItem = new ToolStripMenuItem { ShortcutKeys = Keys.Control | Keys.T, Text = Strings.TrackViewerTitle }; + _trackViewerItem.Click += OpenTrackViewer; + _exportDLSItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveDLS }; + _exportDLSItem.Click += ExportDLS; + _exportMIDIItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveMIDI }; + _exportMIDIItem.Click += ExportMIDI; + _exportSF2Item = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveSF2 }; + _exportSF2Item.Click += ExportSF2; + _exportWAVItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveWAV }; + _exportWAVItem.Click += ExportWAV; + _dataItem = new ToolStripMenuItem { Text = Strings.MenuData }; + _dataItem.DropDownItems.AddRange(new ToolStripItem[] { _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem }); + + // Playlist Menu + _endPlaylistItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuEndPlaylist }; + _endPlaylistItem.Click += EndCurrentPlaylist; + _playlistItem = new ToolStripMenuItem { Text = Strings.MenuPlaylist }; + _playlistItem.DropDownItems.AddRange(new ToolStripItem[] { _endPlaylistItem }); + + // Main Menu + _mainMenu = new MenuStrip { Size = new Size(_intendedWidth, 24) }; + _mainMenu.Items.AddRange(new ToolStripItem[] { _fileItem, _dataItem, _playlistItem }); + + // Buttons + _playButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumSpringGreen, Text = Strings.PlayerPlay }; + _playButton.Click += (o, e) => Play(); + _pauseButton = new ThemedButton { Enabled = false, ForeColor = Color.DeepSkyBlue, Text = Strings.PlayerPause }; + _pauseButton.Click += (o, e) => Pause(); + _stopButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumVioletRed, Text = Strings.PlayerStop }; + _stopButton.Click += (o, e) => Stop(); + + // Numerical + _songNumerical = new ThemedNumeric { Enabled = false, Minimum = 0, Visible = false }; + _songNumerical.ValueChanged += SongNumerical_ValueChanged; + + // Timer + _timer = new Timer(); + _timer.Tick += UpdateUI; + + // Piano + _piano = new PianoControl(); + + // Volume bar + _volumeBar = new ColorSlider { Enabled = false, LargeChange = 20, Maximum = 100, SmallChange = 5 }; + _volumeBar.ValueChanged += VolumeBar_ValueChanged; + + // Position bar + _positionBar = new ColorSlider { AcceptKeys = false, Enabled = false, Maximum = 0 }; + _positionBar.MouseUp += PositionBar_MouseUp; + _positionBar.MouseDown += PositionBar_MouseDown; + + // Playlist box + _songsComboBox = new ImageComboBox { Enabled = false }; + _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; + + // Track info + _songInfo = new SongInfoControl { Dock = DockStyle.Fill }; + + // Split container + _splitContainer = new SplitContainer { BackColor = Theme.TitleBar, Dock = DockStyle.Fill, IsSplitterFixed = true, Orientation = Orientation.Horizontal, SplitterWidth = 1 }; + _splitContainer.Panel1.Controls.AddRange(new Control[] { _playButton, _pauseButton, _stopButton, _songNumerical, _songsComboBox, _piano, _volumeBar, _positionBar }); + _splitContainer.Panel2.Controls.Add(_songInfo); + + // MainForm + ClientSize = new Size(_intendedWidth, _intendedHeight); + Controls.AddRange(new Control[] { _splitContainer, _mainMenu }); + MainMenuStrip = _mainMenu; + MinimumSize = new Size(_intendedWidth + (Width - _intendedWidth), _intendedHeight + (Height - _intendedHeight)); // Borders + Resize += OnResize; + Text = Utils.ProgramName; + + // Taskbar Buttons + if (TaskbarManager.IsPlatformSupported) + { + _prevTButton = new ThumbnailToolBarButton(Resources.IconPrevious, Strings.PlayerPreviousSong); + _prevTButton.Click += PlayPreviousSong; + _toggleTButton = new ThumbnailToolBarButton(Resources.IconPlay, Strings.PlayerPlay); + _toggleTButton.Click += TogglePlayback; + _nextTButton = new ThumbnailToolBarButton(Resources.IconNext, Strings.PlayerNextSong); + _nextTButton.Click += PlayNextSong; + _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = false; + TaskbarManager.Instance.ThumbnailToolBars.AddButtons(Handle, _prevTButton, _toggleTButton, _nextTButton); + } + + OnResize(null, null); + } + + private void VolumeBar_ValueChanged(object sender, EventArgs e) + { + Engine.Instance.Mixer.SetVolume(_volumeBar.Value / (float)_volumeBar.Maximum); + } + public void SetVolumeBarValue(float volume) + { + _volumeBar.ValueChanged -= VolumeBar_ValueChanged; + _volumeBar.Value = (int)(volume * _volumeBar.Maximum); + _volumeBar.ValueChanged += VolumeBar_ValueChanged; + } + private bool _positionBarFree = true; + private void PositionBar_MouseUp(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + Engine.Instance.Player.SetCurrentPosition(_positionBar.Value); + _positionBarFree = true; + LetUIKnowPlayerIsPlaying(); + } + } + private void PositionBar_MouseDown(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + _positionBarFree = false; + } + } + + private bool _autoplay = false; + private void SongNumerical_ValueChanged(object sender, EventArgs e) + { + _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; + + long index = (long)_songNumerical.Value; + Stop(); + Text = Utils.ProgramName; + _songsComboBox.SelectedIndex = 0; + _songInfo.DeleteData(); + bool success; + try + { + Engine.Instance.Player.LoadSong(index); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, string.Format(Strings.ErrorLoadSong, Engine.Instance.Config.GetSongName(index))); + success = false; + } + + _trackViewer?.UpdateTracks(); + if (success) + { + Config config = Engine.Instance.Config; + List songs = config.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 + Config.Song song = songs.SingleOrDefault(s => s.Index == index); + if (song != null) + { + Text = $"{Utils.ProgramName} - {song.Name}"; + _songsComboBox.SelectedIndex = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox + } + _positionBar.Maximum = Engine.Instance.Player.MaxTicks; + _positionBar.LargeChange = _positionBar.Maximum / 10; + _positionBar.SmallChange = _positionBar.LargeChange / 4; + _songInfo.SetNumTracks(Engine.Instance.Player.Events.Length); + if (_autoplay) + { + Play(); + } + } + else + { + _songInfo.SetNumTracks(0); + } + int numTracks = (Engine.Instance.Player.Events?.Length).GetValueOrDefault(); + _positionBar.Enabled = _exportWAVItem.Enabled = success && numTracks > 0; + _exportMIDIItem.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_MP2K && numTracks > 0; + _exportDLSItem.Enabled = _exportSF2Item.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_AlphaDream; + + _autoplay = true; + _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; + } + private void SongsComboBox_SelectedIndexChanged(object sender, EventArgs e) + { + var item = (ImageComboBoxItem)_songsComboBox.SelectedItem; + if (item.Item is Config.Song song) + { + SetAndLoadSong(song.Index); + } + else if (item.Item is Config.Playlist playlist) + { + if (playlist.Songs.Count > 0 + && FlexibleMessageBox.Show(string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist), Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) + { + ResetPlaylistStuff(false); + _curPlaylist = playlist; + Engine.Instance.Player.ShouldFadeOut = _playlistPlaying = true; + Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + _endPlaylistItem.Enabled = true; + SetAndLoadNextPlaylistSong(); + } + } + } + private void SetAndLoadSong(long index) + { + _curSong = index; + if (_songNumerical.Value == index) + { + SongNumerical_ValueChanged(null, null); + } + else + { + _songNumerical.Value = index; + } + } + private void SetAndLoadNextPlaylistSong() + { + if (_remainingSongs.Count == 0) + { + _remainingSongs.AddRange(_curPlaylist.Songs.Select(s => s.Index)); + if (GlobalConfig.Instance.PlaylistMode == PlaylistMode.Random) + { + _remainingSongs.Shuffle(); + } + } + long nextSong = _remainingSongs[0]; + _remainingSongs.RemoveAt(0); + SetAndLoadSong(nextSong); + } + private void ResetPlaylistStuff(bool enableds) + { + if (Engine.Instance != null) + { + Engine.Instance.Player.ShouldFadeOut = false; + } + _playlistPlaying = false; + _curPlaylist = null; + _curSong = -1; + _remainingSongs.Clear(); + _playedSongs.Clear(); + _endPlaylistItem.Enabled = false; + _songNumerical.Enabled = _songsComboBox.Enabled = enableds; + } + private void EndCurrentPlaylist(object sender, EventArgs e) + { + if (FlexibleMessageBox.Show(Strings.EndPlaylistBody, Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) + { + ResetPlaylistStuff(true); + } + } + + private void OpenDSE(object sender, EventArgs e) + { + var d = new CommonOpenFileDialog + { + Title = Strings.MenuOpenDSE, + IsFolderPicker = true + }; + if (d.ShowDialog() == CommonFileDialogResult.Ok) + { + DisposeEngine(); + bool success; + try + { + new Engine(Engine.EngineType.NDS_DSE, d.FileName); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenDSE); + success = false; + } + if (success) + { + var config = (Core.NDS.DSE.Config)Engine.Instance.Config; + FinishLoading(config.BGMFiles.Length); + _songNumerical.Visible = false; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = false; + } + } + } + private void OpenAlphaDream(object sender, EventArgs e) + { + var d = new CommonOpenFileDialog + { + Title = Strings.MenuOpenAlphaDream, + Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } + }; + if (d.ShowDialog() == CommonFileDialogResult.Ok) + { + DisposeEngine(); + bool success; + try + { + new Engine(Engine.EngineType.GBA_AlphaDream, File.ReadAllBytes(d.FileName)); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenAlphaDream); + success = false; + } + if (success) + { + var config = (Core.GBA.AlphaDream.Config)Engine.Instance.Config; + FinishLoading(config.SongTableSizes[0]); + _songNumerical.Visible = true; + _exportDLSItem.Visible = true; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = true; + } + } + } + private void OpenMP2K(object sender, EventArgs e) + { + var d = new CommonOpenFileDialog + { + Title = Strings.MenuOpenMP2K, + Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } + }; + if (d.ShowDialog() == CommonFileDialogResult.Ok) + { + DisposeEngine(); + bool success; + try + { + new Engine(Engine.EngineType.GBA_MP2K, File.ReadAllBytes(d.FileName)); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenMP2K); + success = false; + } + if (success) + { + var config = (Core.GBA.MP2K.Config)Engine.Instance.Config; + FinishLoading(config.SongTableSizes[0]); + _songNumerical.Visible = true; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = true; + _exportSF2Item.Visible = false; + } + } + } + private void OpenSDAT(object sender, EventArgs e) + { + var d = new CommonOpenFileDialog + { + Title = Strings.MenuOpenSDAT, + Filters = { new CommonFileDialogFilter(Strings.FilterOpenSDAT, ".sdat") } + }; + if (d.ShowDialog() == CommonFileDialogResult.Ok) + { + DisposeEngine(); + bool success; + try + { + new Engine(Engine.EngineType.NDS_SDAT, new Core.NDS.SDAT.SDAT(File.ReadAllBytes(d.FileName))); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenSDAT); + success = false; + } + if (success) + { + var config = (Core.NDS.SDAT.Config)Engine.Instance.Config; + FinishLoading(config.SDAT.INFOBlock.SequenceInfos.NumEntries); + _songNumerical.Visible = true; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = false; + } + } + } + + private void ExportDLS(object sender, EventArgs e) + { + var d = new CommonSaveFileDialog + { + DefaultFileName = Engine.Instance.Config.GetGameName(), + DefaultExtension = ".dls", + EnsureValidNames = true, + Title = Strings.MenuSaveDLS, + Filters = { new CommonFileDialogFilter(Strings.FilterSaveDLS, ".dls") } + }; + if (d.ShowDialog() == CommonFileDialogResult.Ok) + { + try + { + Core.GBA.AlphaDream.SoundFontSaver_DLS.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveDLS, d.FileName), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveDLS); + } + } + } + private void ExportMIDI(object sender, EventArgs e) + { + var d = new CommonSaveFileDialog + { + DefaultFileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), + DefaultExtension = ".mid", + EnsureValidNames = true, + Title = Strings.MenuSaveMIDI, + Filters = { new CommonFileDialogFilter(Strings.FilterSaveMIDI, ".mid;.midi") } + }; + if (d.ShowDialog() == CommonFileDialogResult.Ok) + { + var p = (Core.GBA.MP2K.Player)Engine.Instance.Player; + var args = new Core.GBA.MP2K.Player.MIDISaveArgs + { + SaveCommandsBeforeTranspose = true, + ReverseVolume = false, + TimeSignatures = new List<(int AbsoluteTick, (byte Numerator, byte Denominator))> + { + (0, (4, 4)) + } + }; + try + { + p.SaveAsMIDI(d.FileName, args); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveMIDI, d.FileName), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveMIDI); + } + } + } + private void ExportSF2(object sender, EventArgs e) + { + var d = new CommonSaveFileDialog + { + DefaultFileName = Engine.Instance.Config.GetGameName(), + DefaultExtension = ".sf2", + EnsureValidNames = true, + Title = Strings.MenuSaveSF2, + Filters = { new CommonFileDialogFilter(Strings.FilterSaveSF2, ".sf2") } + }; + if (d.ShowDialog() == CommonFileDialogResult.Ok) + { + try + { + Core.GBA.AlphaDream.SoundFontSaver_SF2.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveSF2, d.FileName), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveSF2); + } + } + } + private void ExportWAV(object sender, EventArgs e) + { + var d = new CommonSaveFileDialog + { + DefaultFileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), + DefaultExtension = ".wav", + EnsureValidNames = true, + Title = Strings.MenuSaveWAV, + Filters = { new CommonFileDialogFilter(Strings.FilterSaveWAV, ".wav") } + }; + if (d.ShowDialog() == CommonFileDialogResult.Ok) + { + Stop(); + bool oldFade = Engine.Instance.Player.ShouldFadeOut; + long oldLoops = Engine.Instance.Player.NumLoops; + Engine.Instance.Player.ShouldFadeOut = true; + Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + try + { + Engine.Instance.Player.Record(d.FileName); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, d.FileName), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveWAV); + } + Engine.Instance.Player.ShouldFadeOut = oldFade; + Engine.Instance.Player.NumLoops = oldLoops; + _stopUI = false; + } + } + + public void LetUIKnowPlayerIsPlaying() + { + if (!_timer.Enabled) + { + _pauseButton.Enabled = _stopButton.Enabled = true; + _pauseButton.Text = Strings.PlayerPause; + _timer.Interval = (int)(1_000d / GlobalConfig.Instance.RefreshRate); + _timer.Start(); + UpdateTaskbarState(); + UpdateTaskbarButtons(); + } + } + private void Play() + { + Engine.Instance.Player.Play(); + LetUIKnowPlayerIsPlaying(); + } + private void Pause() + { + Engine.Instance.Player.Pause(); + if (Engine.Instance.Player.State == PlayerState.Paused) + { + _pauseButton.Text = Strings.PlayerUnpause; + _timer.Stop(); + } + else + { + _pauseButton.Text = Strings.PlayerPause; + _timer.Start(); + } + UpdateTaskbarState(); + UpdateTaskbarButtons(); + } + private void Stop() + { + Engine.Instance.Player.Stop(); + _pauseButton.Enabled = _stopButton.Enabled = false; + _pauseButton.Text = Strings.PlayerPause; + _timer.Stop(); + _songInfo.DeleteData(); + _piano.UpdateKeys(_songInfo.Info, PianoTracks); + UpdatePositionIndicators(0L); + UpdateTaskbarState(); + UpdateTaskbarButtons(); + } + private void TogglePlayback(object sender, EventArgs e) + { + if (Engine.Instance.Player.State == PlayerState.Stopped) + { + Play(); + } + else if (Engine.Instance.Player.State == PlayerState.Paused || Engine.Instance.Player.State == PlayerState.Playing) + { + Pause(); + } + } + private void PlayPreviousSong(object sender, EventArgs e) + { + long prevSong; + if (_playlistPlaying) + { + int index = _playedSongs.Count - 1; + prevSong = _playedSongs[index]; + _playedSongs.RemoveAt(index); + _remainingSongs.Insert(0, _curSong); + } + else + { + prevSong = (long)_songNumerical.Value - 1; + } + SetAndLoadSong(prevSong); + } + private void PlayNextSong(object sender, EventArgs e) + { + if (_playlistPlaying) + { + _playedSongs.Add(_curSong); + SetAndLoadNextPlaylistSong(); + } + else + { + SetAndLoadSong((long)_songNumerical.Value + 1); + } + } + + private void FinishLoading(long numSongs) + { + Engine.Instance.Player.SongEnded += SongEnded; + foreach (Config.Playlist playlist in Engine.Instance.Config.Playlists) + { + _songsComboBox.Items.Add(new ImageComboBoxItem(playlist, Resources.IconPlaylist, 0)); + _songsComboBox.Items.AddRange(playlist.Songs.Select(s => new ImageComboBoxItem(s, Resources.IconSong, 1)).ToArray()); + } + _songNumerical.Maximum = numSongs - 1; +#if DEBUG + //VGMSDebug.EventScan(Engine.Instance.Config.Playlists[0].Songs, numericalVisible); +#endif + _autoplay = false; + SetAndLoadSong(Engine.Instance.Config.Playlists[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[0].Songs[0].Index); + _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = true; + UpdateTaskbarButtons(); + } + private void DisposeEngine() + { + if (Engine.Instance != null) + { + Stop(); + Engine.Instance.Dispose(); + } + _trackViewer?.UpdateTracks(); + _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = _positionBar.Enabled = false; + Text = Utils.ProgramName; + _songInfo.SetNumTracks(0); + _songInfo.ResetMutes(); + ResetPlaylistStuff(false); + UpdatePositionIndicators(0L); + UpdateTaskbarState(); + _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; + _songNumerical.ValueChanged -= SongNumerical_ValueChanged; + _songNumerical.Visible = false; + _songNumerical.Value = _songNumerical.Maximum = 0; + _songsComboBox.SelectedItem = null; + _songsComboBox.Items.Clear(); + _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; + _songNumerical.ValueChanged += SongNumerical_ValueChanged; + } + private bool _stopUI = false; + private void UpdateUI(object sender, EventArgs e) + { + if (_stopUI) + { + _stopUI = false; + if (_playlistPlaying) + { + _playedSongs.Add(_curSong); + SetAndLoadNextPlaylistSong(); + } + else + { + Stop(); + } + } + else + { + if (WindowState != FormWindowState.Minimized) + { + SongInfoControl.SongInfo info = _songInfo.Info; + Engine.Instance.Player.GetSongState(info); + _piano.UpdateKeys(info, PianoTracks); + _songInfo.Invalidate(); + } + UpdatePositionIndicators(Engine.Instance.Player.ElapsedTicks); + } + } + private void SongEnded() + { + _stopUI = true; + } + private void UpdatePositionIndicators(long ticks) + { + if (_positionBarFree) + { + _positionBar.Value = ticks; + } + if (GlobalConfig.Instance.TaskbarProgress && TaskbarManager.IsPlatformSupported) + { + TaskbarManager.Instance.SetProgressValue((int)ticks, (int)_positionBar.Maximum); + } + } + private void UpdateTaskbarState() + { + if (GlobalConfig.Instance.TaskbarProgress && TaskbarManager.IsPlatformSupported) + { + TaskbarProgressBarState state; + switch (Engine.Instance?.Player.State) + { + case PlayerState.Playing: state = TaskbarProgressBarState.Normal; break; + case PlayerState.Paused: state = TaskbarProgressBarState.Paused; break; + default: state = TaskbarProgressBarState.NoProgress; break; + } + TaskbarManager.Instance.SetProgressState(state); + } + } + private void UpdateTaskbarButtons() + { + if (TaskbarManager.IsPlatformSupported) + { + if (_playlistPlaying) + { + _prevTButton.Enabled = _playedSongs.Count > 0; + _nextTButton.Enabled = true; + } + else + { + _prevTButton.Enabled = _curSong > 0; + _nextTButton.Enabled = _curSong < _songNumerical.Maximum; + } + switch (Engine.Instance.Player.State) + { + case PlayerState.Stopped: _toggleTButton.Icon = Resources.IconPlay; _toggleTButton.Tooltip = Strings.PlayerPlay; break; + case PlayerState.Playing: _toggleTButton.Icon = Resources.IconPause; _toggleTButton.Tooltip = Strings.PlayerPause; break; + case PlayerState.Paused: _toggleTButton.Icon = Resources.IconPlay; _toggleTButton.Tooltip = Strings.PlayerUnpause; break; + } + _toggleTButton.Enabled = true; + } + } + + private void OpenTrackViewer(object sender, EventArgs e) + { + if (_trackViewer != null) + { + _trackViewer.Focus(); + return; + } + _trackViewer = new TrackViewer { Owner = this }; + _trackViewer.FormClosed += (o, s) => _trackViewer = null; + _trackViewer.Show(); + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + DisposeEngine(); + base.OnFormClosing(e); + } + private void OnResize(object sender, EventArgs e) + { + if (WindowState != FormWindowState.Minimized) + { + _splitContainer.SplitterDistance = (int)(ClientSize.Height / 5.5) - 25; // -25 for menustrip (24) and itself (1) + + int w1 = (int)(_splitContainer.Panel1.Width / 2.35); + int h1 = (int)(_splitContainer.Panel1.Height / 5.0); + + int xoff = _splitContainer.Panel1.Width / 83; + int yoff = _splitContainer.Panel1.Height / 25; + int a, b, c, d; + + // Buttons + a = (w1 / 3) - xoff; + b = (xoff / 2) + 1; + _playButton.Location = new Point(xoff + b, yoff); + _pauseButton.Location = new Point((xoff * 2) + a + b, yoff); + _stopButton.Location = new Point((xoff * 3) + (a * 2) + b, yoff); + _playButton.Size = _pauseButton.Size = _stopButton.Size = new Size(a, h1); + c = yoff + ((h1 - 21) / 2); + _songNumerical.Location = new Point((xoff * 4) + (a * 3) + b, c); + _songNumerical.Size = new Size((int)(a / 1.175), 21); + // Song combobox + d = _splitContainer.Panel1.Width - w1 - xoff; + _songsComboBox.Location = new Point(d, c); + _songsComboBox.Size = new Size(w1, 21); + + // Volume bar + c = (int)(_splitContainer.Panel1.Height / 3.5); + _volumeBar.Location = new Point(xoff, c); + _volumeBar.Size = new Size(w1, h1); + // Position bar + _positionBar.Location = new Point(d, c); + _positionBar.Size = new Size(w1, h1); + + // Piano + _piano.Size = new Size(_splitContainer.Panel1.Width, (int)(_splitContainer.Panel1.Height / 2.5)); // Force it to initialize piano keys again + _piano.Location = new Point((_splitContainer.Panel1.Width - (_piano.WhiteKeyWidth * PianoControl.WhiteKeyCount)) / 2, _splitContainer.Panel1.Height - _piano.Height - 1); + } + } + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + if (keyData == Keys.Space && _playButton.Enabled && !_songsComboBox.Focused) + { + TogglePlayback(null, null); + return true; + } + else + { + return base.ProcessCmdKey(ref msg, keyData); + } + } + } +} diff --git a/VG Music Studio.backup/UI/PianoControl.cs b/VG Music Studio.backup/UI/PianoControl.cs new file mode 100644 index 00000000..102a4b0a --- /dev/null +++ b/VG Music Studio.backup/UI/PianoControl.cs @@ -0,0 +1,183 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Util; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory("")] + internal class PianoControl : Control + { + private enum KeyType : byte + { + Black, + White + } + private static readonly KeyType[] KeyTypeTable = new KeyType[12] + { + KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White + }; + private const double blackKeyScale = 2.0 / 3.0; + + public class PianoKey : Control + { + public bool Dirty; + public bool Pressed; + + public readonly SolidBrush OnBrush = new SolidBrush(Color.Transparent); + private readonly SolidBrush _offBrush; + + public PianoKey(byte k) + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, false); + _offBrush = new SolidBrush(new HSLColor(160.0, 0.0, KeyTypeTable[k % 12] == KeyType.White ? k / 12 % 2 == 0 ? 240.0 : 120.0 : 0.0)); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + OnBrush.Dispose(); + _offBrush.Dispose(); + } + base.Dispose(disposing); + } + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.FillRectangle(Pressed ? OnBrush : _offBrush, 1, 1, Width - 2, Height - 2); + e.Graphics.DrawRectangle(Pens.Black, 0, 0, Width - 1, Height - 1); + base.OnPaint(e); + } + } + + private readonly PianoKey[] _keys = new PianoKey[0x80]; + public const int WhiteKeyCount = 75; + public int WhiteKeyWidth; + + public PianoControl() + { + SetStyle(ControlStyles.Selectable, false); + for (byte k = 0; k <= 0x7F; k++) + { + var key = new PianoKey(k); + _keys[k] = key; + if (KeyTypeTable[k % 12] == KeyType.Black) + { + key.BringToFront(); + } + Controls.Add(key); + } + SetKeySizes(); + } + private void SetKeySizes() + { + WhiteKeyWidth = Width / WhiteKeyCount; + int blackKeyWidth = (int)(WhiteKeyWidth * blackKeyScale); + int blackKeyHeight = (int)(Height * blackKeyScale); + int offset = WhiteKeyWidth - (blackKeyWidth / 2); + int w = 0; + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + if (KeyTypeTable[k % 12] == KeyType.White) + { + key.Height = Height; + key.Width = WhiteKeyWidth; + key.Location = new Point(w * WhiteKeyWidth, 0); + w++; + } + else + { + key.Height = blackKeyHeight; + key.Width = blackKeyWidth; + key.Location = new Point(offset + ((w - 1) * WhiteKeyWidth)); + key.BringToFront(); + } + } + } + + public void UpdateKeys(SongInfoControl.SongInfo info, bool[] enabledTracks) + { + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + key.Dirty = key.Pressed; + key.Pressed = false; + } + for (int i = SongInfoControl.SongInfo.MaxTracks - 1; i >= 0; i--) + { + if (enabledTracks[i]) + { + SongInfoControl.SongInfo.Track tin = info.Tracks[i]; + for (int nk = 0; nk < SongInfoControl.SongInfo.MaxKeys; nk++) + { + byte k = tin.Keys[nk]; + if (k == byte.MaxValue) + { + break; + } + else + { + PianoKey key = _keys[k]; + key.OnBrush.Color = GlobalConfig.Instance.Colors[tin.Voice]; + key.Pressed = key.Dirty = true; + } + } + } + } + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + if (key.Dirty) + { + key.Invalidate(); + } + } + } + + protected override void OnResize(EventArgs e) + { + SetKeySizes(); + base.OnResize(e); + } + protected override void Dispose(bool disposing) + { + if (disposing) + { + for (int k = 0; k < 0x80; k++) + { + _keys[k].Dispose(); + } + } + base.Dispose(disposing); + } + } +} diff --git a/VG Music Studio.backup/UI/SongInfoControl.cs b/VG Music Studio.backup/UI/SongInfoControl.cs new file mode 100644 index 00000000..ce91a71d --- /dev/null +++ b/VG Music Studio.backup/UI/SongInfoControl.cs @@ -0,0 +1,354 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory("")] + internal class SongInfoControl : Control + { + public class SongInfo + { + public class Track + { + public long Position; + public byte Voice; + public byte Volume; + public int LFO; + public long Rest; + public sbyte Panpot; + public float LeftVolume; + public float RightVolume; + public int PitchBend; + public byte Extra; + public string Type; + public byte[] Keys = new byte[MaxKeys]; + + public int PreviousKeysTime; + public string PreviousKeys; + + public Track() + { + for (int i = 0; i < MaxKeys; i++) + { + Keys[i] = byte.MaxValue; + } + } + } + public const int MaxKeys = 32 + 1; // DSE is currently set to use 32 channels + public const int MaxTracks = 18; // PMD2 has a few songs with 18 tracks + + public ushort Tempo; + public Track[] Tracks = new Track[MaxTracks]; + + public SongInfo() + { + for (int i = 0; i < MaxTracks; i++) + { + Tracks[i] = new Track(); + } + } + } + + private const int _checkboxSize = 15; + + private readonly CheckBox[] _mutes; + private readonly CheckBox[] _pianos; + private readonly SolidBrush _solidBrush = new SolidBrush(Theme.PlayerColor); + private readonly Pen _pen = new Pen(Color.Transparent); + + public SongInfo Info; + private int _numTracksToDraw; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _solidBrush.Dispose(); + _pen.Dispose(); + } + base.Dispose(disposing); + } + public SongInfoControl() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, false); + Font = new Font("Segoe UI", 10.5f, FontStyle.Regular, GraphicsUnit.Point); + Size = new Size(675, 675); + + _pianos = new CheckBox[SongInfo.MaxTracks + 1]; + _mutes = new CheckBox[SongInfo.MaxTracks + 1]; + for (int i = 0; i < SongInfo.MaxTracks + 1; i++) + { + _pianos[i] = new CheckBox + { + BackColor = Color.Transparent, + Checked = true, + Size = new Size(_checkboxSize, _checkboxSize), + TabStop = false + }; + _pianos[i].CheckStateChanged += TogglePiano; + _mutes[i] = new CheckBox + { + BackColor = Color.Transparent, + Checked = true, + Size = new Size(_checkboxSize, _checkboxSize), + TabStop = false + }; + _mutes[i].CheckStateChanged += ToggleMute; + } + Controls.AddRange(_pianos); + Controls.AddRange(_mutes); + + SetNumTracks(0); + DeleteData(); + } + + private void TogglePiano(object sender, EventArgs e) + { + var check = (CheckBox)sender; + CheckBox master = _pianos[SongInfo.MaxTracks]; + if (check == master) + { + bool b = check.CheckState != CheckState.Unchecked; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + _pianos[i].Checked = b; + } + } + else + { + int numChecked = 0; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + if (_pianos[i] == check) + { + MainForm.Instance.PianoTracks[i] = _pianos[i].Checked; + } + if (_pianos[i].Checked) + { + numChecked++; + } + } + master.CheckStateChanged -= TogglePiano; + master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); + master.CheckStateChanged += TogglePiano; + } + } + private void ToggleMute(object sender, EventArgs e) + { + var check = (CheckBox)sender; + CheckBox master = _mutes[SongInfo.MaxTracks]; + if (check == master) + { + bool b = check.CheckState != CheckState.Unchecked; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + _mutes[i].Checked = b; + } + } + else + { + int numChecked = 0; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + if (_mutes[i] == check) + { + Engine.Instance.Mixer.Mutes[i] = !check.Checked; + } + if (_mutes[i].Checked) + { + numChecked++; + } + } + master.CheckStateChanged -= ToggleMute; + master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); + master.CheckStateChanged += ToggleMute; + } + } + + public void DeleteData() + { + Info = new SongInfo(); + Invalidate(); + } + public void SetNumTracks(int num) + { + _numTracksToDraw = num; + _pianos[SongInfo.MaxTracks].Enabled = _mutes[SongInfo.MaxTracks].Enabled = num > 0; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + _pianos[i].Visible = _mutes[i].Visible = i < num; + } + OnResize(EventArgs.Empty); + } + public void ResetMutes() + { + for (int i = 0; i < SongInfo.MaxTracks + 1; i++) + { + CheckBox mute = _mutes[i]; + mute.CheckStateChanged -= ToggleMute; + mute.CheckState = CheckState.Checked; + mute.CheckStateChanged += ToggleMute; + } + } + + private float _infoHeight, _infoY, _positionX, _keysX, _delayX, _typeEndX, _typeX, _voicesX, _row2ElementAdditionX, _yMargin, _trackHeight, _row2Offset, _tempoX; + private int _barHeight, _barStartX, _barWidth, _bwd, _barRightBoundX, _barCenterX; + protected override void OnResize(EventArgs e) + { + _infoHeight = Height / 30f; + _infoY = _infoHeight - (TextRenderer.MeasureText("A", Font).Height * 1.125f); + _positionX = (_checkboxSize * 2) + 2; + int fWidth = Width - (int)_positionX; // Width between checkboxes' edges and the window edge + _keysX = _positionX + (fWidth / 4.4f); + _delayX = _positionX + (fWidth / 7.5f); + _typeEndX = _positionX + fWidth - (fWidth / 100f); + _typeX = _typeEndX - TextRenderer.MeasureText(Strings.PlayerType, Font).Width; + _voicesX = _positionX + (fWidth / 25f); + _row2ElementAdditionX = fWidth / 15f; + + _yMargin = Height / 200f; + _trackHeight = (Height - _yMargin) / ((_numTracksToDraw < 16 ? 16 : _numTracksToDraw) * 1.04f); + _row2Offset = _trackHeight / 2.5f; + _barHeight = (int)(Height / 30.3f); + _barStartX = (int)(_positionX + (fWidth / 2.35f)); + _barWidth = (int)(fWidth / 2.95f); + _bwd = _barWidth % 2; // Add/Subtract by 1 if the bar width is odd + _barRightBoundX = _barStartX + _barWidth - _bwd; + _barCenterX = _barStartX + (_barWidth / 2); + + _tempoX = _barCenterX - (TextRenderer.MeasureText(string.Format("{0} - 999", Strings.PlayerTempo), Font).Width / 2); + + if (_mutes != null) + { + int x1 = 3; + int x2 = _checkboxSize + 4; + int y = (int)_infoY + 3; + _mutes[SongInfo.MaxTracks].Location = new Point(x1, y); + _pianos[SongInfo.MaxTracks].Location = new Point(x2, y); + for (int i = 0; i < _numTracksToDraw; i++) + { + float r1y = _infoHeight + _yMargin + (i * _trackHeight); + y = (int)r1y + 4; + _mutes[i].Location = new Point(x1, y); + _pianos[i].Location = new Point(x2, y); + } + } + + base.OnResize(e); + } + protected override void OnPaint(PaintEventArgs e) + { + _solidBrush.Color = Theme.PlayerColor; + e.Graphics.FillRectangle(_solidBrush, e.ClipRectangle); + + e.Graphics.DrawString(Strings.PlayerPosition, Font, Brushes.Lime, _positionX, _infoY); + e.Graphics.DrawString(Strings.PlayerRest, Font, Brushes.Crimson, _delayX, _infoY); + e.Graphics.DrawString(Strings.PlayerNotes, Font, Brushes.Turquoise, _keysX, _infoY); + e.Graphics.DrawString("L", Font, Brushes.GreenYellow, _barStartX - 5, _infoY); + e.Graphics.DrawString(string.Format("{0} - ", Strings.PlayerTempo) + Info.Tempo, Font, Brushes.Cyan, _tempoX, _infoY); + e.Graphics.DrawString("R", Font, Brushes.GreenYellow, _barRightBoundX - 5, _infoY); + e.Graphics.DrawString(Strings.PlayerType, Font, Brushes.DeepPink, _typeX, _infoY); + e.Graphics.DrawLine(Pens.Gold, 0, _infoHeight, Width, _infoHeight); + + for (int i = 0; i < _numTracksToDraw; i++) + { + SongInfo.Track track = Info.Tracks[i]; + float r1y = _infoHeight + _yMargin + (i * _trackHeight); // Row 1 y + e.Graphics.DrawString(string.Format("0x{0:X}", track.Position), Font, Brushes.Lime, _positionX, r1y); + e.Graphics.DrawString(track.Rest.ToString(), Font, Brushes.Crimson, _delayX, r1y); + + float r2y = r1y + _row2Offset; // Row 2 y + e.Graphics.DrawString(track.Panpot.ToString(), Font, Brushes.OrangeRed, _voicesX + _row2ElementAdditionX, r2y); + e.Graphics.DrawString(track.Volume.ToString(), Font, Brushes.LightSeaGreen, _voicesX + (_row2ElementAdditionX * 2), r2y); + e.Graphics.DrawString(track.LFO.ToString(), Font, Brushes.SkyBlue, _voicesX + (_row2ElementAdditionX * 3), r2y); + e.Graphics.DrawString(track.PitchBend.ToString(), Font, Brushes.Purple, _voicesX + (_row2ElementAdditionX * 4), r2y); + e.Graphics.DrawString(track.Extra.ToString(), Font, Brushes.HotPink, _voicesX + (_row2ElementAdditionX * 5), r2y); + + int by = (int)(r1y + _yMargin); // Bar y + int byh = by + _barHeight; + e.Graphics.DrawString(track.Type, Font, Brushes.DeepPink, _typeEndX - e.Graphics.MeasureString(track.Type, Font).Width, by + (_row2Offset / (Font.Size / 2.5f))); + e.Graphics.DrawLine(Pens.GreenYellow, _barStartX, by, _barStartX, byh); // Left bar bound line + e.Graphics.DrawLine(Pens.GreenYellow, _barRightBoundX, by, _barRightBoundX, byh); // Right bar bound line + if (GlobalConfig.Instance.PanpotIndicators) + { + int pax = (int)(_barStartX + (_barWidth / 2) + (_barWidth / 2 * (track.Panpot / (float)0x40))); // Pan line x + e.Graphics.DrawLine(Pens.OrangeRed, pax, by, pax, byh); // Pan line + } + + { + Color color = GlobalConfig.Instance.Colors[track.Voice]; + _solidBrush.Color = color; + _pen.Color = color; + e.Graphics.DrawString(track.Voice.ToString(), Font, _solidBrush, _voicesX, r2y); + var rect = new Rectangle((int)(_barStartX + (_barWidth / 2) - (track.LeftVolume * _barWidth / 2)) + _bwd, + by, + (int)((track.LeftVolume + track.RightVolume) * _barWidth / 2), + _barHeight); + if (!rect.IsEmpty) + { + float velocity = (track.LeftVolume + track.RightVolume) * 2f; + if (velocity > 1f) + { + velocity = 1f; + } + _solidBrush.Color = Color.FromArgb((byte)(velocity * byte.MaxValue), color); + e.Graphics.FillRectangle(_solidBrush, rect); + e.Graphics.DrawRectangle(_pen, rect); + } + if (GlobalConfig.Instance.CenterIndicators) + { + e.Graphics.DrawLine(_pen, _barCenterX, by, _barCenterX, byh); // Center line + } + } + { + string keysString; + if (track.Keys[0] == byte.MaxValue) + { + if (track.PreviousKeysTime != 0) + { + track.PreviousKeysTime--; + keysString = track.PreviousKeys; + } + else + { + keysString = string.Empty; + } + } + else + { + keysString = string.Empty; + for (int nk = 0; nk < SongInfo.MaxKeys; nk++) + { + byte k = track.Keys[nk]; + if (k == byte.MaxValue) + { + break; + } + else + { + if (nk != 0) + { + keysString += ' '; + } + keysString += Utils.GetNoteName(k); + } + } + track.PreviousKeysTime = GlobalConfig.Instance.RefreshRate << 2; + track.PreviousKeys = keysString; + } + if (keysString != string.Empty) + { + e.Graphics.DrawString(keysString, Font, Brushes.Turquoise, _keysX, r1y); + } + } + } + base.OnPaint(e); + } + } +} diff --git a/VG Music Studio.backup/UI/Theme.cs b/VG Music Studio.backup/UI/Theme.cs new file mode 100644 index 00000000..0973e2a7 --- /dev/null +++ b/VG Music Studio.backup/UI/Theme.cs @@ -0,0 +1,165 @@ +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + internal static class Theme + { + public static readonly Font Font = new Font("Segoe UI", 8f, FontStyle.Bold); + public static readonly Color + BackColor = Color.FromArgb(33, 33, 39), + BackColorDisabled = Color.FromArgb(35, 42, 47), + BackColorMouseOver = Color.FromArgb(32, 37, 47), + BorderColor = Color.FromArgb(25, 120, 186), + BorderColorDisabled = Color.FromArgb(47, 55, 60), + ForeColor = Color.FromArgb(94, 159, 230), + PlayerColor = Color.FromArgb(8, 8, 8), + SelectionColor = Color.FromArgb(7, 51, 141), + TitleBar = Color.FromArgb(16, 40, 63); + + public static HSLColor DrainColor(Color c) + { + var drained = new HSLColor(c); + drained.Saturation /= 2.5; + return drained; + } + } + + internal class ThemedButton : Button + { + public ThemedButton() : base() + { + FlatAppearance.MouseOverBackColor = Theme.BackColorMouseOver; + FlatStyle = FlatStyle.Flat; + Font = Theme.Font; + ForeColor = Theme.ForeColor; + } + protected override void OnEnabledChanged(EventArgs e) + { + base.OnEnabledChanged(e); + BackColor = Enabled ? Theme.BackColor : Theme.BackColorDisabled; + FlatAppearance.BorderColor = Enabled ? Theme.BorderColor : Theme.BorderColorDisabled; + } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + if (!Enabled) + { + TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, Theme.DrainColor(ForeColor), BackColor); + } + } + protected override bool ShowFocusCues => false; + } + internal class ThemedLabel : Label + { + public ThemedLabel() : base() + { + Font = Theme.Font; + ForeColor = Theme.ForeColor; + } + } + internal class ThemedForm : Form + { + public ThemedForm() : base() + { + BackColor = Theme.BackColor; + Icon = Resources.Icon; + } + } + internal class ThemedPanel : Panel + { + public ThemedPanel() : base() + { + SetStyle(ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); + } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + using (var b = new SolidBrush(BackColor)) + { + e.Graphics.FillRectangle(b, e.ClipRectangle); + } + using (var b = new SolidBrush(Theme.BorderColor)) + using (var p = new Pen(b, 2)) + { + e.Graphics.DrawRectangle(p, e.ClipRectangle); + } + } + private const int WM_PAINT = 0xF; + protected override void WndProc(ref Message m) + { + if (m.Msg == WM_PAINT) + { + Invalidate(); + } + base.WndProc(ref m); + } + } + internal class ThemedTextBox : TextBox + { + public ThemedTextBox() : base() + { + BackColor = Theme.BackColor; + Font = Theme.Font; + ForeColor = Theme.ForeColor; + } + [DllImport("user32.dll")] + private static extern IntPtr GetWindowDC(IntPtr hWnd); + [DllImport("user32.dll")] + private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); + [DllImport("user32.dll")] + private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprc, IntPtr hrgn, uint flags); + private const int WM_NCPAINT = 0x85; + private const uint RDW_INVALIDATE = 0x1; + private const uint RDW_IUPDATENOW = 0x100; + private const uint RDW_FRAME = 0x400; + protected override void WndProc(ref Message m) + { + base.WndProc(ref m); + if (m.Msg == WM_NCPAINT && BorderStyle == BorderStyle.Fixed3D) + { + IntPtr hdc = GetWindowDC(Handle); + using (var g = Graphics.FromHdcInternal(hdc)) + using (var p = new Pen(Theme.BorderColor)) + { + g.DrawRectangle(p, new Rectangle(0, 0, Width - 1, Height - 1)); + } + ReleaseDC(Handle, hdc); + } + } + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero, RDW_FRAME | RDW_IUPDATENOW | RDW_INVALIDATE); + } + } + internal class ThemedRichTextBox : RichTextBox + { + public ThemedRichTextBox() : base() + { + BackColor = Theme.BackColor; + Font = Theme.Font; + ForeColor = Theme.ForeColor; + SelectionColor = Theme.SelectionColor; + } + } + internal class ThemedNumeric : NumericUpDown + { + public ThemedNumeric() : base() + { + BackColor = Theme.BackColor; + Font = new Font(Theme.Font.FontFamily, 7.5f, Theme.Font.Style); + ForeColor = Theme.ForeColor; + TextAlign = HorizontalAlignment.Center; + } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Enabled ? Theme.BorderColor : Theme.BorderColorDisabled, ButtonBorderStyle.Solid); + } + } +} diff --git a/VG Music Studio.backup/UI/TrackViewer.cs b/VG Music Studio.backup/UI/TrackViewer.cs new file mode 100644 index 00000000..7aa83b97 --- /dev/null +++ b/VG Music Studio.backup/UI/TrackViewer.cs @@ -0,0 +1,113 @@ +using BrightIdeasSoftware; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory("")] + internal class TrackViewer : ThemedForm + { + private List _events; + private readonly ObjectListView _listView; + private readonly ComboBox _tracksBox; + + public TrackViewer() + { + int w = (600 / 2) - 12 - 6, h = 400 - 12 - 11; + _listView = new ObjectListView + { + FullRowSelect = true, + HeaderStyle = ColumnHeaderStyle.Nonclickable, + HideSelection = false, + Location = new Point(12, 12), + MultiSelect = false, + RowFormatter = RowColorer, + ShowGroups = false, + Size = new Size(w, h), + UseFiltering = true, + UseFilterIndicator = true + }; + OLVColumn c1, c2, c3, c4; + c1 = new OLVColumn(Strings.TrackViewerEvent, "Command.Label"); + c2 = new OLVColumn(Strings.TrackViewerArguments, "Command.Arguments") { UseFiltering = false }; + c3 = new OLVColumn(Strings.TrackViewerOffset, "Offset") { AspectToStringFormat = "0x{0:X}", UseFiltering = false }; + c4 = new OLVColumn(Strings.TrackViewerTicks, "Ticks") { AspectGetter = (o) => string.Join(", ", ((SongEvent)o).Ticks), UseFiltering = false }; + c1.Width = c2.Width = c3.Width = 72; + c4.Width = 47; + c1.Hideable = c2.Hideable = c3.Hideable = c4.Hideable = false; + c1.TextAlign = c2.TextAlign = c3.TextAlign = c4.TextAlign = HorizontalAlignment.Center; + _listView.AllColumns.AddRange(new OLVColumn[] { c1, c2, c3, c4 }); + _listView.RebuildColumns(); + _listView.ItemActivate += ListView_ItemActivate; + + var panel1 = new ThemedPanel { Location = new Point(306, 12), Size = new Size(w, h) }; + _tracksBox = new ComboBox + { + Enabled = false, + Location = new Point(4, 4), + Size = new Size(100, 21) + }; + _tracksBox.SelectedIndexChanged += TracksBox_SelectedIndexChanged; + panel1.Controls.AddRange(new Control[] { _tracksBox }); + + ClientSize = new Size(600, 400); + Controls.AddRange(new Control[] { _listView, panel1 }); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + Text = $"{Utils.ProgramName} ― {Strings.TrackViewerTitle}"; + + UpdateTracks(); + } + + private void ListView_ItemActivate(object sender, EventArgs e) + { + List list = ((SongEvent)_listView.SelectedItem.RowObject).Ticks; + if (list.Count > 0) + { + Engine.Instance?.Player.SetCurrentPosition(list[0]); + MainForm.Instance.LetUIKnowPlayerIsPlaying(); + } + } + + private void RowColorer(OLVListItem item) + { + item.BackColor = ((SongEvent)item.RowObject).Command.Color; + } + + private void TracksBox_SelectedIndexChanged(object sender, EventArgs e) + { + int i = _tracksBox.SelectedIndex; + if (i != -1) + { + _events = Engine.Instance.Player.Events[i]; + _listView.SetObjects(_events); + } + else + { + _listView.Items.Clear(); + } + } + public void UpdateTracks() + { + int numTracks = (Engine.Instance?.Player.Events?.Length).GetValueOrDefault(); + bool tracks = numTracks > 0; + _tracksBox.Enabled = tracks; + if (tracks) + { + // Track 0, Track 1, ... + _tracksBox.DataSource = Enumerable.Range(0, numTracks).Select(i => string.Format(Strings.TrackViewerTrackX, i)).ToList(); + } + else + { + _tracksBox.DataSource = null; + } + } + } +} \ No newline at end of file diff --git a/VG Music Studio.backup/UI/ValueTextBox.cs b/VG Music Studio.backup/UI/ValueTextBox.cs new file mode 100644 index 00000000..a83e3516 --- /dev/null +++ b/VG Music Studio.backup/UI/ValueTextBox.cs @@ -0,0 +1,104 @@ +using Kermalis.VGMusicStudio.Util; +using System; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + internal class ValueTextBox : ThemedTextBox + { + private bool _hex = false; + public bool Hexadecimal + { + get => _hex; + set + { + _hex = value; + OnTextChanged(EventArgs.Empty); + SelectionStart = Text.Length; + } + } + private long _max = long.MaxValue; + public long Maximum + { + get => _max; + set + { + _max = value; + OnTextChanged(EventArgs.Empty); + } + } + private long _min = 0; + public long Minimum + { + get => _min; + set + { + _min = value; + OnTextChanged(EventArgs.Empty); + } + } + public long Value + { + get + { + if (TextLength > 0) + { + if (Utils.TryParseValue(Text, _min, _max, out long l)) + { + return l; + } + } + return _min; + } + set + { + int i = SelectionStart; + Text = Hexadecimal ? ("0x" + value.ToString("X")) : value.ToString(); + SelectionStart = i; + OnValueChanged(EventArgs.Empty); + } + } + + protected override void WndProc(ref Message m) + { + const int WM_NOTIFY = 0x0282; + if (m.Msg == WM_NOTIFY && m.WParam == new IntPtr(0xB)) + { + if (Hexadecimal && SelectionStart < 2) + { + SelectionStart = 2; + } + } + base.WndProc(ref m); + } + protected override void OnKeyPress(KeyPressEventArgs e) + { + e.Handled = true; // Don't pay attention to this event unless: + + if ((char.IsControl(e.KeyChar) && !(Hexadecimal && SelectionStart <= 2 && SelectionLength == 0 && e.KeyChar == (char)Keys.Back)) || // Backspace isn't used on the "0x" prefix + char.IsDigit(e.KeyChar) || // It is a digit + (e.KeyChar >= 'a' && e.KeyChar <= 'f') || // It is a letter that shows in hex + (e.KeyChar >= 'A' && e.KeyChar <= 'F')) + { + e.Handled = false; + } + base.OnKeyPress(e); + } + protected override void OnTextChanged(EventArgs e) + { + base.OnTextChanged(e); + Value = Value; + } + + private EventHandler _onValueChanged = null; + public event EventHandler ValueChanged + { + add => _onValueChanged += value; + remove => _onValueChanged -= value; + } + protected virtual void OnValueChanged(EventArgs e) + { + _onValueChanged?.Invoke(this, e); + } + } +} diff --git a/VG Music Studio.backup/Util/BetterExceptions.cs b/VG Music Studio.backup/Util/BetterExceptions.cs new file mode 100644 index 00000000..a40a6697 --- /dev/null +++ b/VG Music Studio.backup/Util/BetterExceptions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Kermalis.VGMusicStudio.Util +{ + internal class InvalidValueException : Exception + { + public object Value { get; } + + public InvalidValueException(object value, string message) : base(message) + { + Value = value; + } + } + internal class BetterKeyNotFoundException : KeyNotFoundException + { + public object Key { get; } + + public BetterKeyNotFoundException(object key, Exception innerException) : base($"\"{key}\" was not present in the dictionary.", innerException) + { + Key = key; + } + } +} diff --git a/VG Music Studio.backup/Util/HSLColor.cs b/VG Music Studio.backup/Util/HSLColor.cs new file mode 100644 index 00000000..6dbda597 --- /dev/null +++ b/VG Music Studio.backup/Util/HSLColor.cs @@ -0,0 +1,161 @@ +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Util +{ + // https://richnewman.wordpress.com/about/code-listings-and-diagrams/hslcolor-class/ + class HSLColor + { + // Private data members below are on scale 0-1 + // They are scaled for use externally based on scale + private double hue = 1.0; + private double saturation = 1.0; + private double luminosity = 1.0; + + private const double scale = 240.0; + + public double Hue + { + get { return hue * scale; } + set { hue = CheckRange(value / scale); } + } + public double Saturation + { + get { return saturation * scale; } + set { saturation = CheckRange(value / scale); } + } + public double Luminosity + { + get { return luminosity * scale; } + set { luminosity = CheckRange(value / scale); } + } + + private double CheckRange(double value) + { + if (value < 0.0) + { + value = 0.0; + } + else if (value > 1.0) + { + value = 1.0; + } + return value; + } + + public override string ToString() + { + return string.Format("H: {0:#0.##} S: {1:#0.##} L: {2:#0.##}", Hue, Saturation, Luminosity); + } + + public string ToRGBString() + { + Color color = this; + return string.Format("R: {0:#0.##} G: {1:#0.##} B: {2:#0.##}", color.R, color.G, color.B); + } + + #region Casts to/from System.Drawing.Color + public static implicit operator Color(HSLColor hslColor) + { + double r = 0, g = 0, b = 0; + if (hslColor.luminosity != 0) + { + if (hslColor.saturation == 0) + { + r = g = b = hslColor.luminosity; + } + else + { + double temp2 = GetTemp2(hslColor); + double temp1 = 2.0 * hslColor.luminosity - temp2; + + r = GetColorComponent(temp1, temp2, hslColor.hue + 1.0 / 3.0); + g = GetColorComponent(temp1, temp2, hslColor.hue); + b = GetColorComponent(temp1, temp2, hslColor.hue - 1.0 / 3.0); + } + } + return Color.FromArgb((int)(255 * r), (int)(255 * g), (int)(255 * b)); + } + + private static double GetColorComponent(double temp1, double temp2, double temp3) + { + temp3 = MoveIntoRange(temp3); + if (temp3 < 1.0 / 6.0) + { + return temp1 + (temp2 - temp1) * 6.0 * temp3; + } + else if (temp3 < 0.5) + { + return temp2; + } + else if (temp3 < 2.0 / 3.0) + { + return temp1 + ((temp2 - temp1) * ((2.0 / 3.0) - temp3) * 6.0); + } + else + { + return temp1; + } + } + private static double MoveIntoRange(double temp3) + { + if (temp3 < 0.0) + { + temp3 += 1.0; + } + else if (temp3 > 1.0) + { + temp3 -= 1.0; + } + return temp3; + } + private static double GetTemp2(HSLColor hslColor) + { + double temp2; + if (hslColor.luminosity < 0.5) //<=?? + { + temp2 = hslColor.luminosity * (1.0 + hslColor.saturation); + } + else + { + temp2 = hslColor.luminosity + hslColor.saturation - (hslColor.luminosity * hslColor.saturation); + } + return temp2; + } + + public static implicit operator HSLColor(Color color) + { + HSLColor hslColor = new HSLColor + { + hue = color.GetHue() / 360.0, // We store hue as 0-1 as opposed to 0-360 + luminosity = color.GetBrightness(), + saturation = color.GetSaturation() + }; + return hslColor; + } + #endregion + + public void SetRGB(int red, int green, int blue) + { + HSLColor hslColor = Color.FromArgb(red, green, blue); + hue = hslColor.hue; + saturation = hslColor.saturation; + luminosity = hslColor.luminosity; + } + + public HSLColor() { } + public HSLColor(Color color) + { + SetRGB(color.R, color.G, color.B); + } + public HSLColor(int red, int green, int blue) + { + SetRGB(red, green, blue); + } + public HSLColor(double hue, double saturation, double luminosity) + { + Hue = hue; + Saturation = saturation; + Luminosity = luminosity; + } + } +} diff --git a/VG Music Studio.backup/Util/SampleUtils.cs b/VG Music Studio.backup/Util/SampleUtils.cs new file mode 100644 index 00000000..85ecb43e --- /dev/null +++ b/VG Music Studio.backup/Util/SampleUtils.cs @@ -0,0 +1,16 @@ +namespace Kermalis.VGMusicStudio.Util +{ + internal static class SampleUtils + { + public static short[] PCMU8ToPCM16(byte[] data, int index, int length) + { + short[] ret = new short[length]; + for (int i = 0; i < length; i++) + { + byte b = data[index + i]; + ret[i] = (short)((b - 0x80) << 8); + } + return ret; + } + } +} diff --git a/VG Music Studio.backup/Util/TimeBarrier.cs b/VG Music Studio.backup/Util/TimeBarrier.cs new file mode 100644 index 00000000..c253c0b5 --- /dev/null +++ b/VG Music Studio.backup/Util/TimeBarrier.cs @@ -0,0 +1,60 @@ +using System.Diagnostics; +using System.Threading; + +namespace Kermalis.VGMusicStudio.Util +{ + // Credit to ipatix + internal class TimeBarrier + { + private readonly Stopwatch _sw; + private readonly double _timerInterval; + private readonly double _waitInterval; + private double _lastTimeStamp; + private bool _started; + + public TimeBarrier(double framesPerSecond) + { + _waitInterval = 1.0 / framesPerSecond; + _started = false; + _sw = new Stopwatch(); + _timerInterval = 1.0 / Stopwatch.Frequency; + } + + public void Wait() + { + if (!_started) + { + return; + } + double totalElapsed = _sw.ElapsedTicks * _timerInterval; + double desiredTimeStamp = _lastTimeStamp + _waitInterval; + double timeToWait = desiredTimeStamp - totalElapsed; + if (timeToWait > 0) + { + Thread.Sleep((int)(timeToWait * 1000)); + } + _lastTimeStamp = desiredTimeStamp; + } + + public void Start() + { + if (_started) + { + return; + } + _started = true; + _lastTimeStamp = 0; + _sw.Restart(); + } + + public void Stop() + { + if (!_started) + { + return; + } + _started = false; + _sw.Stop(); + } + } +} diff --git a/VG Music Studio.backup/Util/Utils.cs b/VG Music Studio.backup/Util/Utils.cs new file mode 100644 index 00000000..a7315069 --- /dev/null +++ b/VG Music Studio.backup/Util/Utils.cs @@ -0,0 +1,153 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Properties; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using YamlDotNet.RepresentationModel; + +namespace Kermalis.VGMusicStudio.Util +{ + internal static class Utils + { + public const string ProgramName = "VG Music Studio"; + + private static readonly Random _rng = new Random(); + private static readonly string[] _notes = Strings.Notes.Split(';'); + private static readonly char[] _spaceArray = new char[1] { ' ' }; + + public static bool TryParseValue(string value, long minValue, long maxValue, out long outValue) + { + try + { + outValue = ParseValue(string.Empty, value, minValue, maxValue); + return true; + } + catch + { + outValue = default; + return false; + } + } + /// + public static long ParseValue(string valueName, string value, long minValue, long maxValue) + { + string GetMessage() + { + return string.Format(Strings.ErrorValueParseRanged, valueName, minValue, maxValue); + } + + var provider = new CultureInfo("en-US"); + if (value.StartsWith("0x") && long.TryParse(value.Substring(2), NumberStyles.HexNumber, provider, out long hexp)) + { + if (hexp < minValue || hexp > maxValue) + { + throw new InvalidValueException(hexp, GetMessage()); + } + return hexp; + } + else if (long.TryParse(value, NumberStyles.Integer, provider, out long dec)) + { + if (dec < minValue || dec > maxValue) + { + throw new InvalidValueException(dec, GetMessage()); + } + return dec; + } + else if (long.TryParse(value, NumberStyles.HexNumber, provider, out long hex)) + { + if (hex < minValue || hex > maxValue) + { + throw new InvalidValueException(hex, GetMessage()); + } + return hex; + } + throw new InvalidValueException(value, string.Format(Strings.ErrorValueParse, valueName)); + } + /// + public static bool ParseBoolean(string valueName, string value) + { + if (!bool.TryParse(value, out bool result)) + { + throw new InvalidValueException(value, string.Format(Strings.ErrorBoolParse, valueName)); + } + return result; + } + /// + public static TEnum ParseEnum(string valueName, string value) where TEnum : struct + { + if (!Enum.TryParse(value, out TEnum result)) + { + throw new InvalidValueException(value, string.Format(Strings.ErrorConfigKeyInvalid, valueName)); + } + return result; + } + /// + public static TValue GetValue(this IDictionary dictionary, TKey key) + { + try + { + return dictionary[key]; + } + catch (KeyNotFoundException ex) + { + throw new BetterKeyNotFoundException(key, ex.InnerException); + } + } + /// + /// + public static long GetValidValue(this YamlMappingNode yamlNode, string key, long minRange, long maxRange) + { + return ParseValue(key, yamlNode.Children.GetValue(key).ToString(), minRange, maxRange); + } + /// + /// + public static bool GetValidBoolean(this YamlMappingNode yamlNode, string key) + { + return ParseBoolean(key, yamlNode.Children.GetValue(key).ToString()); + } + /// + /// + public static TEnum GetValidEnum(this YamlMappingNode yamlNode, string key) where TEnum : struct + { + return ParseEnum(key, yamlNode.Children.GetValue(key).ToString()); + } + public static string[] SplitSpace(this string str, StringSplitOptions options) + { + return str.Split(_spaceArray, options); + } + + public static string Print(this IEnumerable source, bool parenthesis = true) + { + string str = parenthesis ? "( " : ""; + str += string.Join(", ", source); + str += parenthesis ? " )" : ""; + return str; + } + /// Fisher-Yates Shuffle + public static void Shuffle(this IList source) + { + for (int a = 0; a < source.Count - 1; a++) + { + int b = _rng.Next(a, source.Count); + T value = source[a]; + source[a] = source[b]; + source[b] = value; + } + } + + public static string GetPianoKeyName(int key) + { + return _notes[key]; + } + public static string GetNoteName(int key) + { + return _notes[key % 12] + ((key / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); + } + + public static string CombineWithBaseDirectory(string path) + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path); + } + } +} diff --git a/VG Music Studio.backup/VG Music Studio.csproj b/VG Music Studio.backup/VG Music Studio.csproj new file mode 100644 index 00000000..b512cda3 --- /dev/null +++ b/VG Music Studio.backup/VG Music Studio.csproj @@ -0,0 +1,433 @@ + + + + + Debug + AnyCPU + {97C8ACF8-66A3-4321-91D6-3E94EACA577F} + WinExe + Kermalis.VGMusicStudio + VG Music Studio + v4.8 + 512 + true + + false + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + true + + + AnyCPU + true + full + false + ..\Build\ + DEBUG;TRACE + prompt + 4 + Off + false + + + AnyCPU + pdbonly + true + ..\Build\ + TRACE + prompt + 4 + false + On + + + Properties\Icon.ico + + + Kermalis.VGMusicStudio.Program + + + + Dependencies\DLS2.dll + + + + + False + Dependencies\Sanford.Multimedia.Midi.dll + + + False + Dependencies\SoundFont2.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Strings.resx + + + + + Component + + + + + + Component + + + + + + + + + + + + Always + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + True + + + Always + + + Always + + + Always + + + Always + + + + + ResXFileCodeGenerator + Strings.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + + + False + Microsoft .NET Framework 4.7.1 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + + + + 1.1.2 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.2 + + + 1.1.0.2 + + + 1.1.0 + + + 2.0.1 + + + 2.0.0 + + + 2.0.0 + + + 2.0.1 + + + 2.0.0 + + + 2.0.1 + + + 2.0.1 + + + 2.9.1 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 4.5.0 + + + 5.0.0 + + + 5.0.0 + + + 4.8.1 + + + 5.0.1 + + + 5.0.1 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 4.3.0 + + + 5.0.0 + + + 4.3.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 4.7.0 + + + 4.7.0 + + + 4.7.0 + + + 4.3.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 4.3.1 + + + 5.0.0 + + + 4.3.0 + + + 5.0.1 + + + 4.3.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 4.8.0 + + + 4.8.0 + + + 4.8.0 + + + 4.8.0 + + + 4.8.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 5.0.0 + + + 11.2.1 + + + + \ No newline at end of file diff --git a/VG Music Studio.backup/app.config b/VG Music Studio.backup/app.config new file mode 100644 index 00000000..42aa5898 --- /dev/null +++ b/VG Music Studio.backup/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/VG Music Studio.backup/midi2agb.exe b/VG Music Studio.backup/midi2agb.exe new file mode 100644 index 0000000000000000000000000000000000000000..e5dc76adb27526e8e384996ed5b3b9c5944fee16 GIT binary patch literal 3404655 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%0AVT9Li3%49NmPV}S@bT*WYPp=m0VN? z&s?b)M3aD!;)wKC3k~E4!S?O~$P}JGc61~J0!cVW_}J0qn9bD!QzU&@p>m%ID8)~2 z(8QMFQY!4pm-1M5Q%ZUh78b9-?4VZIvNGVGCx@I;s!1DYzc zwXQC3AGTH-{!%5R0W(@3n@E9LNEw7XuT&g0xH#5304+i_sM1{LuXEYI6c?_xt?ndj ztGiqH#&PF2#J)Bnahq*P0d%LtoWgYovm|aS{L#Iw@H8~8mK8>&eOq`mFvZl#X&sSb zb@z}>#}q`(ffSQMefPIg!m+x@i-h`xnZFkIK<+mP^(P?D_3<)smo9zfh7gp~U2u67 zsV^k>THppT>=mTmH4i!CE&liu)UPw$2x?!_$2=YnRCvk+v)= zR0eZ;lr337eovN$k%37MTinuZ%OcC{9g-7tBa}zt^jKWf>8XBkiF*C8(@6IGcc$w- z-{0HO?}@(>4WUN%Ltg|uhgV28Yd{uyBM0$3axUy9{XES*(p zMa`FtrZC9u-uy~W_m!;eCN`J_E)lt;n&x;Upr9iHkiHD0gg_$JsidQ)UG2W}Xicj9 z{NUPmqS{Apk(9AKY*ecSFyTz~CRwWuoh@cL!faVT02ft(YP7DYP6ZKMn=s05JhIc9 z*D?5prd?x{+HBe_ua(`jzP7*;P+Nzdp(Nq7pftV;H;*~ch)E)o20`CR%oPJ$=0pJ6 zBt-UY)g#DC4r!uS(oO*%A%KYZaxix$W7yO3HtR&3M?wjGzhwuKnr>xm(3ZVH_Lm?+mNHbUuhIwAsH)Oo2dIlsAWnZ6WARY;SUFK0ym&2E` zl^PdRxvC$Nw4wP0`K}4bw>{G7A>M47XP7p|OaCMpx=>GH-CqjdwvPy(5o$RafbYjw z2_J=cvp)*Jw^-tvV&LPLO9hs1w8UqGT0&@@0R}}>M0;D}qY!WQi~xL(0$<{R0E+p^ zlvFVCZc|`ONnSGm-Ge{}+ZqmQLmeip?mVwc;@xb^>TwZnj0{)5LVvjn4l3g9dTOn* zEC{Vzy^q~4Pk_(g7dBT{;`j4f!BUzz?*Tro@nfXtYbV$ITCCOT{9AEDxpe`UtQvHo z+vFAoxJYo3%|c!>=-YXTboXJD6|#h%Rdad4jvKwH=N_kxQQnm6IaFjKEEV}9BE@E# z>wc8S_vQt7B9Pjb0=F1}2^9De0=4LkkaW^Sa&m*5C0R&GMpoL7Q(!}S{#i69M!s&5 zo?g+O5tZEQ$#m`ZUZ?N5j6~fkS|0EZ{=R)7KnvfG zs`z^A^t4V5=Aw@GITDe5<6!vLdZ5ohm)TU@gLH1I+_geY{5tY7cCY*d)j?~Jk3|DC zEc3mD`f0b_B-x84$<|P^3wT|nFb`#SQnuM1lrdM|L*d??R0Q(^=tJ|Ww|1bEZgL=R zM4v;^2O>I^#Q{MAzsdN~UI$jdBwMrzI=qnJ7j$?Z!NYX;1i}4uIIbSxM|HR_!QFLu zA;Il+xPahhIviOaa6KJvP4KNcJb>UR9iBn3qQlz=zH~sAt%%^$I-EfDIIP1&J?)A0 zm;r;+d~?pr$Y~VerO8AWc;=pyo)74=;F+^&7W|OSl>uEMSG0vB{agQ zI`*ovW(ca8Db@_YkNaBI0X*v`En!wiQ)*D6-z;NzB6!brXiLJE=Gy^Y1fM;m_r%CI413HbgaxeSE{4-w%RK>sW3J^k z0y#V`^7&J1xgQtkYq^xbn|+2Pm$6pq%)_j$SQ(YzF3J_7hymUmjc?zJu+WgSqZB0V zWEx1j_CG?7bi|h-?Hpw4sX0-K&t3#q(BU`qdbOBo)GI%@UZj(o^}3EM%zE_%r93Y3 zp?b;jiWux9HhxK>T4JZ|=*YCjdue_`(Y)Cf8V>RumG()qEXlehxmZ+^bJl%7>dQb$ z`@|Sb&i8FMSAgR7)u06<-we!%sG5X|swt77nvb1DHJ`v27FS4IuA{28njowB4#5p| z__$up`?LSJ`6{8DW;KUSqH2=QpK87zztfwK5_q$#>lK!}0=I0X=6gkNzD(JCBdPh; z?bYi=DX3mD4eIp@b{+Y~ROl1fEPc!H zWmG7cDl|1qR;W3_<8|1fS7_mMS)t4bKaaiKer!A11IHYX8ynv;yXZYrE}dhy7j>yd z@518Rt_#I`$Whf?2Y(cWLgyGo`P{1Kvxwj*9bQkc0@%PutN#YRgYyYrU&8ko25HEQ zT)7jE`xM~RP3Y9|NBF1S0hY{Sp!9jbgXV;9gB}Y{NvM=5wnK=8YMBvvhl0w1J6 zh-}K?pg%Amhq)9hp19nQWXur5SribB0IY`K=xyHS|!oJRHeMf1w z)t{3|jDouxxxn(MbwHH(Yc|(MNGiu8-xpvs#Io*(T&CE@TzU=W#@hZ>G+&kmDhg`h zH-ZG(s?IuZW~=f&4&1s`i;`FJC%0;AN+t3&kx|LgMp38eQ86jp2!j&{QAszR0SdO5 zk)cl#evpAsR_f+PGhZ(F4>?%(1y^@5Rkv`ZoZDFog2Tp8*w;qb-r%sF6t)6kw$*|5 zzSVNA&zAKzhT7I%GG1b`A#ElyptYCGl(wuVF_a=9Im}pVBzF@XgdUq4S6QnPxOZBBNOYBsV zVr^f60*!qJL>z;cwb95@uEUfVl}&R?scJl+7tUWK6wfdq`?g?mXY@N)Ka@Ox)dOuX zk(5_G%m&M5<+CWByjPm)=?ku>tjmmLI434gu6n;fOEWreA;?fM2cJAa8}sQ8)g4>8 z17tCIGi~)4Iga#&SDqLHuPhmRtPwj`keg|*>d`vLXe8tZ$Y@f!18J@xH`4~|(d;r> z2Y7Xp(YjEej7Dyzbwso^3GhH+M%+p{(<%@B4O~FGtMfz)!<{Eu8;};M<33l7;@kb< zv3@AdjQRbG5YVNMSJ3$i^9V>s z=#h&4wrR9v`lSS~0xX#Y^fc2G0DSM8ZUW$DX88z!JKA&;0QXTdJ^^r_Gu;Hhoo~7c zfP16qCg8^Mwp&9CP0zkr9pTPx#pi6BI$8_EaHE#DSF!tXyg>F!IGOaG^mTmaTJe*c zEo_=`s{YTUd8)>BBTdP@B-__w7-6lyk_w%b8E5zJn~ot2{QjhVL4}b==^`Z;5Dte?9P(k=+5%-vCrzx&f&9hHvHb~ z&PL#q{}yBu{wS{g%cEL!f0@nTn4iz%;$?*JsD=-XVXu?V+gywxpUC=f+*k7%_7Z;K zj=1xOirXu(LzQ&re%@c4h-FK}T=hyUJ8KoI>az1;0`R-xqOdMI8@`Og@nxi#U5_1! z64?IM@Y|Kd^7^>6_A(`&a*H@spS@j` zBJHdy=ESiX)gC1OTg5-)*wR}G(dyg8(KzWJC#EGw4ai?OKJ(z-sX4Qg9)p6UrH4rfy$;JrEGEel|x zA*;)8i-T9nMrFRrKT^dqRo$tIZ&Y?bg*Wt;mkb(5ELKFW!ZsD|<|$}50>P{p;o#A3^Q4#x!{ z_I444Aw%q=S^>XqA8 zg~Bhtp12+_obe*-E-@#b;Q8?a!I$Gvx_25-0X}O$9$Onw;&TnhS`nw9g_wN1IN#!p za5(1PDNc40yYHmLXYUl-JBbAy2wvYo%A+Puo=VVd(}O=zlY~ zrFfRTtj7=QDn>9TaTE#>)>gd0yr!d%ctz(mghY6_c%QQzBdk`scvWGO$@LKzuPN*s zF0OHQo(r$SCMi0$sK@d9lE8XqHb?KGN;VSrur4>k<*##r2MT6TC)}cl9g2)xQ?y~X z>yfK`z{N?9D{L(z?O)?-aoUl_sIGDTdg#kQ&6VnRCIDz4RKi>+q? ze;p~lsmD%5o5))%_ zzy)EsmBsPeY;qO4!+F*%;&5%2bBowlo1M4?ffsHOS_}!0&tlk?s^YMnZLS9IZ>otC zG3abO@;$$rQDwZgGj6~6#x+1?WTUnRNalWqjwJ!Uho>)_l z?W|Wzwz$(Wg^O`b7(~RcjoD^?&yvP$gL3zVMD~sP*z!hfheddsF{coKmk9w)*(c#Q zLWC!QeH}vnzlMk{&Dp9@f{%s@Z*w-wN^p)PUU+a1vj z@UBRr+vIZOpIc6R7RP>*@Y*QxeH{Baiu^yZiR<;*sp!|k;aGZ`I8m2rw^6{t>f%yU zcCb3ZvucR1nzH;F1W&0c7B*#COc%h1YKxznvtMeHzq5`&#lEaV@bNkVRh?Y7E8rt_ z3D2ALK(9~g5qbzSlelkGI*5tU?2^UD zr$)1!P;nrNoeOQ59nDT!-z2XC5u(t>iX+7GXy%Q)>*MmwS+4G@<=J;p6m=nS>N=Z% z_hFlW_s`K{et9;zJi$Md7i%ieP!0I43SxZ)wxR;T$h=aV*udFi;xcEW8hpn|Sjo=$ z|GIzW+iUOPEF%sQ?Mse{6FXH>F<7mhAUXL$++NOhmH1;(@?mkFQsr~DgZ$+-Vq|ci&0O65PZ}sa%0ph){`M{T#Xhp?dtUMl)zm3z(y42gEH^q*6{< z!gsh>TOyIlzskjUg`MW&O@(C#V3H-A$_36&tT#*O=HeI54h1QhvNlx0?OdGTY@S&{ z;B_mckj^h57-$~PZ>=e2bCopb_j&jx4v8dS58p$1m?DJH%|9TC`Kizhf%k}KST8Y= zv++i8s-%m>oI$-g$k|LzBaGy5q>H9GX?xL!-7hlP2*B{G@~n7=GZz>C z1C;WS)e7myG6BNhk>Q7B_(2MP^i#k_xKXa(WV!xH9Q5>C3W*e%`u9Uo+Y7R`?<%P6 zV<`IsBOOA35rzJkLi)`_35<}6I@OkOcK>5}9J->Ok>R}p-j8qRY>VLy>H`m`zY=BQ zPX^$*c}PXQ9nn&~9zVd@cV-S{dz-EQyT72tU)rY}#FFUthdGyy6I-Z!wtZyNG1_9+MD0P3Sp z&Ek&-=5W)H|4^TT4pl?c5742kP3!ez24`FAHhLO+m?R%!O8q>0;BE;=_xuv0{r;Y)K1i=+wo=%#B>XLNW5WTgJC}6nEFq#@UxuYcT}-Z znw4@@HqnA4Q!NzF(D?LnH5XkV)oz5FiO%dn!#@yC6rE58J$@kk7vYI+A-0m-sk@T1 zjU0#UNlusL*H++Cw@JJ%By^4$uPnc^(w5~{R@yTBV8>|7Xs;8>6$Cec736Ni|4+ct zjl_DYXN`|ITVs~LEWZ|I#4E$k+ER37J&gPV;TGa9^bWneei(XrKZP#MX2Ky@m++v01_rb-K2;^ z5$cbM*b||?s*3j_)Q?qhEJEF{iX#!~HC22aq0Y8^u6!Ax7KJDuL|~qytbO#HH9O3Ny~Mk&7aS0^CTEowjs<&gjK@V@%CgAJi#p%TKm5Kl z%sH+Zg3d-W*M76G4#RQ{H7lZ)hmTYIlcC>n%6ux;K#|+g_}@#+QP{Tz7Hdt>o+as~ z^?wzP?1Dn|#uDAAe=of?PCUn+t}BZdm7sphF`vRzaAY6pW34alW}Ql5rhZz?@8~EN zMd4;hv8f!*{yr{8YgxW>>@=wzX?Px4iN_;$x6-G zu~4xulD%Q2$gXg4B$922yiMYdNMB%CCRh%o)yrG13BY@?1~bWM&gfxc31^t<2F!tE zqz6R?)l1@u9xj$kO%)2?R>Lm>X^wIBi{Yf|(C5YsMhMKYfGJ=$bz=t42`Ea_I88qD z%nXnQGtZ*H3}k7{tbXse3>`C(*J~iIE9@?@QDu({EL48X#cGvfUIus$NuFh@II8-^ z3*oo^Mf%`hg{Mp%@_(QQ{?+(`bjd&+^H1oK)_^#F>y<{g_iK!%qA8io)IR-M;|(7F zU3z6Ie^S>gSCd{jpY+N(Qm@R0;>g9N5S2`Fq3WB8_$oyGL~55`DOph2)Xyyn66IOS z0WP#C8$;ExLV-rPEksNRWz&Ln$w{2QNEyMjwIqVif&nW+T@A5nRmVc2M!@7Q+~F!r zPDNqr5k+hYQ!gmua=7}MDt-)yX<0yH+@*>i!qs!?d_FH+%?fFbrQ8q&*uD>`4UhLj zm5CAR+E8Ufgt|Rc$%#)c~>q0+{W-I>4msHd}mO|^63d|XG z%EU{)IiVkkU&^x;|7UG|F^BD?T zVJKDMwO+yM+q6G&$x2bhN`El>yxp`DMCn3iET{cZ)L{_c)?U2M*)u9`$-ovV>1BOD z^kL852;VGvGIo{>HKGT=I{t`9kMnH`J3tj61lIeMhoQUP0GBRzX}8n|x>JvTTcSe0 zP2x@3h$AW+B#Km8(8Quuma>emQu#JjWLsFiD!x_u1=0&(Z$ccjm;OiP_**ZKZ4h0C zZRtio_8d=398POz^OgK^>?_MDbtWERw+c8WgbBcJhKci${DW}l!h92n(jUX0Mc|bP z3S1gV5!Xc$Tp-~yk>Xk;CT@uMS~&{*g1{Z+D2Col#s4?*#IjbiO?;xtrYfUmt851K z9qKHBiclaNpzs7LKR1K_a;4}`7`p<^PgIv1^pP7pWUT)t^}`&a{@9)XJ9P7^XfsT0 z7TU4Z?BnV`#jx=;vAoP*ixHb+_+r}Bv^GYC_ct-(Xbd|Y1O31h3}natAEhtm;g9yi za^*iwflhPnA(J`wx|z$VEh}Rl#THpESU;`E7Dpj`ca#e70)d~{-s6CeM?)rKKZ*_T zjtY|iqv7IZ!jCNO2zO<+%l0@nkVP-%fTvfu4@fFD0ywGC+`!21S}uh9DzW7h`3pn_ ze4#uduc+7vpjPqUiQKX>EgF0l{+qhIGP`O+p;tu{xF;ImtL0a7z`H9*~o){5+A4i*>82uNRIGS||#L*aOJBvrKh@s=W4YrO1 z@i2SF#91=NhCOZWg`_gGL~gIFwp9s1FYJrVz@h>A;S01t1ai!!?EEXnOkXobnp)4Q zUao#1XU>~>HAcz+CecT^_<^&Zf)mMgX4aC|%Tzz)Di)EkQILy5&W=&Ss}d~cB1?h% z3e3$+SGR{ck*gU9Lgle+v4N89l_2yk7y}B+$Pe2*`-tKGW$f?_*z2)f1W05vm5_sI zki;8SL`Yll9akR{Ta2v*Kg$gTUWGic;*QM)i=fRbesbvLYsVW?+Bsa^!pUTf`hyDC zgoWg|(-C?;XV^~z@8HxXEkPGbe|yYIuI9_7aa0F;O(zm9rYN)*0G3tk8}PS+p6^|} zMFqr|&F$BOx>fl=#Siuqt`1>yLdpZiM1`@hqzA0vmqXakA>vv{2^KT|dw6S_x@4;7 z6%|YNZ>Uh>+5`5g;scevt_r{lX+&8rJ+L_%QTD0=)>D6p{xi6eXxHd?u8vdHIb6J_ zvK*y7VC;kXOl9w?0`N*I*kPTWJp% zw*2iX)eP_!!i8BFJkF{V<+3WS|EF=araHc;UQ$$ugE=aeHQNIQ)jv_$8`yZHs2@=6bf9e2rMg9*b2KX%je1+pYvu4eaE> zJ}U&iPj&fNdSJB=Y^eyWdK@EA)<>@4nfTh1{CZLSQ1TFV!X4)XOKGcOt>Alw?I5|W z9-`!h(qn>3Tjr)=!WF^Sfz+K z-?HjIkFPz+w9(WCSVseykNy?ykxuxq5$dnP_gD27$hX0P$4Sz#Q3b`6LLkD41scx2 zHJnlkNEz6XC(zM{Q7{jx(sYDXE`ZqbxJ6-~DtMxCw*(8Y_^hx~{%()N_|yA=fg?l3 z#$BmcuA(i6Sh}J8(C2Ac9vTEds1VR5pn;U3$0hN2vh>^q-AExQ6A4bN=Z~~>IOAb;3HBK_#w?hN0?xjtx}K6C)|FrbLd^m**3&}gsg6z1(CX%L z75fRX4{Q%9b=Y?U5Q-CYrg1@;>gn#nqB778P*;L+2=PTupxm38uMo$arc^&k@S-BF zKq}JkSy~2m zy6ni8>}~g}V^p$s!PtzQFe`|9NCfa34lR~c&@%IFKxzBdUCvc$Ov8*1I~aYGdb~m) z`pgt#`=dW&f1B0rC-vN=Tt!xB+f$Sd8ZJPWLK|vf=MKo#>?aLsVp(vQx{#}wSTO8M zqMmht(%!t2nQ7x`sj|5`nX8Y2PFRuu2WRN$m<>zt3r@6ZTS`FhB$Rg^R1$@>yni8z!Q>%eZ=ytGg&GPzI~ca}^}UkP7f~MQl{qmw}1R z@-~b?TIj5wa@Z!3cDH25A1ULTvZG9kIbs9ayETbFKs-)!k~os1|5pF<%ZAm&VZgEs zOOCQO7de;=eH6+9Krl8oN4^nQksIa9-#BQWH2a&TQtA3{>mL|@pTPzU%v|M=8}_Vt zkxUo*_=|N)h=&5h`M+!Yr7;x4E+jc*&7To7ez{n`x)bIqB>X3*_0ON=FwfX;5`sESGZVT7PET{2!0G{bK*kF?SgDI&*b1S20V%v6w3~l%RP4 zLT+Op)8ZiMt+y5WM$JKF8&Vf>6~~EjsO2Eh2bvW??4`WU^`i=<=mWjv|3BkxSOYo| z^Q?ZFsu=dk*jsv#9qa`%Q9=h>Mil!Ub^8735r@EZ^3;Jn* zA-=R>-f&0N28VQ@DWO~*Al~k75Xh}x? z0ngv-=8$Ee!;Nd?W%;pG{uG@SE8yq(KE1>I2>uCe}vq z-Qi+!1lMTwe0l^0u8I(wBiW7!ijI|Z*uEkw!+Tj|d3fxk-7tF76#t*fht|h)`D6<_ zs=RzsWiFDw)F}ej!~@~K*T;4G1gz5&y=;H&u@CUE+#mMG`>8NCH#XMCm)FAj`05(h z?QB+z__7vXU>B=v@iq2zoRY64_S9mBYX$VLe={BC;>nNVgv&Z(;$Wj4NYCux>v01N zXWjRoEPgu)l%; z=&Xn(F8rZN;jz83?;ob9GF-M6^~Z5GM&JuJT^XSaujqU0&ypuD@r|Wn8Lcco$}R9y zy+56|T*~Pi+UhEWVM8YQ7{o?6PD8?mgYU1gtx{oFA0QtiJhmcE7(5!Y!h+Qa)Mgov zdDVh-Wq?zZnyW1A6NBL!R!m~-$(UjbgDr|ImbHGh;G8+Yb;>MdkA+>Jh(#6wZxk`A z9zt%9#1S*t@?>Rh^hv0A7h{8BJ`F+f5qmR_Srvj;tpUzeVqOiw%m`qS60;(Nt;Z^# z#PaW?=~@woAJbxHhSIzZ_u%ka-cSaMIF@*o*b^aaz6x-gT5Coq%eK_TCW{aOUngx- zorKF|LfNaK0%54tUxz8GF0i-3+AJn0fQVu@_*5i2YfsD>*?>qG7O_SlBi_W@_9SYt zg1@*6XWEUJ(LLZbk5A(`w{gYri@Fn7GSs;W&Pl(ZXMBUt9Bd{&ENzypeS zzakb@af)GzMQp0b)?37x3T&^1_Efq?<1hj^UcMitP1>4QSyD<;28FY+;l*TakSh^%{_$wF!46 z_6`IDcC|#miV8_$(KJ{dqJ!<&;K3>bg-fOGhVQW|ePba=#zqL%i9W~e1q#DD5x%8m z8$|!JjM$>_M0uSVP95pEQ0bVR|mn(Ydq&aGNOCb8Ra+cG$q~}5OYOk9~0JfJZ3iLQ-pPf5`%-f zhX_2Q#9&k9GIfBg&cBgF^2@-Ou>uqVzTLdmcaR^1dW#bKn}y9H&ReI}x@^Inf^~79 zl|>-JS&}3_TLgTsSOlV>Mt>cqNV;UHt@;#u4!^)$+AR-+Kdi3ctbdJwqb(K5fb+Qe z1!rk>3OL!OWojF-*n9lh`3l3DWQo@Y{N>e#nHzaTbwtoe71p9}`Ry`NRCoBipkkNU zF~jG-o9T*KH6Hzgf~&$V;qo+vO9lda>i79n8O za9&H2A`mvi={JhboHY|uaH$9p8*%Aua2&u^B8@+S-{-_gtWf|!K`0!JN!x+qs`r3N zA;4YhF_s$OPN@Ebf3FwCPpH+$+|4Z0|EI(MFP5)TA|$xnH=rB(u=|1Nio1~FqHgEy z(RRiKIc57j&hN1|!p-?2Cxo_Zy&fWFD$A7FLUjstWr#XM2*95Uu~XpgUxL3Az<)q` z$P0|NhKMgi*dE&LP}**TGSm@`IKBF4mO-{>14aX5TPo*`Zn@R9ogP*Kv-|m-^*_RJ zvdW^)6Y6%0`jt>mSa7ac{YAoFq5gnfUP62=*gh)aVF{iR*o(m~k%wvAGw14wE#UdB z8ok$|x-GDKerh4`vPHZr*bGqvijyF4wGf{RwlxrgIp4pZx1sgoe&VbUa(#B8C3akh z`n^RxZBeHQF;B1$DcuqY<`XypSKNiL>|kn{_#WroX!9fVrRktK$(|V&)n`!`TGYKl z{nDcD1NkL9RfufC-XV+&CAg75w8~MzP7`QW7CdCux7qEu{)BB+D|kUR8e6WYHh>>k z1kOQjv0$O@hy+hqgwN3Z{8)4xxVxzF10Qm}oR_pH^)>PrA921i*dJ|MMXh`DJC=|R z4cu7mpkp&_T*YYXcDKeeTt+n|RDB=kszbycWx6^Q2i`-}{1EkGh?o}2-Xvx2olpWd z5SSk-z71tZ2rS(Xfv*PerHeBbEDh{avW0rUqP`*2Qx+94T57gn3y5s*OAs9$1D6or z33fD?m^vMRFE)=EQwB_4tdO?zAMz9WaoTKh=qcELMc~Ba2J6Gg*YZyVHx=T zh)#bL&K}{NseS|KNEg;O-J#;R@}W90R9zaP?h8@Jhl(Z8jza}}aIF|lp-T_kDuyvR zR9p^a<4B<^p_#I|OzLe#aLIHA|=yLqM@gfOAVT+pknPsE$)Z*erh!mvX#> zZuLmkj1N)Y!)A|NLR~78TsAcP8%m6W!Ga%Mf-5+DD-)hViqE94kEfz3Lq>Lc*F1hI_~_wsI4!F5L^h|6;^ zRlZ1k>E}AL9Xj%6jc^zmSSn0cRjda=MyM*lCDOe2C1F8&So%(@;wKf19f*a>^xyk8 z_Rg_Qd!x#X;pKp0)`79;Io)M(OYVdw2zN=tH|LAu;*EEj+5wgs=0C`A;+{Q=@WJ@i zwFX9LZiox7zyJOZP{7{56HC5TZ^Qos+5Mfx-xT$_&D*Qu!;DXe`i38i}+l}Cj|ac_*B8CCO-A>X@bw~__W8TD?SwV06u^8N45Zp zWB&e8{J%2Xv@iU-I0MVv`%m<%e>b217t#$$>o=%>|2`Rg28?RfII+oqAwyW7KIy50 z(lQ*W>3s&KrwmQ)lQwMNaQF^LEz4gbc&2gU?RwhoBm49)Qg`@%vQhmy(f& z>>cT8!v@`PN1yxq^dCM9uFU=p`>@O*L+rzbJM1GMNHA@-PuSx$Acxq^1 z+K|*fsiPfz1`K!f88JM=UaOrQ`2kLG4DOSWIwCba#W6gcB@anW$w;+l;*(+@nK5hx zGI9*GI|ipX?8s{1@bsY>cE@mg=cJ@Y_ViRoX8N$y0rtj;EVy7PBSxeS8^DGPx3_I) zADo6GFsVa_k3^a6_utdy9((G@)L{<$aNvD@MCu*(x*0I0GuEJ92Rz%;LH-gP__BCUeMs`Qq&{TUy+Mr=6sAsCA zEl4|v0_~&H9E0usQqocMl#C{h^pyV3W;Dr2dm%M0-rlaAo!-&xpP8PHdiFsqds~Xz z$C1)+NU9MFNA`O3c(BJE_6LSLQtc0R?J7|Y&ve++hNU^uQid1-4@lY=aXSys95P^7 zeTRKu+OPp7DbXweu~#?jz$!NC!05O1Cp5sna{ydAUZA@b;x)u4#)$7SexEmf&A3wo z+>?;^lW70pBOZ37q&p;Z7kWf+H_6ECht5lFUpM0pdmq^`<3p$3@mSL~^LT0?Z zJ^Ef+M&tGdX@f&qhGTdd8#;V|qtBor!~3NSX~!Iy!&2L^Ap?hJ=->VTcnv_f{PBCD zdk=3%2r~MofI&GbFR6rwkiJ{GE}CDzWFfc3{7C?;w8(4*Z-9 z@NaT&a9CixK)8w#@1I86)d4sH<2`V93BKC_gUS1(j=bZJ;RX|<`=zF*gPHO7{q#=k zZR0ZyzyIoIqmhPwzrts5O2%Lld#3|k*~$I-=O}*v)z37fr!@527auf3A4hts{2h@p zO#Y5c>&HeWG)ZjStnr}!EeEttNNU-5Kw7%vdB1#XWFMKDo{=_um=q(#ERSTQ41!cK z1)%-O;UgRf&v?q-Hf`95Oh*%hw6{Om7Q9BTjP_53hqDLuIClU+;|zP-jOT~8x68jV zcEJ6HL$pG++lkXMhJ*hHr=~!_+v5hL4ot}$;PoHZU999O5keL!lz%s~)%G_IsjBT3pdCXNOjQiy(I>^fK4;h8x^Mv}t@ z6{{IVXN@U-Mt>tgS@C5dkRZX{*p87m0p)2-BI5V*$QY4|kxjN#t8f-rdMZ;K3TJO% zZ`Q)z)ZVP+?eX^E8U53S=*7#hgAwihQd1m8J_CmQPT2;e4oUI1iX#of2vTL>KlR{@ z3>s@jq>x^L{)3RA_+MrS^|?DrqT1^Yu+w0IK~_pFBppP(t1O%&SmO~l!-)WY_GREq zXN}Wul2nHLUfN;k75^W5=K>#BRqg+sWPs9?FhGHdRS(c&N*_$yw3J$4CX>mdP1=D5 zC{Re!By9p|G9;5Wg(3qjP^4ZX6sQ6cwP?|Li3(D*YJdXuS~W@$RFtSy!HXLIhEm$l z`G40rXV09O%;ZJA_y76)Iqffd?X}lh`+4^3>~qGdys+)0wq({Xci3RItR|qF!XS3c zrjTH3WpX*0O~Y;>sVzyHllGHfXFz$2NKJz!TGNo)l4(Z*2hAKd;_9)EW~{1R9pa8Q zJGFTKquKT?<`6G$YBE+=HLf#dTMSxwb^Xfv+M2Oyag)=EBVb00O6Xzae!HwurZF-L zWW;Hd(xV$SZCX>gdYSM{HD<~@s4Coi{S28V=~<5+tnBez4^;a8no-|a-OyT7Yn)XU z3h!szV?BV#GT=+z`_s@1zO_vJQCA$EQ3 zx@m?db=0N_dXbn~3v&b+f@U-~)l`M693gh+B~#p(v^j}1j9C~@HFsRSQbwqH#zM!W zQk9EO=0rwr8KKktb&WG7*Gy4kDGIm@TrvAA)-j_rj75PfmlhjRMipdcD(O|~%ql8l zMtuuwmX)ke8msB!)~`1#c1=^QBS3kL$)4H7XBg9md5u+8V(bJp&^j(+Dx93KD=wHj zFtt}q_RMOH87N({Bq(EXvTD(i@+D(6)0nZgskz2%43t2H=nS+QytHJAA*JAK4R#5Y z2g-s=%&_wqO0?M-Da&v2R16G|(K^~q6^jnLd`XEvkR~N@nNrBqsns&tt*%&OCQAEt zy0t&(TMRkP7)mZbu()8ja+eN~w$j2SONUB7G6khW*eP2=l7^7c`AoRC?7Guo*eN&d zIPbqCnexjP2Zob>ZPU|6Q(dN0m%dFb^Ou?BnkZkpW=3sOcm{VQi$dmoN}EltzQB_5 zfo)O?69_5yHr@T28R7a`heeyQrj~h!8AbXuYI~DgFt&#KJ;hz3Wy(te=6p;0lM_sM zEKNhp#N#8mY+KaMu)7Q6oO9S9h_l#1xPZgTO7a<#n{FU&t*5Vc@R{OYXv~gD#tT`? z#LHw=y>SuibcqcTwOJBJx^!!PyqxU9A4F#{&1A4X!wko;Rg3B4cFM&|ZDjIxBsv?q z>Vm#<8W~q}Z40Xy*%hj8S{=eCD=K@xf}Nb)DN3s*b-HxC16yg39%Uf6uw-$7A&+eU znUyYO6{TWb`Ha|@$PkiZESo%imJCH+V{z+h!yF1}_$kUhCU;rm^^HwySx;0j7Z_Mp zBG-bspuH?YjzK-W+@O-&5>4K?h?jaBU9$+W;=2hm!A&u8d2jU}^KQq+cJ;lQGy z%4lk2Rc6j5n4U>{JhX11`WJW;ywx^xyl(4>>$-8Fy7jXQRrkFM)$iIDs>#sDf_(7P zPc2mcb@xJbHz)-sfa1*y)$!m?P(gV2hK1_TZ3~qT_ZPP=RE?ipsOCo&s<}31;Eoh0 z+dASFfK}npO!+fQ{_xG+C*~|tPlF!-e`DD!xc)|o zN@9|TBp!)IVv$HB4v9iykO;)SWJ_`-nUXw7mH>1SZV|2z*N2;r>u;Qm{A}a_ZUDDX z09}MzgzLlg;pXG|8}pFQLmuD;a0>;{MYu({K3pGeKCZuU4)Sx52e<*;LIHFUZV|2z z*N2;r>u;Qk{9NP#ZUDDX09}MzgzLlg;pXG|8_!4neB=Rc0Jl&8U4&bN>%;Zo=HvPs z=OI52d4LBspKm!PYFlYnoK?H0Bo4{t!4z_@;;33cfwt?+n z2j~P(fSq6$=mNXJ998MK2fU@LeC zbbxJOJJImDcon<`dcc05GBUGV#~gdynB&K0k2@jfME6N2 zpECZ`(@vkjHF4rcKPsc-B(B`tbI+Z|#ipDmFK_PLd0Y#)yx#o$07SRe%u90tU2XOC^6a3|NPIRRv%A3_9ZWX!;je0xRPktwtqOZsbCSFc{j^p@vBEk?Cv zUf|R*Ke6Tpm*%fOVrmR)OOY9;x zo7tdwFPA5Rv9fnWGDw~is>`90Di8Lo)lpZ>*<`UfNWCmw*Ai$uFTloTsP_HTZ+u(t zQn>}0o;OJoG0hw0wau&R8`<;L2}xbgNj65Ln@^3oyVR8@=k&ONj&Usetd(FAUzUDKZU!@k)m5wNt4*)OWfWVF9johF z8?Tow5LMF9P`irOZVH=SxP7Lg)#_hjEMt~DQ#Xg;_z;$=4@wyF5=X?u0WigVX`Yyf zji1DKV{LO2U4<2oq>LRG^XSCtCQejvtc2$WjS$UjZpSli?RA%#S+{k|EnV|@fsA?8 zO&qS^)QcrrTHClPT*rwGv17;no~S+XhZj>=Of418S*?AB2gupQJ5;wLGe=<;aNLLgIS`R)E<=qPIlP zQ_;O2?l<*BfO$wRby*Kbz2)} z9pqe*b$*46H8hbi>q$*onbt;!6t7EK;{}=EV1>-=sH|2V)3g7!ZY=F#KzP+kSTu;{ zrnNHa&@L#naFxt6=;%MLKF)4PvQ6FLBV5Jvmo>FUZCiD%`5<3*pVo3{DR;_z!yrDh zd@0Y0Y97REe@r?a9B1)6w@0l!5@+T1;V90;$e|Moduc_ZJpY#YAL$F%u4-ytN4eRA z+_Ed1S~)z!)%oLM6$Bli9YjC~#B7z&7_GA^wN|TKNz)9gDpj>YHLI$rs(A%}SE#jW zEeyOuPv!rLqnO`wC8<<$b8D?yS=G=|tHMOys%l}bBrOf9#iW@-!_jFsQ)(7RH63J_ zvXGWX5m`qsq#UfHZKeJSfBBWcfPz?jSy|xH62Fk;C1obdzofL(S8kGJOBM&peV1M- z!DWG^OMOLwrJTXM z^V?ch)rK3b9>rX_NZSq9aZUsY(YI{^u4=2GBd7byouYea zE1PL`c}B`6Eqi?2T}=0B``kMAm@`W)X#%JF81GASTZ!I=u8C4N( zlCy@j%`#!MmR9C^%IZQ$VdUabp7%;x-&S!dVB{-5E8Y4GWpz_%U6NTT-6{?uB?@wz zTJ184&&f#|c#>=x+sW4bCj&xapgkHSGHvlBQBH)i&`V}ohbxoyWwMbZDM?OuJ~UC? z=}R(VAJRLorgqJFoa||=XO-Q$Ym3XKbPRMRuWT1_98CfAZ?{CfA%dd7YYEJb!YT^>_Z{rKH?a3-1|t;avgc{(fRu7k;rD{6QjL+ZVU9E1)`_ zLFUU+lWKB-(Js(Q$7;oeZ)-4XBeGY1DjiJ%l;^osu@<*Y^cS$!pu}Ihb{?)^q%5el^Fno2BVmRig5n zo33X=F4Ww_Ij>q(XjYU_)gTLwb@bqz8)xsSMg=W_SV#-!>RRL|kemdf(`l-%X8^9@ ze#POykNGUk8s_wvru)yq_86VL%KZ8Eyhmp#G0?iz<>4Pax*SkX-pw0yGf!Q1N`aaT z-affN?E{t2t>X$*0GtZ?vI|rM*Sgo!wJ4?9H~UrO(|*-Te@GIkUe)TPI$>-y;!gtCFZ9M2# z?T$1$!+QvG`W1PTe5pTS`_H5g7@!7!_ddV6?U(pL@5_Fb2NEkt>e-x@jk=B!?H!G= z``fmm)*eZ|k#J9W&Z!3XsyFV75rZuS32J+7*4 zy~sx@68TOUH(@7V>sMJIat-|} z(BZ<@w00|(`;`Z1ztdGo{M#jM#BV&~SHF1HuR_2J+VOij{pt_jBu)?n9r#<>4>#W) zl4nVbq@4D(-xlL+%V|Z*d-~w09Ym(n5%#uCzjP}0K2)SC&lnuH4d%^;SICs$Xqx&av|;Cwth4wYE>J4G!i&l!Cd2XbG|9p*Z%_7*^Gd;pPU#hG%Uv z`wVOOTh$P%t5P-ftLnol)Y@1bR&AE2s(}YuYE^Sps7|eDsA{}k=2e50XX2eQ*gS}R z8_K<#JPn4o2a)kQbZ@umYLTbBdOtikP8}q?(h>HkO>e;FNXwqtDEpr+vYS=L5viuN z6>$Ci85xY+5qGMcD zl2+{>iL3J}I%3<2RPk%u6-A{>@_nThDz~b!sd3%vrdFQ8@G_6tM@`|&mb-Yyn1pWp z!vBGdYmw3)Q42(r49ajI+3cKy)xQv45wfV_^5QiR$w5a*K#mzqaejFmD=m9)p$Yni0VJEn;tu~)hbmWLcttKuo**~-$YtFNhHV~}TzRV@~W z2iuJ;^4Ju^)eZ6xK9;#OK^03mlA|ho)z!?CD+=YglbU8I{n~kqFciZVN+`}PkLiLwvnr5%$T9%{GPgrXECeKR`!c@ zvC3>}ZZUYjgH?`{z8zXq%U%c{!$=IgsiA81ikhliHRWt|6Gw&^f6-!KpmeUb@~&F0 zQfD6Hq}zWVOtYqs#ge&{+d{;*e_c8Ie?pyOSy>O109 zVbJ%USKSQ8fV=vcLxC;GzN~yPya1gJF2V#lJj|D5zm0$GLB@=Cn5P}^s=xMGZV9g7 z)qi=_HQT7QjH#$4f-}$zM0{ns(@oz$A85rM7I{$$Sw*K9# zJ_5wn_P=@6ZwQ-$yXY;i+VkJ^Go&Ygn}4QHodVu7eCjXY256eR>`Ku>4Hyo#2pm9^@ebCmT!v&~TTa^R&=nl5+zxr!yMbgJbpHd(Qw!t%I3`|Fd&5N8jFznxv?o>7}C9T{%xpMb_Db|9? zUWGNu>epP4LD?&*_~&F2_7AOKdt*_>)?sn!1@h#1nqgWIQr=eOY_Q3`$mYi)v$)1Z zmQ}u$!$$IIX9{fAphi)*R@@2tO}4SbG}_$Z+0!P>ys$X6QkRft%^jXWElrw|uP}^l z*DO+54p+-mL40#1-<)!|UHFg3zXJRh^hpl)HT>i7w}3aHvvCjN7UJqKraS z?o9lj#61C54rXn(!ydB3erku+5cZl)zlA#s|HtaAv{fxu=Uk|&mMZ>KsdE&c>Zsy` ziF_G{qdKbUMpa$S4b8o7owniSvFg=o8Z7lmRdcyI??P3>4vf5{RD1O`bo$k*W*wa} zA2u>i=hW1ya}_VnTM~q~s2S&~8LDQDI#1OuQP*CmYMa!xs`dtT;e~2tsakodTFI!= ztX9>ki!S6XA9ayxSgIB*;Bzmviobe@I_oS$HM9_*8p52bUtOZ6^YxcfbuNGF)pWJG zL9ONk3)59&m5fAP9hq;7g6(SHCZ(?WO9qJ zbJw&|?%x)+dU^h)i^U2sRLhMVtYK(mlSPI3FN8t$n3t=b_f9}2KEYV%6F`RTKN0$} z+IaXm)r-{0cDTrlht_`4KOO%WU<|Z`pPl4C2mcfxXSziGF4}e;ejWe$(DQ+ge*yHx zwqJNY=mMbSi=Y<*Y4Z|Z4t)iXb}lkC(5rxi3F@I6z;U)e480a;dz(qmjrc`Ycxxie-Y^TreN#FD$6fv z+(y_}fllvvgx_z6i_T+&?En(D_&cG$4Ya)-6^qn&@JnAHHlBrk-VweB`o}=q{|R)r zBm5=kUpoB1f_@EX`QJh7@|QS&kAFW9T4eqL{a0`dw4|{Y`d@aq$Q*#y_C)?&{QW>^ z!4aXai)&|`qkUp91(!+#R=cp$nWa~kxA9N`n8KjsKO6M7QR<(CUR73lQOfY$P2 z^E0%cnRd9y+#~H9zl4d6xzHCl!WTea477bebRlpfEBaSJUj=k}mqS+o2@~9b{T29i z`~Ma7@ANki)&!(}MScVFH{sXqe*?7Be+T}%fYgUvpMkyy91AUJNWSE_5tCyX(bbBk zrUvs*Ej_%&;ds8PX=-JS!frZ8-E5kdTNnWOkxFz z1RF6{$@8O(glgUMSo=CAr8F{5_3U-Dp<=--74D{U@%}li*#Ocf@vKsjMce zUbHKH?pSzI*V+}{Y4D^zwJW@d@T3pcuJAq%PsVxeLaI6Nq-|_eYUy1KAcqcntxsUXCpj}vMm#wQ^;k{t) zN&2)ab5AN&Hcz|4`#ze2|nDf{2lDRc^ zV@t!{CrJuUSZl;i1|>dF)2A;+ecmMTl#iUyQ@%14EK^H4hRd!|8L#x=l(*Klh_vq_XyHCNXuAI~8h zR2fzl@?}E)G^|s;)>W-6gqLzuy;feKQ%l&f5O*=}a)QA~wW? z=!>RkDHNDNu4aNfS=E?-*HP`0Rk51S-!k(r`RDTCQz~fwUCNp4M&zrMNSCo9i2KUT z^V%)3P=31(x{<$eUb(76inU}+ij$J6KIU7BWjbtlIu0vF9T)wE84Kg0X$_kVXO2r;;m^X-;T36NanKX_Qn46<) zkn)v6UNYh9l;(lEpmoV8lbJJ2iT3%v3{L%J^eebu^fI5hvf^Y~`)3|_98(bT-^ zJOo;1)P+|!Oqp+f7i84^TVi*4G|tlv|Dee>rIX~Fd&-W zuDRSA^)6|xWgpWz_h2@l$u-vN?($w*RZWe2X2+NulD26W`jBm?rM9(3UW1PvoZvKX zxT%^ikn(PY+3Y6Q%#b<5U^?b2`RpX{#TQf7v)re;Kraxw3aD!m5rG$Sgm+%+Q-1(w zRQS|9a5-oJzX2yzvKI>0gMHxGDxaDGJ_B}xx5dB0r)~ki0!ylW>VEKz8lO6hdwlJ` z2a@Kez+&P#xxuG?Zll9?+i@cx1bS9mG9nkWF{_a>g1@l|8E^^kfU`sJamAk4dLB9% z!gq!_X)W84LQjVogt`Bh(M|sUH&IFa|C?}A_fp5zwkvfg{z}``?dbzIes3M^vgo0# z=QR7&CvTv?0i9qQ(D8f@`af*{xyXGCj0eX9k$H!(KY^FPGvHGAUXUH84+I^+2YOqe zLH7pgccV|W1EDi;JCb1FcRRuhK93Fvfi1ve`x{p(Ie@uNHpMGDu=mBm(MfnRhSC{s zxY!-8tg>Z0aEH2N-7jllSue{tBzp(4?v{1uDd1Ev0h|FQf{%eS!P(#(kPFTQ)4_Q_ zo=M~Z#e0GL@dDf~-nx9zVd+S*8b!t;u;klnfbVHtSLTU+%Ii8V>Pb#PY-#% zgxmhn-MfB?n|IFJ*N^k7NYDA}p7Z%t$E&wrk+;UL_AFkt`kAl$)rFs}+`sJie(&Zd z+dlI6sRhA*{Nb1T&sbD&&&y@U{;W4rfL-(2b=Rv8eCrnvEtvh)?Ju8wpyYxJzuH&u z-ieRSdia6OKO6Jit0()LcGTn^yGM#W^<~F@dgC!qKDqPW!nfbO_|2>(^U7v@#Pe?P z+S{&L@WYl{e!JvUSz@KUJbTG9=>0Km2N<>=Van93AJd&-K5^$4Fg(-wF3762Y^rh1 zNNLl_v<}%?XK<~vxz^!Z>)5SzLf1OEYn{zCC*St@UHhD_)7${syMcY24Ax$_S-7RR z22z*e%AU$H+|%J-i<^nR2KNg5*Wt<@Z3tKPS=QjfR_k#ixcah}N!G$jITlL@lu!sI z>?z(xbNPdfP6w@d|4z5dj<3abwLe{)!8GX;ewD8_u(EJfQMJFOzNxisS!1YWsajmM zm=6@Kl)G5gSiu z$yYzlwcwd-xerdVT=nsI*bk6<)Rx_h8@A~Uwyd@>8n@k!r^a?K#61ms+xDO5h-0OL zPLAuWbK~jNVOe(Gv_F;mYdfA5$R0<&eoZ-i14MwtEj*nD?UoQGw%>B3rxRMjopEYf z_;OKI)_5fK=AoWun;H8UQ zhi-oOwCP(G%(|+GSL|N0`vAkIl-J9yB{zQU(zqc`pT$r;R}JBKa8V+H@D+76jWs1Ly&Jfm~7# zejxQtwJHNhoyw)_NL*`$fmL8NxB=V(?gDp%`@qBCtKi$<8So-_3H%ZK6J(HqQ^90#0k{OL z0=IzAfv3Pr;2rQ0GUEf+gZseq;9YP&8C(mV2FFoYmx0H?@l5_|!ONhG2QNPXR~)C* z-@pbM%$(zudK)~$?C3!X<}PloPg6J#GCBR;iM-xVVOQ|*=5;2iw=hZdQCJ^lGCBFf zY=uy0-=ol$oW=Z-f;%&pvjr5~e>0_din-0t=kj|^6yAruq$j}rwvaNvM5)KGQ0iCL z5(gvV;YQNiMw#4-z0L3+puD${_HQ!B?ZU>-NP7?V|ApOSGt>^)9>?*1|A&V|P--w9*Y zyRVN`Z$6i;ZvXB$weIrtI460)Hi;6vieBWok@pSd&S3jg~ zIPhWhW$#DT&%bM^f7P9>E?jbsx{_z0=f5&deSCvQoxMI!U9o4Ldc5&Mb<>)B_5HsT zsZ)MjrhY!9T>a~WtJRCQtx!+id!71YS&KUThwIfR4%|WA-lNJ6KCIsS%h%M?f8MEH zAG=4jG`y;sCjV8vd&zrht^4GR-`tv;@$`?p8K2I%IwSkWwHd1~xId%n=BG05yYV*} zS6-KyS^cSLnRkBVs?1Xw?#`UEyDRgl8xLiE=)dP>HMOnJ`u(S$&sz1!cvtJW*SY@t zJ1@BIeC6C@rcZs~m?y6pckGlYcOQG-<>Qb0?GLvfcS*2h%=eCcXUw|?zIFTwi#|E_ z-oRDa&-shT&HQ1>3D*QGbFTj8r%$ZtdddC$+wx8>%l+0VcjYWO_0ioYoPPU>|D16A z-w&VhqlagHq_yL|kDc_))U&QRWlC{YlV9q zbObu|fW_}}(CSMTUu<}x#qKiPZreuKL2q%;PdMm(4qAPgIg^gxK}Q^PmxET@9P$o2 z;-I^r$J}G3O+D(6chC_B-Q}RwSK_)}2OWXd>FjdQ>Z@_S7h1;~aqznwe6`&n@1P^l zTDQx=SC7T{Ug-WutbA2E=newqNN8FHWJ~;Rtu9(C=}Cm#5G-9*@UAF@=7mBfK(&eupDGH-&zWBfKGn zzR?+vzt)#Q+N$v~XK6U*&!rh-A7~bOu-=0F>_+~tQPYV4?NBEu;`W=q&U<&;n zNBA2_`qDoe-?IAp1-82kSK3_!S1!qK#MXPmE*}Z+7$UrDi1404!ezWr-)0@n{UWph zEq#a3UTEnDgpNSVJuP$>wA`OUt0(9KwLG-kH^T3LmV3aId6G3P`4ZX-Ep;Pw1X{{j z=q_j}GgJOMgxT~2o0f9$Kqr?|nE8d2n}nAR5-xV@9CkN4>~^Hk%d^|J*i}yrj8}(? z-AclBdb=F7u`|w(IOrZ|88^iCL1;<8K;}E*%3Lke?sNJHmrLfd`_ai_GGnIc>4AJn ziwnNkmi9ItTHHyvqLXLqNF1r!S?LhrHA952A0%ATwuNwAPTL*yE@(ZD?uA}Yor?Tx zHZ6I03tD*UyUceO`-C13-EPyl(3`j?g}=Z-2OV^cgKmS?@oa|H@pnLvf5@`a13l5E zjqkC4@;QrN2`zJ7Dbo&Uxukx29re?bf^YT#yDS@dxH`^CXsLTidk3`4Q^j@v;D%=(im7xM$-s z)1V*vf~8vwz1^m3pmkYC95NjanJ$OSYtXM!4pP?#q2+Skn?2+~{1QIlxxvG|DZ+P< zKdm1bBD`yeaJ742{Mx>hp_g!7CJ|^|CSA}{CXxsB{6HHzk78pS;X59-%5?(tUYni< z{gzF8p~uf?{&}-Xx)EqhL&`1#Z9go=eVJMEwg)`?b8P!S{OYFJ}*KAwho@X>F+=Q4$jMXYzLM>Jb?@ zck#?w`kN{hiz{Z%n(6!)DW^BBk7lRM#6W%PYiib~HAkU8%l<0cFbym|-~Ji5nI7{u zUd>;EA+>S-9Qo0<3TI$EY_9q9F$saxwj-R(@R?Ajazgm|wdSvcBniaz=kZ}0`SJ85 z0Y{69$hs&oUqKC z#YuiXh*E0?mQ)84P5n>nuYI5Kk6eGYeY3TsEP$c&f0Xr;GBD!!N3PHPZk}x%ZT^q0 z{`*b;$n~Y)tl(!;Mw|bmtN(t}KVtp7)qHDfUAh7oy8Rt({lpB6JpNJYhgwrMfMLgf z)b!tP`bVli$E<&64^Xw(U z%-=dn+)p5EuJzICIp*|$`Grgn%n3kpSmLZ8KF2{$HhtRKX3k^>L3$FiC6!d$g*ZsK znaFhEx)28mAE2M7XEH0~>rJ)hT*j%N94>c?Gn}0cbK1?9qvHnRSxrd|s-Ji(4y_*> zNkFi5Df_-u_wEgqr?Jrh?&U-mAWtPvGzSR2hdubHB`WJKotn@co9~KjUBf?Pk*2PeEXGl8H<+IfO^mb z!ZSVc%>?sXxx&X^Z9akEiv+cn->SPLzjLS(o9M;65E;U__XhHm=KCLlt2eVA0gB&SplZdA5j4~^hZ?yaee)p#iLDsOn=1bkKMEO9a>#)-j7U2?R(kc zuZ{Dq4LLmvv-_Zd(^so~=kbxIX1;W{HdSSk|2bCu#d`Y@6+kNekraUF$J<}5i;^}t zfN%GSbMzb{Hi@(P$^m>WugCqEJjRDVy-3sWZMLK{*}mjIrTtU|Bb7JE6|*n<2fD(= zeyY(k<+OVc;{>I$Z>B$${ZXbr_9Mb}10+tClCfH>uBxt+#h9dJAm19B=Gs36T)~(0 z%|~F8E96;Q#>P<=uePDIVbUTjy;R>&5_|FSJyzQ05<%;w47Y}LGd%;tW7C23;c@+W z6%`H(c7}8UNMD-p*t{ZDcq{>F^sP07RW3Zu(7jG;c;!5E8c|YU^+&10TFiP&8D`yo za_GWglPZ>3mTAvPz)e8DHKK^e-OhdEmrU|#& zh{(u=9iEQp^e0*>sFxmj@M}>0miMP$J+%6u={ELUTKMTIkHt1$V;GqZ(Fh{gE%~?80>Dk7`(08lERzZFQA=KQbI!stK)! zeW{uRzZ4wddVtX3F~76ck<-KPs0!GrKbiv8`lBsir~YUPcxm_oDPQxK3sSd#!ex2I zB6&m;yIB1@^d*7G+is~Mb?``^bGhdTu}H$r0dKT-NP_+-Dln!$$_h--A4LVCZ?<0< z6QxANug^w`fB7#t`}!Hq-GXvZ2R6bBfvsQ%=mPsd`>!}-P54f*7xaKWunn|>orG@# zdx58&cR0XA&;TM}87KgKg!h0kza|`bKmlj~TL|9{B0_^*(A~ll8tjKY2*!Pu^Qd4y zyl!w1xxB5*`wJp-Rlf3q-r0pFysPq6H)sdmx_s3I zu2;E`PL1^z~7QI~f4_b5`v|;Oph5wMnj|lxGi{1>~ zz0{(&3jbk?-X`=F7TpQmRc_I{g#Sg0-UD5El|}Cp{^b_k1MRueqThncncy=7K-DLl1;x~`&qEVAlxndrcC zg9!B_R}H)f{vc?#c_Dc2j~6M=IYr9*pR@(gZWNgE?SzF0>mV!wzw&X~$pwBhtdp?L zTz(&waMej1z&qK}>lT|+ut&HTzVS`cdZDG)OPJ@}A{8RM^4qkXZ($QxF83DFM_4yu z2D~oxy6rFzVeL~1BfRrT!k)k;u3SOFdIy0rWwS zpHGz)@EgNGu3kycd87xl@1%a7qCQA#H+1{cxcKFA-$$A}$RO8ASO|V6avo%Qfd^h^ zt7X%JY-lEOBEJip-^1p2u?5}!eO&x<1(EAU&MmTph2VE0=Ru|yc;E$hST;k*_Rb=m z!21mS!4EAP?UJt9!~?$ve((ntzf;nXhg}JKp7P&q@w+8X_-^2Rfp+q&#qSmSbFdFW zdq~HN7T)emDGHbma2D51o&F!n*zoo`5R|zZN-j({^1aU>n*ae$WZ81IX2j zPVWNJg`WBXc>%$PxDOuYzJqpyUg&P&0lD0~t5|s<L_j6*cEAI21<~~Nc_v^Jw4X}8PRE7z0t0lM zh9Af!-we=~jGZ#hh?_jl$U7k2eZ{H^$R+P&!gd&&1b zC29Mypu~YpCum2|T-hS}t*JAwJN#9{xIH+rTZP?RI2ur2X0b zP93trt>hOB_Z|#OdLKgm!Qt0$MAH8V=>fy7-_4SbFOv^2-1?RGm%Fx6Mj*L<<$Y$) zS4xxu$@MGmH+Ov%J&;_#^1gHMG4d)%s$Y5kxnl?Q2GZ59ycgZ`IPD6g8>i%b=}0H- z9}HE$@;F?XPFJ;~i^tV_< zt{r~_@H(q`$vd#Y|0q-j5IuqLI+5uBVoxr4=QZ?rq4EO#%@*NxBhv-s+J`^#dLjKC z*kbjc@=mDxkE|8`Kpc!o!x^jO-O;W;7pe|0+;KU{Ap$dWFjv?|cYUSU_6BzDz z5JJazn>Ykq>+yU0$s3SM-kI%uk8}e!^Gdn4!0SFzs5*gruf^+t=ZWGM*t{o1&dnGN z)E^jMf6tf%4A8ruu?l$pguZ~Q3;EuYkQcl`81NDn`7`4UaD!g_9l-l0eqj8Su@zUY zz36wG%zO^$w7x3o8jl}%?DX%4*M+}B_%^Q>Uhq_8fadkXi{P&mo}H&LydT|jJ@Xpi zw&(BT;q`usGy`{-xmGJ_xDo$)#`0zmDN^1wjM29;hq$(wzLdDzkhz(1;WE%~|2ktb z=-dMTe&ioS{$b`F_ku5C6Q27^=&dSN!470Uhs+m>l>VmCG;Fw^CVVG)*pzlJJP&Qz z0Qx&e!t2G~1N66<=E3WF0h>U+K_t4u3nJqM`rAyxb1&uI0Q!EDcgnl|MH+#=-^_Q< zkuiY2-{igW_LGZMCD8Yq@Pf#Afn4$qw|7D@^N(V)y!H2xHoJuI?a3G^)5sP=ro%=WO{&HuOid61Y5xKp#l{G;<|wWf;KI(`;o0& zT1;6Xe}*O7MR+^t1U(-vP#w5(^&;DOd9jLsj)?`#i;y#{uzte4S5dyeI1|0I3Sw)) zF%QufT+Q4K==Lt(G4R&l2cc`2+tGf#Rn#L0;_qAmuL7NF>=72hAF0HTE0=uxpnEmx z0KF^Gtt(dD*Fl4B@i&n7dddk`+O`{45!Qaar9VmZn+O9vHw9D|=mm0l2NPM8@YXQ}>oiNb-4c4CD3aIwSEV%}ey9qha^Gl1@CcO3VKNKcd#qCCrHM%Gh;DImK zW_bFNHQ`7gdOO~y{@!8gOI&Y_KvupFp|8R6Br@{-2z?DDTZ(K@$`5q>+A41^WvRQ>+0`7`{>bM3S&~ddTKZb947~dr> z(B6uiJoIcs53~aVe+U?a$u%DN5VBs-%{omk`5s8eGsFqxlJEN%&r!b5mJArD<$FJ! zyO9OSeRWaRzHXAeVeoPxT-VKlpLh zspnXH`9`Q0z5(>Jt}dQ=^?=OG%&cRMIqtY@F4Ktqlffsc68;&-7#Y^TM1E>trOTT+ z*uQ0J;J-xq%T)YdKn?Go)57H{{;yCC@1NEuPye4^MSlN$Be!VrWmAl~GiJ~5NO7d+ z@e6Q8SICe08OTgG6{gROXF*)_rh) zd~kn!aDRMoe+)Js{(rZRI9kh^@wTCi8!uHE>&Ik#c$~|%iB;1C_!B|TiG9&WWfkSm zF)}Y5o8wxPZHCQ*6+Wpi`WIZ~%NcWfX8sAs6l};6d*z_`>U>-$0_>THK38(tNO~lWC;oUKIz|Mz(#h)d93(yF zZa{I)m^(6kIpa2Dg`Ue9<9i{;RVbc2GA$dDp5--1q8}HdlzFvXc9nQtr1LzRFY$$e z0q=a_EwyBYzZn$6mo%y+ImRMv2!9(0rr_^_zbplRANk9Kb8-oohP<@h<>ULJ3v69GZN)jp zs+rRR54wo-y{DxzSay+%B^#=1Vv@d6xFUR8_n={`3 z90_>=i!YLp=W|>>5;BQ|U@Hj5Q>Q)HevCM*c4*r6#cUVmbRL_PmyxsMSU*%57RgqU z#T)EUOi0<(VRON0ebF#TzIJR4PtgHJ#sYhwIJI?Hjt{BVxSNzaIEARAmOYY3N!?od< ztS`CDF6BkDQ}Jbsjd4cszx+ z&Z=V^I=*b_C%Qmo<&o%kB6aOVr(j%$Dsmr_*{TXp%xVoC>pm{Cb-nx8%+@w{R%UBB z%X8+~XAtp8A4r1z^m0DxlVqwc`n$aLBhgH0tJ2pW$#Dr^uH^nt_)>NoD~?3Z76Ifd z=wpT|PcNcUo(&ZTqL1Rb#_O^f(3ZTJH7W0}A>)vCE^#co_CWN@6301K9CjbVy(fK$ zH*-Z&9}=J(hDyI~51XeRiQXo*=jrs*9_)I`ydC>j|PWwlH^j$k`_IMKS^TqD7DeO`a>GG`Y?)x{{-O<}0-9K1*zmg)oW$DxF z#@0kI3A81=e|F}B zDe_mEowX*lxS2P}R=ROq#!49z!aq9{?V&BWf_i*S8h46wyj3}QlyFu~ny7}1%WKEA z_4Wg1U+ikramB}ob&eroQFi9j4zF~_L&l-Uq1}5AMSmgr*)%YY%x@=JO?RK@_WZ`h zeNh*2%Ozu|#IYaVNax@M=t=z-GnYA0?z?hX8=B=DZ^MPO;fDt{sQeu7hGTBeyu)>8 zmXdqn3E=kiMb8jB@i}MQP7yVAY{tNOIO3w;2YS(;o=#tC`eW!P&$`i*dnOOPp8US( zGwJk;=$9@})ly2b8&fo4nQO^?C|A0+s_S-ML0_~EIal8o%(Jg_7y-sE+I^;4@N9LSeT4&twbh^+P7wC(wl6WR0=*alXs%T(5QV+#i zXVTC*UgUEr^Nr}NlXyIqPKvSAlevU~PF{ymD$<4etSkIsgNabUf+Q8_vrVQlQg#s<>lY8b@E)hQY+1tNLb zi49kgwRU&44PxWk)b$}Y`mqtj#y!xk+tb?cX4a*)A$96yJlTPb2gJrbmW{;y0VDH< z6m^;>^2gAh6!%4MhnD`taZh<2_f#OeF@A$>L39Cnb?E(=`gHU|&au;**^na5McJ7$ zH%@MzzU<6pu#uZ;}3r4sg6q~DUV--WKRs4u!h;(IBzE(JM2*FgRtx?9k_T6F(} zZjrP6u?v49<=m}h{y9*_i;NeUaix9HGPytBv(hl2UtqqN`ErV~q%BK48?iB=3>(m{ zlRKP!C5sUDpWtstrU02kBJ3QXg@jVH>kJ0aT*bJEa7(M9p-`E$;KOUXezdTT<;5dhlKYJSS zjAzed!p6SnVJVyC+u}O;=AP{nrh;tAQhy>}fczU;K0LI182O#I+x9mPEx!%@^U$kFz${adY9Y2{|$GOseKHV36 zTjXm8$p5EPUgml|?9c5({-5*ve=8s`)c44!e_TfmAbN(92sz{Y4Ztfka`I6VY*xhr#)$gaYyNLdeL2$qb zLtTjexN)@C2l}Ei$8zY$^D1l1!|C-)(J$WG7cCw{KkMvN>CaEQuAWFz1n$5AYtO*k z6JpzbnYAS}o-nzR*UVaPYr%o&d=b!kO-y##N=m0EB=|Hj=%T(1JTQ%r9PeWZXT1hn$Nz<66b$XFM@gV4@56WOn1B= zlxJ2f#0DgWY07gc*i2=Wfz2JDXYPU6voP61_GJ3($B7Hge!Ux+&GQaK>v0`zFdlaR z6JH;&XMTlxUQX+Eqe_{$+2*Z zZEKXI9Vc|(HZSi;l^4WxuiOy;#|cTmz_t z#}dc9gckwhUQCqR0`H9%;&SQIC~Zpe{nqRw*4YrldYLrM>iAY(1lPy+(WCjLZ`HWG{Td zB-+7c^v$lGgnlhPHpo2q#uWWBCsjP?ZxlP+iv?n%KcPR9t;Ne79h%v{q|2-DB|k3T z0qc2sX^wFj>6G-p0Uq)kiIz+FNnbJ7a(BwJ(slG)1ss0Cvgo@>Lm%x(p0~M9MUG4K zWscu}-Vt-mc74d9&tB6Qzj=7Z)DI%JQR{!i(w8!P0!+K%Nc09=^ISj)Lz^9^u#)HF zx_!3MHm2J;l0Lc8$xbAHT2}O>Z09BM3gA64H!hdXW*%}rCA~rLt8{SP&fa9*C~Qb>u0b-{4T& zfGK4GL*D}Wltlw)TjsYZwqvgI;sepYC)#z+v%F!`!b+?s&)f^M=@JIS>Cdh)H5Lz4 zFz4kHshiihSFcHo)0r2~a&x*;l)`#h_7wx8Oxs?i?xcM8KKWi$mV|N%Ps+RQ_(M^7 zR4kY9q&^GaIp=uG;DyjV9arv;rQCal@W4D|Wp7#dY<;*&?KyZcG7{$&uy4$vsPt(f zV@*Q*gbsZ#?45BWcFuDF@{-;?oB`seW1RCI>S=?kd8}iuQ<8nH(<_6(G*l$Lo2DO$ z-f8Qbb4=-vWy2znz1*hcQIuJh9k_LS-iw;;n3|D3N9diP_ebwVPsAlpZhg*Eyy^Pd z-rS+*%?A2XX09nmqF3Td8#mLaQ#PFbe;DydJxe^Bzd5iy6kmKep^Zp;*z(c%oJVw} zJp>&*DHEBaO~jtFjR=1u*mu$41isjK2uz!PAQqR%c7lV`4h)dp13#1``x?9j@SO39 z-9Eub;&z2MhI_5k!IQY;N>^8TFZL&-%Y&>No?YLubG^mzx4+Pza28K&R{}3Kx6T+t3vV;1fycJ1c}cyJ$JqP8S-*Ea zpzvpNZ&(Kp*~;e_xw+P=!$#>V2cm|I5pCLr;~Cax9ZLS}Vuvq~z24#DZjm(0T6_6J z{ZYw}+~>9Q1;yD5`CFJR;bFpe?dD979e$Y^PQ&IvxRk>d!Z#DH$2lo;j!dd~&-I)8 z7p^T<8sqDw%y%5)tlZ+}v$EIF*+zM;m3X*+V)CA5hj<{H$A+;%T5kAU#3#@>PC(b3 z2k3k6D!W(kWmh`aaodS!M`wTZ5nR`9U4BVxe>vy0EY&_{p_%7R#8>xSd(BAvK}j>b zZC~$?X465sdJ^m=j7OPIoY0${qT3adMMVWWB0k6 zUBOtn7c) z`H1D;Bl~$wXv}t3l3krMjZ(MTy^KE-(3ya1wK?wJpw+8MJX^t--yMqWy~w??1MK_l zq3B0(V>*||b@od5Qu-?i=MtSB&;c)J%+YPU0Px`d?67HFtSVk_|P5-3yWi&~`3lnwb z!P^5bRT)XS%ayLroJN@RnXkTZ$b3h~71I4OW4`$;xXK*V?Z^G5>~`YYPJefn7&iMT z+KV-TFR@yHWwRT($R#`<8A9$La&5Gwq2vrYrJf<=BtLy%YtLaTKkJ9iPaAf}yf}3H z9msjk9y&jJ#9m-tb0y$)UNcaawYdal2$>@J^iuw-7c{DD`1wv6WD7&K2<*?^&JMg zUbF3O9@?IZ{52q-%ASEOnGa0knf#dv<#204IY@syk2Ix|k#xzNJ^U9tT@MYNF6nP4 zA)hK;VsAO&Tw^4z zlb%kUj&yB7=D|`9?ceE-&c>B;x7SN$JROj}%AOYb%r#swBKwfl^8vbFbKltukKw_* zgckz49K3Y#=OHKYcb$LOeD)ym+kIzoj+;q;te=o~Lt>t^b4qQ`z3k}NE4S0<$v+%h zW2dNpBa_pZ;|re4aR(f$C|;jTG5t5!?&-=y$5*-Huz4n3$|GfbydyupnD`Rk3m$QN zQuc!2?8DKSxH1;&DUXal>C&n7b}l^}ot2=s&>XkY=^4n1ZfEdtbgJl?^DM@oO8Yh+ zWc<=)Q_h~9k9*0{R_!&qd89kxT(IBX3(vet76#syq^HF4`e52Tt|v^=I_dJm(V9#v zz5(lJjAs5bXP!8+bDg}_E6l#u3}g~8AdEMVc*>Q6ZEfEVnXb3;4o5E=EM5E4r^}!H z$+YPz$WFCt)g_s}yq4MJ^~7A(6L$R!XuEO_X1%;0?@4tM$Dhr?mH{O)$UA^K&R*&$ zcd$4&rj4^CWt>aWzjx@h^SS<-!%^8oNYM_tI}@5Ar|T=s=OdX{o9!@d-9~KNUiOf$ zkQ;DOiu*s1Q<=FcXPF;IZusOQMMbpv8(u+O=7?p3G0CsFQ+hPPz=JDQo^eocOzacKoxCZv5%W%tNB2%r-IZ$zFuC@%WmC z5rWx4)`=PMl?@_8+1g9L{eEqg+UasnJsiC?{rw)ytjQ_mIrL)l@4}qY0MDfVIFmlJTk_I1iF+K^8lNRE_V`?oy(-6D zm}3+Up0}`VXWrz)(Gz0nGoKg4=c{Eo-5CtbU3|-3{7k=LX% z!+ys(%}7S(ft+w4Cz2uW8zfI$cwu-&&U!zgYjYaJkypu*h5TvHPwM_-=d8og7fDM( z{}Ie=l)*5_UVCgtKgbxb>uxXi*H7%WWUo=oc^{9RLIcO!o6LchtR)P@JZiWj$HNQQ zBq?3p>9j96^RRX1uprj=-NbYDF$H`~sWjUsh)o$s=IuKi{Uxp| zU-#pVd)E0r%P^h`X`AIgv*tdo%Tn03Mjr_kU&{9^@IZ?km#e&uy1g9&6`Qq2)>^YY-;ox}CN-+PQcC zC(_KlA9wT_OxH#__uhTM;n+71&Gmqudk3=XM>6*=oqsr*W4DeX}`{=EzfQvdvQIertA}zU|c6_8Z90jP(0& z%&!lopI`Ih{Lz?SPbfYd{SoO&=odygzaGc>{AYH1c^~uZO~ajEFK3_7e5dCq&adm3 zU%x_exc2M**)c!(Pse>Zg8B7E=GRlD&wnR{%>yH|spkh?=CSulI>&v@`7CFM`Sp#X zn!oK14}X5JX$14@_1M*Y@^I&Gb*jaoGyw`y^tR<~+(BL<05*Qik= zMy+dbyKZr-#@(cyWPXvfo8~)-z6%@f&v1L*zHv2Hi7pKkEQI`^ud@!m}R=wPF;AF8{F0%EF{6_6$ z?D;ACxA?TW9i6=sH~!$hV-Z8ax#|$kbz~i(6vtMxo_NQrqmowaRnGdJHSEx$aZEW* zqGE~E^0Gz7G5$)4W5hoWw!NY6rRa8#4bN-2xJxDmG6r3E(-KL=Q!Dqw2r|fCMIj7)E^Z7vqsX3oC|2j!H zmld4Mrz*M+KKEfhLNQ{i@}Fg2h&oF1`JF62b?t=X*2ij7_KSckYdjR)7OA4!=@v5K zMlTy|0}5(0m9QVWo6xFf-tzs-H5>tRqG5=G-6M9PZgA-hOzF;DA?dDrdg)fe4&O`i zHv6G_i&kD;eUTq0UDTAPockNGz9h$-O?18qR~6hGsY7I!uE5IjL7T$eBC?zN-1{v5 z6wv}rdg=J8$8W^f=%JK-3jvD#3Ai8Ta&a{MGO_Uw(Rr~xvo#FE!rq0(!fPRn^T;oX zkF&BRWp}E4Zg=6c(I206-$>azRGgQs9%nDc<;BK1-kh?pR{3o6=F_YF#et97ee2eg zeGU1fOSm1VK51U@NloE)mQfO=mPNN)cN5p-9tk6jJ5#LvcH0BX8t#fLhuOeL?bE9!s zq0thUMq*a3|AfXeeI3`wI~5Z7Vi?!QBxuaShOHO4(0EsIXq10l#>`_1jdXqdNFlN* zu&$0Xjb=9*KT&9G2ux$>n<@K`q^mB42G@r=M9L>&TptzRk~H4xLStAF!==u3{S=RP zO8+HgzY+YW`zb7+i@vEje5py_#CYP<#lbU#n3xGaUTuyKC`jebCX}10nRq zq01Z@@m!~$a{NX#j{Wf|8&~#;mnqGKi&AzkN?0FJXnLq5$=yBLHm9h8nlsNZheTs2lyKN}$OwfSaORsAJ=(P-Ojm~*npHXQx z8|$^!hpyOuYT(o9yuS6h3{7m-HKth_fMzxN^&5!h7cw-Iy^{8zG3;NX(>$aUA!LBL>=JA4k6< zVfb@>0iu@d(z{DPbi(Bc`Rb=H!@}tRd6z&0b?e|ewng= zMts&OIMaNpowYfiZQEtc9|bN{qWOEcjnhA`~4*Er~J^pEpW_a--}b?PGlPMxyMXs z*BjPx(`R=Y1IJA2qvkP_m9G{*zA=Qgw_?X(2C-(^iTX;9nTrrJ+4Gg+$2Y357Pmv? zvj}mRnoXxJW-^ZTb`j6`9Jd2CW>TDZ#v#Oft|hqhOnHrs$t^B4iWD>HIUr*ug9?pweH162k$@i(dl^6FM&qYNX&*-s za}Y6;LkbP9k0QlPdJjq(KXIX<4#V=BxpwZ#qbKZl1HJ$TDrB9!Z$(YHXnLqzA zlUl^c=&m5^e;9|qn8|S;e6wD=(bh!Fq zN71i~n91KVH2uU(=6_*sKZB2(^T#ocJxkOy=RIMa<;%JbZy-Ces*u ziI~Y5dH9TCCath*8XwiClJ=AMq_J_{`KYZj-$(8iQJhfPmcMaS+>Js$VFoxCBu=`^ z3yCGgSy;U(vWb>~-cPZq6-4SytMe-iizPx_2ztX0e}Lru!&RxD2H zbXtz$q|?eXPo(d0@Pa*(SWe&L=mi^%WbV}Tg5CFQa~}E)@K#FMrk-`exvKyQ#&r#p z=1Hp$E;Dkv%oBq$*l|#>4C`7|pE@gFNWVUyme?j8uWi?a~AGK{}^)3W`K zR0c0E*lr)NySJL-ZLKqhJ^C7R*nS^)%Y4AL#>{QC8vLH_sMk}!BW=s}-xrm%fJVB5hj+`4#q>9+-V`5s~&Ec@wl_-PsouA$D^upc;r`ZKL-!q0OW9WV7D zPwmA5?ES<`roRVQ*u9%ROx+k(k5(7z=V<&*IOpL@FCd)Q%O=MVNY#k(84ftp9m+bM zxcqeMveJ(s<3S6?akvAN7d^eo$1ez0p8Y`FGS_ue$Q*X)Z;M%X!;^|~?-6XkGtyxL z@P3N`GGph=T?ZKU!X@U75$W)_PB(;;-ur;}T*8Z5z6vO4!=Z(Lb>%qD(X;l1*e9Ua z2`0VQN(`BWULBq|Xb}B2(C@$};gRbN$oC}1smLsmwb0NKz7|(tDJu^;+^rIKDQsDs z0dmt>A`%0EJe@U%-;#g#74Ye3u^y{HD~KMeA%C5eKRv$op~t9{bqaVkN<0sQ+R(hv zw04RipO$xS^R21Hbgn2lr%`im#X~Mv3VZ3J2RMW1V{mzKRr=sITLtW`o4~(1zS$lQ zKzD7jT;ADE=)*0UvkoWoG9J(#j;_)kY9I&kK=vs5pp2gpXH#zX;ZNF8pLgr@qV*>@ zcb|Kg9&mW+t0$oC^sUl%qTo%`8R&14vZigPtFS+*lVwNk#oW8Z(XSw*NxEA|gg0ac z6k1(SEQ5HNTXVwxwB$uSI|`%-jbwER8^f39#Wl3}$;JtP6n)u@#P4eFNXM9WF;HWe zZnnt{z|e3=OON}d82WwsuIbBTh*P;BMk>eL%ZUtVb%EAs0b01+CJ$|6m^@ytXj`@S z@o+tjfEJyHQu=by#!yeV^e?NPwh)P|%X19kN2M<@pNmdK&0%9T=5z9WCl`(9rQ->A(rKxcjUbov(5aOheY zanwwKiqUnVWovd6p zotVEon`FgH2P=Fgejh$p&NYBt?6H$nyvmh9pM+zLVL9&Cx=?3gjDdbFJ$@{|Qt__d zJja`s?UxX3F-}03_;P1v<;8R5OP58ixHQkW`w(73o3EOAUh@yj&U`nE=gv&`09JIg z8Arp*q+MrlfN?Xq8As(g%XVDKEp-P&#$~a}Tvx{j*LNTIcsb!fNt5|g44_nY1?6^7 zxeWg>$=k*JNxjs)(EOU`Vo~7YHQuxhM6u)`j65xjR;LY;r%K@Aekr35BotC^Ea=XaQhWnWFM%m3_w|j33>4fWL{JiCKJpOvjBfaLe z)sokR__9soV5coWflgu1SOeSCAu4nOC zpNHVLc1m9I^p0`-TU`8Uq5S0R5963g=Xxxzu0Y1~@b7*F>b zSBHn~ryMe>IT4Aq+_+aKo{=3?pg;YFZmd8@cyTTo;V)<O)7t?%=eCwpZ+8iM4Nz~hpkD_*v+K<~#>T#I4|M1!4#DMy(9XrW3z}OO z3xOmlx;DUin|Q8l0|j)<{<06T+1E-L#drxxP{bgjF^tg~jVAQ<#5mSq{X8fR=K07A zym=J{Ud^xhJDGKPXN%B%%wQRo|-*f#1x{6Tl zMd6f3HOrqBz|a2ZDEhO#F8I-|ZjRKW0b;Q`3n%gblaA=6yEBC9jr?WJ^PPIL%CWBm zwJpwqY&(O}Canu^rVoAH$5Ec>G+}J92cIN=ULS|C1#Z5@4vH?9v4#GAS+iE@Tud&xwXOV2H;KF_LPvg_nfLf@Cs znnxnMl|D9j)>Q*^bCcvXJ;tU;zcSZpi_~D0oIM-U)m4M!sRDgwG4Zk=9)}P3rsP*0 zU%&$+u$ndSZ49>+zQdc_&0*|C;dUeJ4ES|E8!kln?1lF|WxoyIt*EmXFe_KsrOFm? zpfT zGYj-Du#Wp(JVjF&muiZv7-}90{kVzf(EAYf5s{u9{~_j{!b#|Cj?1;4AFNy=vTScA zdhjcV16y0ePF^@2y*hvr%~7-+J~x$Zi?NW)Fjw1-8DA9h-D2S5@nGv87M1+2(Blol z`=TLB?+Y2cWUST77!4%j80H7=GWx^Eb$%TD1+kSu=sHNbhOj?dVLlInp2*=Od^GB! z+o=Sd{<&NW=7?_P=y!VQ{1mlYYX4x1~5)~0pY<*S68HGg6##Y*fT2X7~CUNg}?IVDt&ow|1`ekb!w>x zj`hgsS@sk|#}D%!1wSI5U4VCn?E-C6*ct<#fBkQ*r z-a9jFDIhcB7_APDgKd5QF$K5wsjC|RoB(lIqrR~V^!$|L`WrvA zXuL|0!q91P!DUqj;UZh-ZQyYk63zMZw9 zIN|EY{@mZMb#Ud)eHtTGv>z&WCa^9X`m*a|pdnrL;T?ZjX1^sp8^U{_Ci~f5JWJM| zNUx(2+yT5TP78Op{-=X9%c@KHXbXbwKg)jp*>BVF&tvqQl5BI1ktKAG#Aejmf{ zv(MD$pPs_+he2Cco^rExgQo8HvJmnq7obnMduj^MZ^7@C1?czS_d}ZB8%TcqUjO%~ zTkuKpyVzUheJ+MYNfQ$&GDY~Y@ALEc?phx60`xLULQLGQ-j(}olgKMt2?HB1%hWgN zzZTDi@XSqrWJ@jheFXZUI5EAbOedaoX>>+qcHwu`UZ~74p3Q5@MDhDU{N9Sa@LTZR z+24K*=C8Oz=z&l(zKC;9vw)e3p0E!SjJqzYLzM9_xA9_vLv>MUf&X&~?1gNMRELo2 zK3%@?F*w)n(KK3=Z%6qal&8B{we;2U>tuPv0_#v@1m#<>5C0vqygJr_ClBRr1tA+C z-A$vMRei$AdnVcth6OvFGhFohJpB$EJ740Zm<#_N!?SWsZmV&A;Xmm2%suc|z)Hm4 z<8|Qy{<@y(nd-0!Wy)LRnic*h{k7v+1AH9$A+7vwUJ{!Gsr(4a?>*O}`~gM(tUu_# z9pz`KJoM+b{};Q0FrdXjbtG?48p9TmLMp3$G>6GNR3I7;)WNCF<2*9hq1hHl;x@)JDjk>e`Tm!PsAwAKmv*XPQ=E?@r5dGb@NpziM= z|MO4ScMva-UyiBJ8-|duB0XMiA^V`dx(o2-JF_>)QBw7aPHxU;sr{P`(B_`c8KFjE->WZOASE>H07& z1EZ3&(%!8L(~4T|(?)FD%X{ePWuO%Dcb;_eozNc9R>ua0@w^;uSgdK%^GluQV|YFU zy>}D8^!!@q`6Qn2s!ZDk<-HX9Nsi!oJ$#_F8S%UZMHldV`kIsC>~v23B{J3LoZXCK zGuGhqPNerDsD6l8K!vB5GT;IcOqA^(<&J>%)%w1yH8kvAH=_Ncd<%4NVWINxcPS75 zK{h&z@_R2_w!cWaQf)LILdwFljfyqRDu9%pf%z|VtNO*L;x}vX`-)56FJkTk<}COg63&m!4lC z{%*oJOmT2dn>mJJL#T@&<;l;|D_2J(EAd|b`$glul5IX<_dj92x3go;95(6$cHdER z*^c>uZ2&B~2=VgL?>uy73VYB8Z2NCh>G8A|569=sVdo!78r#E+57_;<9~RBUNM{|# zlj(V^mu&Helj(IAFW73pV)$Q>2lgHH*PpasN|3^L&^*HNPxL6aEk*H++EU*M=ME63 z42{N-6r3C~e$(;BWo!QCzbv=mmKSH{+B!N68;yFyk}N`=$5QrId{4`Bzax$pjU&m- zT+a*kz>+!a0`B%OwV{zRZ#h{fJz;a$PTY@a%0nIQ(e;95T~{uoY_h$yuI1IYI{k6E z=~S?CN!&f|HedImx9do-azj<_@f$CCtuG8#Ze%i;Ugo|QOuoa5?(q*V`SzR$R_^c* zgYntzsTKPi zd|K_!nya`Q4*U9eeutktaU5J(aqg+^P;hiQEy0wJ>Ay%mIu#r~`RD+S^GKF11&2>Q zY92~C`sh}0_~hf@etB6Mm+fUm64{;}Jz|pzO zI!tTmi;+ZQq}6Sod}DY=qy@k26o1|BHcQ~b1cCr9ul5<2JjE_MX`hYt9cvHsgu8s@ z-i(sF*Jf?Mz)c2f8a=ITF#X9FJtguG(wJ1rs@`P~3ndWW$vsEg6b{|(WB(!52gy2g zEamLI#&?~~GVIiUdBa8}-uRQj=T%4l;!THnsxg3Np7ze`dGe}vZvWlb=Rj)*)->09 z6LRs)sa`6T8 z0oCjS+6v>-*$>;=ZrHBZy_5{w>B9$96xj#V71;;W6{*eE$#ytcB>q^}GicYQK43Pd zP3!8Mo>P18&Vl#x0dbU}F=_9k^3MHE#pd{qciaq@9`J1lzfJj%h6aKzKD1+lIDR7< zEy$z%I6keuQ%7S6a{P*D3^5IiV_A+HvN#VcM;GJ}H1_(Zp{qZ(jal%|v0l@<>qE|B zq1OY<55^!YbCK$9Ho%lcPpgvVSCcKn}hqE0XpND?s~QUoy#9zW%;sI zmakf6`QvTDp9J>tah4I!Iz;Ye*zdxBit4DhwW4F>l9`ib+@^64YNRK_l z4MD3a@5_ez-Gq^M4GV*~V+^#tWdT*ejBgU`a9 z1lx(f1G)I_3B+G@g~T_$I(#wkM0Pg`d8diL!wNp`LoQT&ufaVojC1Ze66Y~*oL;`Q z={w%>3bvnmAFw?>U=KXJ=(*>Pc{n&>&Tj&+uYvCA<)w!K@L&qt3cQ^Ht2f?3;Jr)2 znzp$ZY(}ixmVQ;uPle5bX6v@BtD4WH?2pqPc)Yyi@7QM!JNkL^{&?Z2cOT3A?l*vC zU%vORNoC(5y!icQ{V~%T11-}rMfrzP#$$@X51PX!eZcnkfSm`dscdb(H?N;kAF#*2 zolKuS)!81`<(emhjV-u*@3GaDpZLz|%FnH`d}~1d*_P+#%x$^reLn2-z2>mBKJZTX zz*`NxY|Ezpa};C32KYRyEX#jx!i}-Y@twx7DZXipn*_ef;^3QLeAUIlH~$OCU#u`b zT|10ISEP#}j4#ES5(0`3;B*#t(KON_J`2C;%2GLc_n@peXLM;Kda=CevQRm0yEE@i zrtbt6doi6cx#t=)+kQ3ZJ{$DuMSRdpw?`+6--vD$`WO4=8XSG8`{s0VGCd3Y;&_;N zdBGmroiw)H4qznS2H&2tNAPLI9p3Wx)L^^nC6U?-BQazJ$~!o+VmkgI4g!u%>n10Q{oPug$M>FC46!kxpl?k9B=I0UJN=he8zUUVM;-Jd{KR)1J*$0#7mjv`qjzUY+#P6r zKg&)t=U(B%HF;}w?%T6(W7>T?B<){4{j}>@?&DdsGkMcUZaNd6dyYAwbq&UHH?Z9H z(?^?arW0}tn|b!Xi21r|GrD$F30on1I(XhwPVOfh@(pdorGU7gKv$+imYJSQ+TYio zZSM??MA{o8oi~WT_lUotiz7{VbemMZdXGzE_@+n)e$$Wfnk3!Cza;JVgz!&huWzPq z6UZk$5lL*s$In3}_wY7dysDFnb%e@Atj)vzY~>tKUAB~hnJ2f1IRo5@_ag`@(zxt?Ek=hDAsNld0$+j$eYmK z0n73pds%Qk$_`3CY7WXZ>&zNfiBCQzUX{;>m)@sbY{sihrHkHmW!hyMtN0z}7rM5+ zF)|&(MITdGyTF&G$b|TEZRD`{#2v@Nvt~DL;MLLOdWoIyUmpztYveCBZ zKV@HpPxR?@R9c(shT$iD!q-*$!#{YIKlq9l_=9iq$qnn`XZwS`Kx4i~hzEA`U+ z(UP`3*GO6u*JNVj>HC$c;Um&l*XVu1ki;>Lv(b-d>mk$E6Tfs{QSLZyy|{=u_vxlE zQat6swuD1FA`QTFrTBY|_}eZ1wu`@d9$C&rUotmu4;BIjR57)>s+V$BKe1vzgzv6n znvl5X3Tcze_+}cqK1z#|ZD<#89@XIlhoPIHkK8j+2KRTzfs@7{z5l#upM|=yCb=)% zls?bZq4da&;n3H!{zX~Set)cX$$mNY`^T9E`dhxw_cPg8b^S|}>3ut;+*gY|i?9cA zNY!A?GY%XibH_tc=9;g`{=fJ#54}vv+{!fcGMnnLx_ZeDW6W&5OTo$G#kZKj@;nl6uwFHf!4%>#pq@MqpZCyq|g=~Ad%`cJC#w|LrBeHQ(4PrKswQ;u^#k5L^7yTUL|-V1k!da)(^miljF7<;8L zZuvffr!cgwg~lyWpeEbz2c66Dxn>8&Pti{!RgF0LSs*6Ge&hJRChae{=qU6J#cyp5 zm&}AR$BlG0&T@DEQ_{XG?Nf0d-aAV&W6(robZzKQk%30~Ohopsr7vzNqMJR}dE*ed z8t?rJC+)wLqOU&8{h-oc#no6xW4L6*QUuetGxiMwuTI+aX&cUw1<|+Ag(5(QNj=5l zx+5K%Bb_A&LoNeneVm2;SL}kM*gr&VnzyMIvU8H3%GRHsOy?d;&l_T)&CbpmW$C;* zEE1=}Ev9D_RGwbCwbwX)BOM>LlJ>Vr#|thx{&bFx&o$FA%RO5kOzvGzgWO|Jp0w$1 zRxA4TO#F(+Lc4P0uJkAO{Of|rz5f-bzP{U*rQ9vWmwUvg-0Z7*QQw?5E4!a=4Q-_q zmaW%hE~X{#3A@$#Dza;`zr9a`E)x}q`(yKg)$gLqkE*ff=5Wa~PdD;U+;$p#>XPLi zJnz)oQ13I6_Ag29&$`Hcy4qH@giG#M|JVCvmV2QtSlc;%?x~l%rV8T~lKZDFa+l=z zjJ>BB)i;l6c7HNyKdO!&d2WxiZt3&D*f>IZqL-Inuli%sKKGg6bw%No@0fX99Vemh z{NNc`L`-QMJ0WRQd?VvSi%etkv?ZJJT3EFT4c@Q)afR)z;p>(EXp4=puF9~!**^_keGEY_q{ng8$JZ&zt(cSK)Q4CnddQ^~DDR`QSQNUI zzg#W9+{6#w9y#DFF#SF<=6p%;lGDxn$xt%B*?7W85L;B%z&1 z`wjQe>|$5?es&mW$zBJ(lC&v5g+e1Umurmlh8}Q5i`C9<{CbD@d$0J5nNklGcw3~E zKK6)*w>UpNC_YMVTjb<**Tr2V06ov8eFhT0va+$fZC z6PQmtnw`>Nt2Q1G1%1-Vu}?2KXHGJA*ENPOa`r2gWi3}tA}YJEuYSt5@o_RZos>1X0M66VwHFg<|T zjWrN~=bznS_5h~lh=louJIq1Av@y)1?l7}}=>@I#;nVtwJIo4x>-n9e^*eW%DC(N@ zC3yaCcbF!?^f8{3?l9eeiKEU0o{~eEaf+ft%2jn7=Tsjcp3hWZ#NBL;o(}+KKlCi_ z%s5+tk>i~x%FY005Ij@N#yVetk+jP3d=Py$oey9eqH<+;wiosyc)TFLoR_V{fP-vh z|A{4$qr)0dbire#-AX@=Oc|y)yS|LO4R9I0l~DNN^^%9O$GF5tJKnp#p0anqw^$Fl zl&Q%s69@cmyjy;{XeXfu>klqv9#O}u8?iYpGbE;@A3EPb`X)K&8<)g>FnsldoveGx z_%8j**FI$09b#+&X@?>|_>z2~_G9L)tCpt5FPA{EoG$$q5j=g!w z-h)r;g$f_SCx&hmT*wP6z3{PIbYISI2;VDJe6;@9gdARSo47^QbRsyH*1&a0JoH}Zx^b~|bg-j0rkQ}$ED z^FJ}qN;~?6!m^KcbPRfZ58?bO<1}eUcdn`(4WE&+hlu9a^)&O_k)-Lf9kt`keko|t zOJ|?l=BjQthwbzMd$89$7gG0a=CHLsU=Q4BF57|+*aTor^E|CUXQr?dK42$r&YWfP zs^j+C%weZ4HO~okzz1x_R&yR^vDb)}py}l$Th%u6oKR~3>xGy7cHhYpzH>s24(RXt z*2xitjMNv`4f-$3ZTQeZ^K;|7zvc}~{Lp*uvXpXH63%fAnB{~@yw95k^Ir9SbJ$Lt z4Z^fdymb0xyw+#T@y?E#!?v`V^U!&rH!SPA>E2*DjE>+uEZM9xKSUiF=922V!C#&R z=JWVX!OAUsC0OpG@vjCeXI&ahFD;2+^3}a37@sQv%hllwZIE0`Q=baP=kOgZc7tIGwouw`}Sk9w3R8>N>n_7cT!WXmldkmsJqoq2|OhSe0u>>ioJX+ptan6rra zs2G%dOe#2h=1v-YK=N@|!QqpS;|=mm@05bWCm;R5aUOJQO)EHj^3n8C$;XU>!zUlp z=SV(g6&!|qaBQyjktOF&vLgEs2F-s!n1v_?&H*6Gm$gZN8M~i*c zVl4(^ZG)V@%+tQRyy-CPu|98D=BeXuJx^Z#o_)g57nYoSP&MC@>jr!`Xt3Nel@CdL zb;ZF~1AJ}JzZK8L7c95Tq0dWvEycliY`=4!%WBWX7c93-Kll^*rMe2^^Rn%^E6sDu z9PIFht&{Db`>NvRZyLIy$oL?4G4DB_EfRm+zH5NbG`CFc<=Oe7bahV8slAW4XTzKN zfI9X8y}33Q%m+-OO*}1IQ+uz; zf%ozOag?FC z8oK&p+em_cj&qyV-S9T4*V2E@jNyu0cfD*MRi}=|7~~LojWLbl*WD1?N0WaVUUjzu z{<-eFU?)CqerI!^0W8NdXTO$QReq`ZfaTeK>%pt34#o}SV!XWuu&jfMUnEzRUutkB zue@`>TO6eIo>$iS&_VJIDW~s)5B9Q|9s^kN8T9T&+^QL$>9rxbe&^*=cwX51c9{>V zo!be|3xnmCitUp4x{8Od1>#w~x%h(RmzsZ@%m+149DH;4OMFAc!8gkM4Hw4eWkdU4 zYo1?fuK{cw>VRzO0APzbhMOodzGginFbY`X{G0}`Tn8iJ)#aC>@~g=&?UVRcii2+u_(XoG((%mvNbIEz zmS1Y$XC%Ig)!~cDKBNi!P2;;2Rq$~ia-p)%)!?o*#@PuT1{R=J9q|EM<^y&QU`=J~dyBWu)9v2}>=@40bKVzS8{xW~xMX$tonosj zPxl#Q=F|SicIB^HW%+>obG@v&+uW9Ezl!PDY8LxaOkvA>;2rgW*8*O)Wz#Xs0gQ7R z;AgF_EdLoex5wN~5?^m|@J%zmf#Tp>!}x{@rGw z=2(`@`D&bB>LALqpYgJfD9Dh{i9uiI;L{p*^rh~l)9HxY+VD%v^Go#u*3@=8ByV-k zmHC}U9NwPp{8Cnhr2kNH>3iw*2fA z=HcS%#*}{dyrln={~P*;SpGRZ{ZpD>isk=nap`;6+b+m$+Kzb)KhQ4wjL^5myi|_i z1NTAWmq~n)!uWKukd4v%An?TS>G)go{8D8;%dx*)c;tj}eyP&$o=|x6pI@pSw9X^h zR*_$-3vvoSah9WJwQulhr|lq3{mZV!WhcMXiyR!%hiA@npThi7ga0dOzxwH?onX0} zvS?@SL+=p#okLA7zaj?$T?(9ANZb|YrMrF~x?_D?kC^M5%SRG0}bvVI21sZ$pAX*9S<&l_4<^3nDSK2 zy2yV*l^>SyABy}5?>iX!|1~&Y{{Wa!cmgi$gGL8^y!lmfij`<3jW=B^SHpuUfCYtFTq#o%+v{_BZg| zwO!LqmfO*ab1ywYD{jeusvQT^NLp)-WY&V^yrwZy9f~W*st@<%c zoX!p9vYQBRb9st8Q(c}mheL}ZP1P;R-`mCaJ7oGQEsrc`>HNvS`K#)roHKnZ_I`YK zt(W_e0-{{K1V(>958F@OT`TtQbodau4$)PYxuYo`xvTo#g!3YFR&1X8MQyI3oY9I? zuf&>eeT(LuG$xumh+JzHXjFbD)A#Z{(Jxf*&EBN7`zY{`9a)#;6i_RvI zor?9eBIT~CzF^UQ0%)xX=25HL`;^gN!SO%ESV%{Qe|gfrIPF8APtMnHhI*z^Z$pHG z-sOKe87OyE=^sx%ExD`W$4}aHue9SsGIkTXBwJ&;u^;OzIeXGx>!PpFF$E9WjSdg% zEBa@D`l^?DF8%M5MbBMjA#cY$z)!l#&J__WYW}dPU!AmXchOzRt5sXm-?PlRtNznT zCC8Do^%$;Dt8?z(_Z~a>wB@eq{LM-GSuV1MQi`2y^tO{B*zG#18}v(l@^nR?Mp@f_ zd(u63)Vx}+P1=5l>$MiXAS0`J?y8x&llBwom`bK?mmF2;d)w6t+ChfCckZeLs)cy3 z!YwfzQJexf>QU+to!(8m;eL~4G`KpR~`Sd?ClUFI4)hm#E_&vYU=Pc{UTd;DpT zyZtXG>}@3Xn_cAY%#r)4?Ek&wKJxltavy!=sju%*TgrWD@#P*2DEAoZ`)0HiO-iy99{jV@a$ugIyFj|9t) zHH7@4g2tCm9gVj8lJ*?rwH{Mw$o@YkS=CNu^d!b{rn)-tR^-aWxXJp3LWAq|Z3@eIhDpI!X?rw$x` z7X=YD9Tu2M{zYO4Z8-z5Tf(93Oza-{WvBdXm!FqPoZIB5PPa``?nEeQ{|Mg|xz*S+ zCq23q!f(h4Oj246ZQb$O((D@K0oIMDsfAuenVMTET$>xAYz%k1KGo7>ds)ET;Pu%w zRhc?N?-dDv?sciq&0%COqHL}@y>&unvX=u`_oiGfirzAN8fd+JpP+HQiv(x^tuBw7 z!@YvCUZ;bScJUxc2iUjT5JiMg{spA&2!y zX8u$pIik=b%>#RzoS3y$;m{EKMXq=5U?v!{?UfBFy9S?Dr2=zV2BrtUmA-GufvL)e z(b+WFGrgx8-S5$j;(gr^6C#^kxBLWt$xAI_44=n$rJbmBCPJaxT`^t}IS7ms zAsVktfX@Z!Wh=g0HI7V~eS2_FO4x&R*ai4Oz-P_`Vn5w&j*dF;dpY{Cw)ZUApCx>C z4nDv8d2e*#x==sL^}7PSpAWjE4~pkUcbeVtz0kjuwbzl>Y$ou`?Y>5`Wr_{-;`ej$ z>CnQyy_N{hCGw5cdLLJgBE)a(BJ=YL3){V6SqFzM40gA1FKD>Qt7}s%N9lI+JNA!X zY7V;xuomeTFC7oO*J3!aBHASFRl<_5g=2-4e9v zqu}{nl8(!Lt+5?0KDa4-4W8x>W4DC2(OeWnbu{Zihx9bnnzXN!G^L)qX{$Sg8-#u?xNX5BBqW0*o1U`xrCq2{2~p*=jxxm~JtL zodm4un4uf6relU$z?zO3I(>{8DtwF?W|3>al!tL2V}^MHylhK#KE@0*zoj-12mUwV zlg13xZ$-C>0RrM+V!DalYkD6@WgfBGd#v>iQ zD1M`|BWTBd%d#Ju^3j*ioua-x0h<(k`CF9!Enr{Xu->_oF54cQzWn-(0SLW8b=v-f z``+Eq&mDlX-mUQF^ySV{v_9kAm*1dB z>!rJH^yS6S9k-#H-!9llb1h!=z?p^fy(sb z-7%0Im)VVHHF)M0ry!j4(v43^m!#LBBMWvX>e||`*dVvJtCg|y=I|}be~sZgl>ZW8 z=TgSU);TR88&dczS}U5mc*Wj8OtnR#z3^rvS1ZNOp1bIIK9pcdrjf~I&pdw&JN~_8 zW4z^um+cn-KfSnK*4}%`nzaL^YtJsTEZTcX&xr>MurDUS#z^JmkV6O0d`v!S&T|sD zX`2(hyzo~2U^#7TUa-BuYdYqce9RnnXv&=5u^;)Uk7*zE0a!ovaqx%cJSTzMxITXB zqdtJwxITVj4x2zdM;!Cjg{pPVWiWgc!$&F-am4#X1BHNpyC{Uru%l?v(U$5gwv9b^fCL z5`6NwJ9?3rEO_2S6Gag!Uj<&Chfnki7`Jc5!5%~)J;v<`S$5aIF57ofS=5z`b7ICo zdt2hzh?^DF+z{SLQO)?K?8kcf+9Zv=pfMt8$a({f2d#CHiU&@kHS7c`4*X^vN7@p; zflh9BZp!~jXT$0Ggbnk(JUZB@&V%GSxUN58#nzk|vO z-?Sl8enq;i&^Kyu>%v&5C?$N@#h}9%mH4WG?=r%7r)jHw%+)EU4EgK&k9* z*h_|HE79B>_MpkK?Lk>D*%-DObzcv9^zzbKKlESaV(_Q2W+1Y*x7F`d&b9E z595cHoxFPndMNk7!@e4GJ?!@ZTjfKxHIR+#mOpdH*KDu*zO`&$LdI~rY=0LP9&51v zI=*49>$nftriYj9HxfU1>3C-zM&LWVV7b2;2VV03j^DzV>Q1x%rU^wzS5seKwm&4x z%RZyY=`&*3W%yW7$M)cc%Co!Mv0tst! z_5JtWm#wbT`>F1~J3!;_{-45K~dTYV%UGT8py^vTYzk)eR9GF?Cx{S`>nVS*d`yaBN)e& zp$_Qf<@*QFZ&f;=p0IN-F_*2+2W-C&*dyTARJIB9^(;Ss>iX|CjBB&{?~Ba(ZyHaS z^0USVYz4-QUVSp#ayNXD7cBSRyMcE#{r7H+iB4tzU4}JRKmGT3*P?MuDt?_gY#Z>J zuA%O~)*QCS2W<34bAI>vfIZZ0&TrWb=CIS(o5LRcOXhxLUE5^)?*CtN*oF_A>n!R6 zw(s}mJRJL>d0v3pJ?85O<$x{m@YUqYssG)#d(mk}VtjH9x>4`GVAmS+eSU3g&0%Y7 zbKUJW&@+!QcKyj;K6Wlw`qyCP;*jG-q<=Z)i*7I6N2KXNuR+Sq{Vo{2k*9){o1b$p zM{>|h7uSs9H|k&ZEXuozr=59`dY_~zjw;|d54fxu1xKf&XHy)zb^_+m7Ulf9#t?$aSKpWC~OG`eUec6Og7qwCs zE1mKV4CpztPvdTEA&x{h80=YKJY7Y?Gj>gJ_^C$zWX_$j8oBls3D3Uw6o;R&BH`&n z8!*w2mrYXrQ{PbCm2&d0$Jr)1A6Kw^?gy`x_*#mCZxZ<0z^B!oi!WF{_qH7pUsrMP z4T8UI#9wbNzF_&>E5M(?H&7U#&Oh+jeBe^tK?lC*(!KbT# z-++4|>2}xLV+@U9$H8}tRqqWhD}!|GN6Tj%zY*;O@?_B-H=>>K4LFP@oSw1`vn9Mi zM!(ZLGj%$M10CTmgT77`?pRy#adSQQ8RRb`*-6I3$Q^b8J`u|--%ub1&+zRz@Pvb2 zy74Cal+m5i2Mp!fa4??|W1liy9DH&1DWkdgg87u`PU%y|ii2+r`;XCH zl*z*QbZvxft=qs)Fzf*QgqQ#6ge{Rx?P>qN^C@+wpl!ybT**`DQ>sn@Zmzpd=;XPO zfnGYBU>|W1c^Uoql%^bbFP{V5c`Ay1x9G}*-V#B<*v{RX*YzXIG5h831MYVn_ zj^Btz;#ZQ!(Njlb5OVwqw5%l4P|la#pvch}T#io2A!w96nt$hj2Mt|)vu(_Pf9k90 zWmF1C*j|1`YnZWjD=-Fd-I{kM6p zzFh{e)V|1G>RySv&&YU0tPYz2a6ZSmT~e&lYW)qDA3i@=dA8ps@M@}q!v=CO-T?zx zmf^sgPuNYW4(`rUfu|0l`Qf4In!sbj^>B`($zOwK9JX6~^b)t}5^K9`mxlC^LZVly(>e>!PP!slTVnBX~ulOc0RP|xi_8re(QIc_^rr` zFhJ2O@4lPM_x=0o$`6mNuKdAOmTwElKl{x=tc7~nQ4`v`=)b}1d$8W(>Nk;JE;JEI z;JS#=&s{>6h3=+d{QFVwpTJl{!LOcy=ncJ{PCN95=z_vT&QSLQ zxA=hFrUm&+L6dkf>!*%Q?py)EDo zbt-(2dA{k6DSzeIhaAOzQ?Gsb>>rbW&G3eMXRempF+AcjQ7Kyj<sZRkH~q;h+slk?;MYQ%TwGeS)}?v! z&$QdVAZZ_X`e|3P+>d6_R_`l`(QVp!t~{}NxvN<2$DcmhQCZ({$SrK&8Bl1dzI;h`bE;`!Otw(FR<_h{g2ou$$1KV=ARh& z_M-hR7d$0DQFD`Q2#4-eVmk%yoaD3r3!h%JD?-d)PQHVUkppW>%GQePQwP>kf{VO) zX`y*9)0qA&_Dbq$%&jeXXl-O1SqSFbX&@E1cHG#H`84UdcK@RNGBRSJXCX(X{}K0m zKkVX&8^hN|4r4>mbg1M7Yvs4lx8;}n_`z2q2Lw#$_mMFN_?2trx6r2}`<>tVB9qRS zPKMsgKjnYfng{$b2G0L!HzVB44BaBB-w~N|Xs(xEcJPCH8QnYi%M;9dW19C5GU7CY zTbROqTx^8D+|EC}S3c3#NfWAp>SX^%7wyxt;~km4F_VwsHW#xhxW9|>*MH>H;AguX z`oW_8IX(Zqp$I|?SCuHvRhS0$N~2iX}68v$2Q%^ zux9r?y4oXjVPps`X3yH&BUir(d4Dk;6rNmmxkN%88TL7Gp zg-c$()=51#kn_XNod4T%{--8LuiW!_-FQYeQStpn`^DM5Ak$7snYTMSy-(=$78ji& zGs&0e^0!{Pur>F_5ONiQ4nDN80vdBydBT1^Y}@*r3(ipmCoXU|#hLi^ z3Hvl$$ZUO6!O8l3LhYv-jrBS6=M(mE!udZgIR6|B=k$+H*i(e_#gFCJ`*bjz9seP5 zwiFNN;p7Q>4Gh}4#|7uh)Es47jP==9a?<`T;ryx#&c{`o-=g(hXfnbkhFVj`@v@ey zQ^t3Z?0JLt`8ejGX?@E0o|Tb*FhBRa(qPW-^tzO1{sQKACgvSuB;Q1BZhuEg+&f8g zr5=&%;6m}2@+wdVsq?k;oXXc+kaF%7Y{9Lw2K#qs>r+lH0npo80KP%uFNKaii0?G! z%IvSk&0gtU)XiaBl&}6LqQ^SESx0M-(}eHx@PeI0{d=9IWW2Ml_O>gYr|v~g_87?q zT{v3T>zm6YfM480fik%Hw_qJHPhFtHGV*6`-^uvf}R<38}tqaAqV0Et7NR2OaSDf>(KROTJ(_^p-h zxyv7~$Y_LO~-E*q{ghb_NG`XvLr)v(Lw2oCf-P`%MRfqm}c zz7^cWf0@C$7ME*ZEuGh7Yxrj6KW#lT&VAsqSU5=^R0n%t4=*4-4g0mYAw2iH<*UZC(%wbExuW22RzXo~sJa}lw@6>kYE=2#D-LI*AW-N5E zYln78q$UxGQI^Zx4z_wlb^n+W_kdeR(I#{Q z*PGkk0oaJwc!Jx?KFm=}VN2n!yP6&_h_=`edwI-b2UZWA2Akb$rBuj&RRn zetZ)?y>aInv_3Bg_fcc;eZ}ab`cPk@k6I{B9~G0bS78h-?y=398{QyBSvb&}o)Z?D z$MPCVIqIIilO8kk%^dwaS(RM+FkE9Eb^9>Mli3f{&2ZCQ4D109}5 zO?caBgXE=kQ_9{UdC9vI1p~ypT)MzQ>u~Iw6NppMx;?#g`e0cGU`wl!g>8QFWjM>1 z9q-Td^)$d~{ zsKedBP)?^0#vcQI8lx%W$c&!Jhi(ampY`1e8!o0^81Jzn*CV&N=s%?F%cVXtZSEE} zW1}|5JWhhgV%jj**>Q}iKBcQO3a~b78#B37txYiBwHQmjrx1QOYXfGTt+Agt8c*3D zm3YXC>5CwUXp$s@PeLJ*4w}YcIq@>h3owB{|Ixm$zzn$ryuV+?_ z?3HmA;7i^g!3*lu2U?#gvm27c^+B>s#mN}0^W4H_R?Mzn> ztebh@3}R!YD7P9LjM^L22*G>hm8IvM+BB7|dER1fj%@Tow=2=$1TJFqj+5~iyZiZz_Q!yi>{0DYfb>T6C2H5v z97aPno!@f3?g#!EiQmmP(B2LZ$y&84!6W zPBl1n^~X9r0GT)*AnhJ;BFfhoyEVtkH-+0GHF&PoDe+J5-H6w}0-tNJ7v@TXaUqwZ zc)g!;EJII4@noPnBYtQm{^4 zr()=eTdEjcT{BO0h*<^UDP5jp%06|%^*aNd`l(+ocLch0D@VW6%c~8Ry?8~e%UTsK z?PXJT8`SBL>t)SzidQe>BS?l7tdCqrf@J1HFt|IMmZSKyLUCWlHD6aeTr)-F>u?eH zVtc59JDj#JtBj`&vBuTlVL8SE%74(O z{7mn-rv2&K3YS{|J-2qNE5~iDC%l-BqpD*uEPdW1l#WIJtnn8%sOoTX-+k!91>?P7 zEk0lm(EcN`4ZOT~sK6eZGaOJ)*d*rmrh8wGyxIKRLbTj`&sHDSu}%5y1`p3Rl&t~t z2QQtm4Rv0Tvfn~@F%Z82dyO_?f>3CFKy^T6N8r2s?CVSbUhMS+okHgo*v}53FB5!a zd^XH8`@F*CA}Z@`7gLCnd&L;oj`sl8)GoR(zw&~ul=><~oRVZCJ)53Um_a@IJ)qA*iS=#gkC!Q4BO-bcI0w%yyIv$raZLzfIaF1 zw%-RV?a%OPk8$Xe+EsLW;WG+vbD7!E#KGi@f^`}C3tbEzXB_y9!l(<4LSv%_$$MQ} z%DxfbH8CeAe?;!4gaYe1Y)30sd!G|v*zzmQVUKpCJnuqhyvcTNSoWz^;P+*u2aKQm zoK>iXFZNT8<6=`NceT{JoVQ~Y8%MR%L#IvJSQX{R@>?3 zXAy>=!<$Jj7(2@~0Q7b8d|>)40vTlPCUO1xLLBI*pPIs)H)kBB!JR*t(zRpmqYl9L z6{C-${p+HSsxM9-6_c{le%@l8MOZ*gjPNRPKg=P`&k)o~o{j*|>+Tq^* z2k9>3lQMfx(LcJaOVU7G=QGMB-X6rB#JmLm>tMO9#9>RTk)=t}Y{yxIhckJr;g9QO3}XPXj}7%=au$L7 z2$h{dxppa6W<5N8*eE@_E)*MyOI$;UD_$>gx#=j#xd!&b9jJe9_v%~`;`%->!2Fh? z&EJyEFV;up_y)ct$oYl_sf%%pL3*-rXZfj6+$8{2VDB#bj0O170(;|Qq4nB*+*E~H z-^P4b1AlKJz5|}2Xuw#in0Q$qUEr5v=y^6r_xy_fMRS;@fIhf=EMQ!6rPN2JecZfqm;QA)Ihze$FBcVBNt_IhJAO6)C&J zMFx<+l@9=zjtQc`Q-yaqaQfYQ&+TjQ+|{2SU|f~WtH;GMwE&l2nYipUWb#{<$15`@ z7VP_>QyQ;`vk17AMV>&oJpBzn>&B?uc5DAWWk0ai!IS3~aY@r#UAmuUgK=_Ka=|z* zF$7smV;JNAv*3A8JN$rUA zwT0-T?lXN2P6?E4l>6r!!#fS;O05!K{mF$w=Kw`kkKD*jOBjb=e8h8zZ+f?YRuJ8_ zL1vO)kr_R<`8f`@i51`x`+%Eh@9oA&*@Gow-*j{K{_-2ue+$hE*+5? zLgBklNath&9UgYI&D9Ihw$(gEQ8U)|hiq>>0qw1CmG%~u@>YSq@K2&&m**6&qjL)S zdu_=t691$ByI_-jDSAc^g6Ae3k%Y$Z75SvV zua{iiptYxfT<+%-+~m@)k8r=U8?+vjv@&CX>~jjZdMrKvEOftSIq;Ajl|I*e4ST90 z150J;<+Vq8WFckm2b|U5vPW9YPU_>FLf2x-Ij7LQDzh*y$c-~f2!W#M0)KG~;MBJSX)9M{7%Y-@z*<<|pU z(UIkDyv`*sFI9-G_~C`iQ9L_Hxhd$(PyI1HinqFzqu=S})y9tg)rnbJ?Jn&h;qM&6 z{%04j9%A5~WLUu3NG}P3x|MzlJH-0XYa^zk_^U$a-Wk_1>~${|uBjsOHBkh<*d8ii z4`Lqyw8Y1QOxj)*VqAX8Rm=KmgT2!@fa>P~%#&hwMye6&Ha?TohInX@@+|Mcfbx#} zl$Yr(Xxf{XZ(9L9w|1&4$G)xN$%VAdsxd-LY6G7;=zNj+xr2oldc(4AYTmwVuTR?o ztl*YN>4g}P((es;AKK;qyG{q?C_`n}oWGFX8%58kJ@2l~envLb|Ivl?o;reS$9v%; z3(lSL_)gDgE){$Bf=$b$^rC0G@NUR?HjHP7&t6Hx5sx$$Ir^*xXD<@`59b`1!1Lod z&k64=-fcfjX5|1rj%QIk%i2$}-8sQ0WX10~pH2HttMQ&5Ua)V&r`4_a>~#;6gj&w{ z-lMqQl@?~me%cYQ>-ybt?${jTS#g$9duET~P{Sx&f8Ijw9SktLJ`vW%Ic996apqm=ZZ&dTVE>U$Q?hCjSuUx@FD|AywSQ9O5ZIHCNv5htXPL^nz}D^W>ZcP(etK`oxe z@l5OolXXC_O?aNbbKXNo&)V^@48w09T3c^rHGZ3Z3-IvR1!Sf8;lF%M)xl z%6EY_*@`v5u&9EZJ+fq*O$dyR0I#}Nw+_!c@VxbrWO~nuoYQ3XM!DR}PIb2%w7Y>% z>{r_5&{oH|@bm@m_nraWnyo9&SqZ54$^!S7lCH@1E3Zu&?=5dRZN+ns8sVh(o*hY> z^y&0bSY5fwsczC6y>icbFh8@FHw}gzm|QaEz1#=y3y&@Z;(f0H@2sCSol-xjrw-Ww zEa-)PrM~Tg+Fuahn^E} z`vFVkXr9qrz`j^cf`1{wi#CgPggvQ>W;1q zZw`l?*#P~64u<5Tm(Dig_>FKcfY;~1=UFQXZkgLlP371W4&j!Kb>Cn2*g8qUn}4Qp zU_;WL2Q91a2bsGEB#o;Tq_}qVV&&h0`xnyo2-_Am1N>ex#{fezAGk7UzmxdsQTR!> zhierU)@jd}F#X{U@AeP{En#n0B$b$8B2pgO>^M67uCsBr)!pDZ=e##s)y_y2x~Aqx z)mHN0er%O>vCy8hzeBpXN6`i6cKd{)3Im&J0v^(D3_9s2obPmSs{Im}zJi%{Sl=9p zUYEU??p2Qsl1}Biq!@c!?{Vm4;%4nGBr@9Z()FH=$@HC666c0UIl9v{iml<6%s-xW zHx2vW37(ZPRzZAT^0S@~fzM8$wcZc;FG|n9k!1X;40pKa@qv;DL)!!RGo~{S{so;6 zof@ALqEs3$KEJ$&*b6WnZ~yCiSABGQpe&mz7OEKI%2$irwDJybTEER>%L)g(*95KIm1fbbHDwF zQuiCfA*?>82X2M#=w!QHb6L`JZw|vQY){&xOM2;a&9Fm&JyK1l^JAY3&ZfIF6Pqq)g@ms7lIEG zb6EjTe#+HJn_T$x(kY3#lznW;iMd!m675l(ds{`!W#AJ_PRym=!IznftUl&a4qipf zrANUneSNWFE>oZ}PxiMjpT=rqE^}X!b#LHjVt}c}yW@*X_PdCmBMLw1_MnEAykjnL z(C2o*pSkTJ3R>iI2FI4t=bau1QOsqFam=M2JoEXc%$QS+xpZWWHNAWs>tfFrmhA76 zE}m3$!F~OwvO5beoIFQA0$%SWoF^Teo-r4sg+gpKeHF78od)2hdaVBNk~oiKr5rjL zz2wJSZcwzNt0(pgV;^7g6(jM&cO3SADq=1(;IotTY(xH27;`COIwe0oC3HA;LG44t zTuMO)bE}M>TYb!B{|A=rCK@9(<>AI8TE=iT2aCBJ`N)zJa~aIT9XRH4c&B&Fg<|~F zR>nS}?u)=6;VLot-0Dn0<$7g-vr&YD-ZQ`xB-S#8a$d0(@;5|d{xP??DBXGt4G1t<5H zIdiQRg#;}i>FS_d>Z9(VrS#m-3$_NZq(^#r!H)0G;5C)A{gYD8eZ`lvL*d7Z_g#QB zjddLTyni|8FoyUN)#rkXoIh165V5O^lz&a?bK!Ft-c9ws3wnQ8^i$Irn`Qf&QyAFf zpP!}x`Z@3^$cY>Yq94RM{MJ|7XEXfh?2_t{-U-02Cf0EX zV+J8d-B0uD`C&y4#Mu1Gv3Fla4pY0U$M{I_Gw8z4D*f8t&t&+~>6Lw8^o2?LY;tON zdBL_l;eGcM*Vjl@QrX9aeLY=f`+DZ)ylF7+GjCrq=DpGf?=8S9?mZD}KIXjdGvJ;2 zG}2M&*-7QxFvgHKh|yzukD8Yqai6>91xfpZz-P@Uc9fo5JIke}v8FokO4@xSOV;|*oq7A%DJfW?HB|CX#CH$s zMC>OP_S1zWL+3e>(2Y5{KBu3bv_Aki>j}5|>`@$*K339z^2A4VWm3ciNG^4MGOX=3 z-Xwv|BiCl1!Z7D`-*c1puZh<`Gp`$-c^dbfUsYH&SWj;ye(|0L&mvaz6ysbMDSKy0 z);X`MBDL7!*vx-7M`E`-XS^D}kEp|Aa5UdDii0!Fu{B9?{>obWGub!f)9hes4&@9@ zWPev_>U?NBWTQH1eJ1u(V7%eXxAWa%zA9ziuFu%WrxhDXgzr`Uqv4*=7d|{`9D&V5iAIYHjiD^8S7!ZZ zKqCPdlA{vs@|i?qk3&PozHsYIyU3-2oT{~$m8n}Un!}rvX*S{kUOH=#v|3Up?KMPe z%ArN^?AVop%+2ZfUbmo!?b%{?w3z>y;ym|U@Gc*UHF=;y7pyDqp+np!e2U*wx#%$k$PNdhvT@?Hg`rE z@IP9Exvg@47+pST|0m&Za`1~7CC*da%Eu{iittgN`@yE*gU_aV+?9yme&f*Z&+*-g{UTF;ZKD2mr0wJKochBqty0`;Ks%wl=&)XDo;qni zgY?ql&{1N|TNGVnZDjVs9hWk6K!%G5_dW$Tk3-Zgy&J+IC-}@GV)H&G0iP`Tm^l|3 ztx9W0oFI22v(8_YgMX5-_VmnH)l23!Df8@oDf@AJx2hf&^_D%plhHwV(5!`?&EdPa zrDq+yHK@l1Nw@RcDaW7GI&|sYBH0DrB<-ci&7XMbVD_7-bZo*4cCQcEP9Lz<2C!t$ zR7a%`7k>}q(Q)(mdeQ)v_@s9k>MTfny$>`@!wcBpTmV{5> zcDpO9P34W-yli9$b2`7VuEGD5{EQW!pY}8T^HcE$$KJfARTApda^ZKa>9@`57-hKZk?p=a}SY&1})@D-nPn>j}wEQ}OvZe1`w} zs{41z&rtFCsSLo+B=a*>e11lr;a@*9zn1zb|5efLXSOuMkIt9Vc$)f>mYbJ~5-V-G zamgMAKC4ZUCmk!jMd@(7&qDTtC(~Hz3SeohXBsOd_#m;;eLl&RriwpA>gofxYgsWN9e2D%}nl`w|nln=iYnf-a8++=V^|S3Z5y(Nb680(!t)lPpWe< zF0_r2KEPsOI3v{*4XyCZVKW@-_$f%**<~G`5$_{+oju;k>emncRKI%{pX4#qwNlrH zIMXo2OmksFY(Bf!g2yn$Osf~2*sQS=N~N=&{p}%2_s}UY<%UyHV7AunfM9={R;q&eGKUd#bh^opDnA9F@oYXY&}s7$wDej}-=YIM8>Qq^!H4 zt1{?~{Dh$|5=U$6eNi%5i-hjox1Uu1g?o27m(^fH_ktQ7M^KK9W+Kkd>UMf6bt~xy zU^^_9*{RbPvC=t0+G0<8LaYt)B$MAKBOt$xL9e4 z2OaO=Vx?>ZfSG-adoAU z2B%tVTBd3{9-?+c|<4;O(Z{p`kJ*?2~L_oM(zaFj^D>BKh!z)Lm0 zSz~}Dp7cu=-`sb@Ngm(Kf6d(1h8W+>U7)Y=N>!eNQ-wT{;g{!7Xz=pHrU`kr55GK3 zX~^R)6!P>BzdVP~j}A7T*yR)Q6uv&}_Ol;-?_lK_cfF8j)$q$xlSX}YNqKe+zdZZX zsISJ!LO<>U!(LxCn3oQ=zBYVB$WuQ2@@&0x@bZk4_PlQR<;hP&o`Q)&Ki$JGPtzT` zevHI&41Zrdqa5EvduS z4FdMy$y3_-p~>pFsgLnJCC2TJa5*n zoDwhNbb5Gxl*8(L+ob%U*L3g>{3|^$J%&iO0r z!)f2RJc}|_8kwLmfoS9!Xw>mgW2!VxID{N|1{&f_%v5Qt0SzXH*Fa;Ca;*QyDXx!k z1{#BuBM&r~9Qg(sgOp?K0U^gk1C2q-kpmh`4xfRBki*XY<$9m}s|SBpGx?+%q%%uS z@H3HdPk9lfJ@1@WAHtw4Gig9q%yF$!HoLc{+!Q&Ytk_$P@hX@X*PqA09f<1;az<&@j;n4ilZi zg{hS*)wWjn*?#qPicOcxwx++gbuBjT{Vwq&qe*Rr+V8=or_|eUb$3Xbf%8VNxNe6Q z?xqFqsnUx@1idxGLvIb}Rg-+{6Y23cGd#MVx_miH1iekeLoWdN==)FZt%>yb2ZshL z-@e6yUf00%?0ldcbN1ZbuU-tf=$8z(`<~>me$XKsI2h_TpJRg0x#wa7mvN&>i8zdezcNg6Z$Ari%X;j;t3PRcn)FI_v**r!^?F>D_N?sE0nTrb z@HGkWskZSt=`(gE+Gu({;|SU$^Fe!uhn^yR#{NWl>G_QHXp_}YtNY;a(A$ObGM{lI zkzRT}BPe~wnStrqwLfWV`v>7O_71{l95{5#@3FBoD(ns6}y-r(KRZl!NgI44#Kw{Yl%{2l+9aW4~1EF8YShYmJm= z`0K7p+DGK<@mPy;Fuit49>ZUEjnY0g44y}_y4w%=W!)u%-FxPgdWop#WU!qnz)~9~ zduhyjNBiC>TQh+5CfV3-B@c+cqaNV5yN$)a^59R`}>ye`)l;^(+_wpJi)H3PJ0oW*1H(C~6P zD#J%>Eop_Nn4;)c zzJzTs@*P0@n$de|c<6Qa3VP2D554tLzLy53XJ;?6U9DZ4d>$-eSET^!M;(w&tq1Jc zntN_Ji?nwPfp)UCM7klr)b*QF>Z`cAe`lyWIWDX*xTRSh;}@LCe3@=>NxJ4~d$-{6 z&$Gv4r!2?2l80ez!N20dH+Yw)UydEJ9HS5F{#NQL36Es9e+KfW+OGzGiuLBCc>I#l z-TiFpu(|MqsnXpGACfAp@~Qr$YwBd*T=3(5wUP?P-tVHfSVc!J>3xJySk(IVI)s3?BssL*Dd8K$%;DfBfe25KL2UKca=qMRnqUZRsS}1 zJ3fRqnySs`4TA3WLD0L^lwR0AI=c&X)BUe1L!zDaQ5#X}( z0&ka!P*v_vznoP6oRluyU<;GTCZ{Y=K{#Qa9q>eFv9K_^Gn2?1$XN zSvjjmKt*%2CeF_C-H!H8n;DH83uky1SmdTM_Llr}b_@QChnIgcT^$Br_RVCJJsE7a z0XCUly+W^NfWzwiesi5Cqak&Z|4H#plaJZ!rrNe{N{3uGCms^~*T28~rC&Yq7vX2V z#`zD`uNF#r-x-*mojp*!QSyO~4_EF(;O3K@vnuh{_6!bB`T$>B^0j`_zXbrR%ZAVE z-b3R%Z&Lr}MR7>Z4OkC40axsshfkhIKDp9U!F@74r(ZIEcL3#Nw*L&*v(eurqd{X8 zDogE;PpQw~>V7uP_VhC;;g_7)IAn53m9}Qj$_J9R@=M-zI|T3U_n-GTS?<5b@h&#{ zW9LkZa+eRi-1)NH$KF5QUepKGcNNObZ06kmF|7f#Ymd3WBYUd5^)!2bNoPGc%SN#? z(vsn9n>%=%{c{G76G#ImSUq??wetp#)0Bn|x`L_mp*o=Y2~Iz)&cT({&rJL8i%a;r zlEJ|Tiw6gPATT)i>_Ni&2PuDditAy4y_0_1pM3v=c`hY9F*5fNnt0RX*OY{?TrpS(dfg;XIYKs)VkUaAb+cjaJys zDd}8TD*xt(`_*Qrl$)Kyq5H?OtZs+vi;k>qjuPCFJ1}ve@u{6|x&@DIpX^sV&$HzL z*>_qv;WCfw3TM_vum(UhtCQcnmeoNSt}7hk<{Sqvzso6Z8nZTW&|h;1XINbQSsT~` z*Y_pvM2DpRM{%=KT<;RsT0!hiakVWg^>?JtBOU#!!>+#Q#MrwjKoq2*eqv>ncyOYr zMQ&1_krnSB&pJ`gjBoU-zq2oAo%4w-e@Rx|LX@a9VWPvnjA72khYS2!wZO40ZY{47bq3_L#P+;axI(;H)bk;c*2WkOp|ppJ%OunXY%-Dz0@2|JPXHmv~&TWsbKnYc0cf z&Y^;W;Cl%K?!x~w@t|DfFBb^Q#5I!eoY^%>YCG1WF*1!i#{H{b{d+dXnT34ZVO-zs zn&~t+zOt0x8y@(xpcx%Ln(Nel_2-b+z12Wd`u#@@V#2vmo2H-LI9--~|GWL_e6$Dm z4-GVByxmB15NALHYTr?x9R->%8EDFProS@2hfN+2lXBMGD`>uHLG#u0XdVu}qkf0v z^!!7A*C6HWGSZxta=m9P6f`FeAI)+1zoSOcU%6*l(7eHIsQ4Kv%jtjU9rY=qxzd8> zPvULf&Zkg5*;!zJA77J+mk8}*O=bb|cmB0cy#rT_`TWj#e9TAhSR4Ot&TmBiy1$w8 zS0TUub#wj({W0o>}@o zRK7Dv8;rJG9O0%?tk^E-y->;CtCXv zbv~jkCkTolDd{E`ec7H~vu=FIKkyf`Zpx9r7WqcqctNKQ`MaJwtzL(FrJQ);KomM1 zO2mN)W({Dv2Y^Y{-(|err|tkf_a=kgOMmy6p((+KlJ8;h0MtLjc``W;jnCI1MgP#J zev$a>FyQdBWcbHn#G-kE--*=uI|2X864r@5DW~0mP8->W8f;!3ik@ljDYc-g81-ZiK-J+#IOfL;dOo@ZB-% z1pA(=`bafYCwg1a&fuHw1HtS7OwFAG!}I{A>#l)e6s)OQUo$YwIKUj9Zx2H{Cyr}0)v<>oS?hEW`9R{sN=+sUJRFByipm+BPwH5c8eLAOWLP^6X z*doAo)ujg81lXEj6$cpztgo=`dgqLf&Ms z)Hjfg9>N%89{im9Vy;s*caUT9)#mYi_w+2uFJic()& z+0%ML{R+{1z=GxqBTf7BWJ!KX-HT4hafP>uma2M7F!Hk3g~ZEkvc^lM7wWgEUDW`; z8&~&t3}xW`JYTIu{V={nlPbNYW~L!(D_L-OT!oKUYvfA0RZfu3AzdLmxO$z+r? zV{L-wh(o$94w0sB^$E3$42Z@&r83?x%{vp}7yCrNa1=vnOcZctUx*p}C zeo&iJ`VggKE9wk0%L*u9xMc4iB$(E`w*)auUY_ii28Ei`S zpS1=%MRYS&n&S#js1Fd$$rd#6I$LX_DcHUrRZD(!^9?7|n~7$b1_0Nt^b4_(a^AHfqyn@=sWvEt5>%THumS&RlgueH>SJ=9{7)VDDk# z)(tJl8aL>5L%E>YjkNB)KDCtE7JBoK;gv3?{{fP z;hx%~X}u+eeap>nDi}>aa3@;OtU>W8rdQ1QTae%VvN?Yz@;iQM&fkap&ERW{Efye;+U*X+xgN#U zPj$nOAOqn>CEngbg@f_PoB7%bTb{gTby4({tyzYJb( zuk_P~^#Y$a^j5HC($fa;4>f38UckfgYOZ^$A%g*{wh~4f2tqqm-w20q9!1xe* zTaJ5b_neL%D<+r>#HXq-{;`Yi63j%vY`tk^)rmD}^8vK3PQFin*0dF!%KJKRM4 zu1_AbCD=WH?ZFr^hqxZwJHtA7YT5u!UrCVuJMpb|3Vf z43>CD0DCAw?};(jYShh^0ud*D(O?@1G1ncSwP|`nITB*LJ%BlppnD@fl1a(H`$Cj8 zP9F($#sOwK?0z7aa=@%huoojQvU?wN--P*A4es5)v@RRzlwzw<#EMgmt*)7o8Z5Eh;>s6)6VaW$(K+oOymMe>ozJ6GbN!i3lQ|&kRBF^6gpX~k>*Of80>$)+$ z*x6ddWxhc)k6X}eiKj_@G5zdfD0z5SA^O}w#XK8r@RI3;Y=i8m0AtceaCJ|3OV@P( z-}q?e-}u0^I_aEO(kjPT_VYxmD1laM>a@rPNUl{2@qKCZTkcr~S~4zqI9|8O$|`B@ z!MgxPvvc@px)DEpkmUTn1)kLdX74rb;)SHJA5>YM4#=RVQy2jEnqNl6sgaA zyaQcM^?m#B(cA)`@;9Qn$UsxZkiMCooa^EL{!KI=u%P*LJk4b7bRYDXPOP(Mka*dU zm+X2A-rc7f>)eVR7jXj~>K)(MA7B&^% zo}3?zH?gGtRPxw5?ganN(AMNUjA2V!Jeq1b_FaisB9-GO7CgQXCwO8pQs%J>u}Sv5 zw^uB9e96FLrssacf7JEZ#>I2-jLKMzeR(Tz<>Pecy|lB5+78WGx9&QnPNHY{5o{IG zhM%2Mr&5NTyF~bsfMoijaTe8kY^n4GEI@k^41Pf? zpzc9#AR#^(QdRL%f*A^xXwiFX%RoP&sD zCW2fnoHm24;|T(G*Yl^;nF5y0-{xlJ0XUqM0rA{iR~>-s037LA)0J2-R)nmLc)lOH z+$o-m7@JWKK4-)t#)g;LLE!8Mehun})~gbIix^$dxr2S3uTuUk%?cvuW{TR;XYOq6 zgzA;j-pW3?r^q*pZ7FJDO9ki|{p{i{rOYJ$g-mY05pxY3$Z%bRyq>=}Aswk39{FiEKj(yn9?N!PUK&!?JN~=|is7 z?l5aQA_T!?&Vg>LFA(uXY424`whC_bylfPEo4EEQk3$Xs*9f?AImzkKy2f=$;HW&S z0O!8sBrnhY##j9qEQ6c4Tvr7!@c{6q5foytK8tt;jR3Divnl+j6U@fgya6V z(TD2qF3PbNw8o;PqemdQ4}-?uvOaBGM0pA-V~-(^+AXbXj3JL?rT0#>ewSb;B7Yn5 zGbkTF%JU;H19Hhcvd=u^RhRdvVcfgNaaox@u}K?RT&8Ii&k4Q?@EZWXnBWUc@b?+v ziDwt!`vH%oK(l)<^KS;9VUnqn91hCO zWXdtYn`Al!_-KOe&LFSIMt3CBK|HU*b7o`XOgv07Qm^l~I)J4LJ#-O3lvP0Agp^a? zr{048--cFQT~;-{b6w0XF1OWwzgeC;*`7x0bEaBFKWqG^J^E_mpNH|*z6E_M#rZAy zmtaTi1GM$E)8UFZvKraV1d<`~&3dKBxtarpES}ErxIXHnt%scs=UjeGRxj)D4mPpg zn|^@r6+=||ksVRJ7a;F)%8>Og`qE_dcUGisgTd}&CRrlS#h*Jxvg9LgFg8dyR1W{v zqwKtSC17DsE3v&MZpBHlXI4T^5ok0b)qqYu>eNt|Sks`L3zYBtG3q99M=<7VG(X*f zRQ|1_+PVfi#=h(e-wu(0Nw$lh{VNc^Eh!i#_>p1F{91MohLdj&HV(Ux?X%h8rv&BjLMrF8>`fSa(kMjvCAWaYB+ zMiR@5F{T&z8-Snx*h^8?HfUjsd(fKBdjL( ztz31n$91<$Xw>MCi#<--#cpA%o#01vmz_U2s*bheN9$~?7K4G^9H*dbWLNI7+LK;k zldDj@(Hx}dCr4GMuv-g#`JIo7q8X1IL(%+}+1xhrFlP7eJj%ae2tHI^AM%QjcMYyo z#}>ST&SFb_Raxe`#>vVsY5|hHCBWmesk~j_eQ4KF^%4rs+sUxmk{eFO<5g*-N`b&8 zkjg|IZhz@0TjOJ(tG8pSOFFtSv4_PmlUBzF6?^b`bQADQ5BbOr@H+VN(Sg^=t+hah zk0e=l06+H2qbkkm4YKM!mVC}`;3uBGFw;Y;?-H#!;BkINWE(Y8>B$HAaTvRIyej-C z^sUD@jsBq0Lq0bM*lmEV!99U1ilTqxc5cA>bP&q3{C*XSkKhCcG(3tJ-77`bG z?ywNJn4R22r85=-IwQNGImqUH&>6K8=4;U(JnS;+%$}oZK_BX117qG3|fzO zRBiL)A1iw-rxQ!C5ywmec5W3`c zXzP62f@{Fz+G{NhYvVKr+4I3(^}pi(3?A; z&*!~pDPlh?Mi*kIGwpds^^t%?yAEkH&ig9I)m;l*@WggYrXPCViF5$8+4CrWj)^UX z^7iGE`rBC5rP9@G`L7|mXFw8Efb4^y3 zu1im;H<`);X;RclK$K(X^X> zlt=waH}d4U_moHT_*DsTWXpSycLA>SBe+hae&o^ggnp8j-MEbW5tYaHIo^id{d`;| zd;-F)7|CkGno<^9s0REI3GY6_-tXDL6MtHRvwgkwIx2czfajf{NAm#Rt= z-AG>GPsi1r6UWc;dlYLxZ`WsaKfT(ipJz_=NSd*G#sfEje>Qa0ho3);00+W;$#O7Vr?8 z#5pr{DpV#~bG7}aC)5t&6S3tJY~!D}`7M*%T7S+x?4h*+li89VvXCtuTqX9@ZMT)h z=X~5krznt}erOJz{{bJ#ps=$oX(Z9eGBWs zbI*TS`zexXJ5mPdoyJv{31JtZKH|9%>EIKB=aJ+*-LSn4fVc9EcKV@yfXbgY_n7L& zm44*ADDC*7J_gdt>cg^J?R`>(M;;(WG#V)Z$BKV49TPvQ>wS-%V&CU-E4`wP>M^f+ z!<3&4%&tsDphNsJj^eB*=!EoZ8f($H&VRD7IT4Gj1=^vv``KDO8+^Z$-d9-60ZFHu zkUD^WKCbkmyzNLaeuYT8kv2olhbY5JPMQKr%?ByUsWzXw6u96Hy(-;TE z8e_u?WH?Uazoqo03+?SPldlOR4*cqr`c*;8q(>(WbyED!3$TsmMUqt0?%I6ZtkP7k$s)&ZI9U`Q@nBh&S)kSjmAT-yQP2zV>G zh#v9W@Qk2Wn4F#hpA!JQ6}=qLqhF$JP~A`+)>a;iUze7!mY?7Plx7YLm&_jgph2`U z&pWN2`*N0N%?^ zXRvyhjPpi_eGK`?DniP?rcZs?_?+## z!O9%kJxd#J#8`{^v2}p$*I*OAX#yYP`~p09;#dp4gq;p3pJY6<=rsRMVndb}pWb2X zO$(4;>o~67AJ0#p2eLSvB|nSPrT7}wwV<`ud0hRJ9nFd?KP??i>;#KF4Hmi6ZNC8{ z>oybb0%__+KRX>r*x*ISwY?w7VBJ`QEaOYbV0U1@LaH)EKo_s~+0TwH`30)81GBaB zbiC-ewX@F%Inh5zoJdKZ`e6b<7eVh+2bv#9z3c#=iwOt6WHRma_o*KuK-Rb2#TG2u z;Yj)$FT|XcaPUh8+cOB*><^?)cO2|IRT*|Dsl!$cg6@Gq(9L=GSW-KY`aPlQ>xh%- zH*?H!ZNFJE*zF%o9d_U4skg0;%TkBk3mN1-vt;r{VGqe*y|6)QTm9bS{QK8fkHX$- z8{TN!@sYSqY2eDb%EP{sRDDHsdU9VO>8`=vk_%BT`q{}ZVLPxN<~$CX6gCrdQxtmRWOEBWJuM?U> z5nlA>K5gEt#k{-=1O+YFYFoDjf)43xChp_y4Xe*Gu|SP2H7fDc>*i)lFDU4d{nB|g zM9)G`KDn6JxIDemv%+FLTVgw&mAcDByGloQIgpXcZzwb7?5XR{3sjO}C+NsH;%a9# zniOUyjck}_t0&pc{e^wn8EjU1!dKl)<9@q#CiPJTn(62xHf?D7knQvg=p>7M775)c zQ`4~%`*tC9kPo`)=)gB+XgZMm_k&Ie%E*1CunGKYS9h}AY)0Kh1fHcm&}Tmrm;9Dl zAAD@bOUk$VFA(#`i-YiVY&!*ny&jdCNSByTGuBz|JmZ-R7ZB+b8H-wt-^Q zV&XoR5*7?ejPeY4UX5&XdFcOdi|41ua}Ym55iHo@Qw53KK*O$Vl2;vgJsQu;xMsqK zRnHc@==DS*Gy8r~_AML0>oLJgulL1a>R{TX0E+R{(ty4JAc9id7T* zI@B2(Yv^+|FuhB6uYgI$dq*}u+e+4D_8nX^$?%jT$M_jXQs!9$<4Ku2-aE!)+i@}* zNBH(KOB#*6$Fx26nt!2k2V^gvoW_xVA4^=*3qyv-)ri>pIa%MEAO~w-z)WTCbmqp>L8)k_ z5C6}GM&&rRB9W@So@)f%n#0HV-hQk37~Waj%g)C_Z(tf8Rd{GAf8wBNAf3p1ai2J* zowK_zg*D^SS47Sev1G`~_CRvvq;Upmmqn+K@%^X_AL?t7%gy!mWp?dK*5{$)$9SwW z&Nh~^wdR$EmTgBzt53vS8bp0sjb&)Yo7l5snY68)NPoBovFJ{$zF$NWCs!Sr~Bi_3i_Cp)|lDv zmzv>OoK=9Zm^}H$815k#V#`~~iACl~-%rST+Xp*u1Wkjz%6g;E7eUd3uQwm~7YLeq zACY>DNVdTY$TpaGgv856KV3=uA%3GQ1a<(})tS`W+P@|D57J*AMqHNd?GgT#Hq8o8 ziP0KN(`{c*l12yKU#Ck$%G~vjZQZ4+)y{>vzsInOr#DcYubSEYhwc=+8C*&TQBIC0T`NfOydz=a0mG zVnVT7yF~Ry{!%%}`v$kazmo4W$C}xBqiBT%PH&)#y?lP4Q(dgW*xq(di?IdP`Oe7l zSuL@%AKS@#P|i7?)K5t{Hb9P7ArI-v)Yn3em2Al%&9Kd*8M``=ec_&e9%J7twWGa^ ztunQW{#U{U(h5|2J<76rvc)4ukEx4rrF!CfuQgvbU%F4>+t6y*9Syf^+aS@T)B(C)T*(jf z;Y7)R?`NdlY(8ZgL&-krE_M<$hW2r7w+zeT<1*}JlRVDA_bwsX%;Pl9gPagdUW>}n ztDLwDdmdhprSj+gT^|pxcGAjkcew}OZKcDu?b=R}z}*WSEXF;_r}d#ev=wdGQ|YN@ zt3l&uUCHQf!5DZr=}Lc_2m5?R$Q$p^**PHbnyk($#Q4V6eq`IOLHt6t57H<7?0hp^ z1@p~Y{&Grv0Qc@7d^7WDhVPHvW>{;DwQQ-wS@;}svtb&|h zAev1Enryuu|H3x)A~@>HR!7yqCHdF<>6rR;TzOed>-hXm+Uu0)x{E2o%-)C?PXJ=) z4cXZb@uC!f{gAGSpO|MdhmZ`f=VW?#1%E!qzpV%#62ql(p#XWBjZhv?GkB@ps_9 zSo-w<@FG+gy`PkQ@dKQUxiQ)4jeH-;6GdFDNYK>!Ht%8~mDa)8+JCm7lYz182Z;`T zc05VX#PbaLC;c2u6dz$JrUcg9?D1t=qyDXo80mVDah*0nDQ*N}Cr_@BXB+5!M9^F9 zY{rV;S^=3Z;?U+JC~g}oa@ljaHooqr|kKl07w=S3weN24gm`rPB{ zN%F({WjWY5D(>B06W>4nC)@qwWgaJ{C8oGEjV*}JI;4yTPN?q@pZs_1_QMASpI2=8 z!1-ewWXUI49q&2QtE~Yg-B3NQK^}{V%kf;2bH|H-L%3)DcGPh8p+3fTU1R+A9S+WZ z$@ss0436ra=m!3Al$`^o+)d+4XL;s`%ul$Sw0kAP8G@IzDR*QhyA>Tw9GsbyhVVpfn&^DJW*#KTP> zPzKr97NiZ}^GmdGWwG}DM~s(z&VvaW6Dcg3)Ry=@g z*pX_YTkOFRk*3@-kCl3Gj>=jKV47~*>W$hA)kovMj;gD0jc;%G7?9Rxm^O_6G5f9M zCeQ4o5$Kyh=Y~W&*|fe)zSgS50!8f~=EpVxehcuZ52UujTLzBZMLdZ{^JIg&khTMc ze0#i%^Oyvc>~E=oGTJXdPatgt9*xn7Hjmd4?P5lI9J2FJCtZM%=Lxd5&fgQ4ac3%Q z7I1q3cd4Mo4+bC!xqoaF&*#u{*dL?A3o}Fbh!W2{_L`69mUHUlr#A!l0BB!CxcHGS zXid&xhDmH@G%1{fq4D6jP1l+@drtZVJ z9&4F+#x({dh)j^DDmT>OOp#X+r*D$E3G|ylKOpFn9NB1GqEEt#6q1Y;vu{)ncK)Fn&j@!D@UDlhNS`KuNq&b0 zJLb5O^LdMGxt2c$a%8 zu6TaB4=vB?c3K{U9`I1#+X?&~u3mPQgr&T|ceuW7B`{8>7;T*ox>=mLfQRMr^>K6K)&6<5%sv4#(}bYf($x&ZeiuV@c5va;_r*n7Z(?QNRV zqA>#bqzG^)X7p<7Dvj~D@~k5cXJz-HX$7Z ze&e~lYI{7s-tQv_?Yh)Tm6eu zI)i>Ao@dZ={HTqsLEZ-B&7ch6U_1ysiyWk_Kzu1oaJ;;w5Y06mPLOoO{?ILmCHq|HDTt8^Aei`@F#(YRhR)DK)Xo%ev9-|4cm)k_a?^Lp(9$~ja&B7IiY+^D&EDa>J~y z^2i@v(5t%Q>FRYw-$Lj!*HsYsHNY*ruvfj>4wt?akV(GOJSedEtPhiC8))X_^s1ep zZ)YxHZ6iAin?A%R)0Y964ta2XUJ3AvpXm@5bdKk^5Mkhd0jQz?VtfjEr`p)7PCw`XO{UIG_8icSfbN#OUUdS=0QQSS`&@(koycb~QslQHpZeoX$Ulhu zV?u}en7V_>LPrg06Q^Y z_x_l^)e}SBcp)#;1y#*qdR~LPB0OJ%=eNSP?Cfz#RuOr`{T7ym#$3*x){DQih)-^w zGQMb~&)gD~x$JKndl_EM?(N6rxe$qbW(?o^Ap0kM(>H`r0Nb&=#MusI;04wjYwNS=P$5TgFfTh zsOxTOb%{PBz{X5lfEF0v%hyBWej?Hiq$u*1;Yu`h8*)QpiF8z`t9&X8QekN?-&X(v9PL&H z%F#+#J1GR*2BccR{t8#(YaH*PKfIqU8FM0b5cfu`*h9G8NJoH6w!vivGqigq?F{;Y z7rUzxVZRdoFCVMWd0OMjdbM*!E#{?u=K~hzB-R>N0e>IxW&5Y|<(Qo!|1k2KgkLh* z2%X4V>o8X(icp8N1$cSo7@OJS`&i&C(aaH^V!EP$e&REjLj`-t%TDo#{EA-nGJ76D z=RYjW4K2Y78Owp6QQ52QhZOC`84wPZ`goeB6#(Wp1MqMywwf=^^f+gk)l&_cyFhc- z%wAQAr>W1&OS1CWX@%xy;%u=l4jUuoqTMxB^{W2@56gB<1GDuO1Q7i5TV#{N$Ob8z zrzx|Zq{J?-MIj6PQR5VZ|}RwD)G^zywU`ZpPANwM)LQ-%7^PNcPf z{Q+;@ho1zVc?L1hLiAFQzx94MK+h~2-xhuwO9RsJwLzY`L<#Mc4G4& zdkuhQIoj3cdA*5yFHjCTL()qX5Z5(RG&dVvk2V(v4bt6u(Afbx6}UIlAr4c=HUzEK z%Geq6I|17R*wq;vmd&NbT-4QQ{7zO0gFg)TqS{{d?}9diFB9JtDvOWr`2j=olrw-2 z3HXHZ2VN`0bu8tc74aE%wne_4Y+Jb-u`z1rY&?Luxs>JUQO}pWQ2Zi0D-RQbV8S_r zc69w9MRd2;>3yQG+48Id&S2Jl1a}WO*#JcjXiF%P zBjhJW7tD6(Jk27&OgF&bvr9A%f!mRgeEb#wY#m^0%&_{HCCrh?$HoA^9`IKYJWQAS zF&yA#JI{7b8pYn;}w zHQ`>+%m%%^3wyP9Cp0egJC_Snu;0HxxJ+M*dincg^r>NVsrWt~;wJb92mclT#*efK zFw1eJa>n}ypA!+5I0r#%FggL+fpidfOYO=_@sFqB#1qSlPgYyx&h)@yO|TGFe#+LU z9P-z8cH)IxRPG&3z3P*=!p{B9wX{OdU(IKD_PC@E!kyTRcoE@3AEF&#O%)=_nw6A$ zX6iOzD+{$bNpF&%x7yjtVhMKg7YUp~$goJ@80#d4ep~BrX7D4>p&Rx|Z891=x?Vvy z2)a~GL)oIDJ=o&P_FN6zg#y;`TWxDA4*sji(k%QSA_$Ybys(EV26trB=b)>U999RG6t7BD#khhJj+M(V8xwC=XA z&!FoZt9hLL`mAj5%0oK1O!!^g+sPnd3zx&D07t)MuoF{&CEbt=UceqDqE>CGG%MF? z-dO6v8V!q>3<h-03#&`DmQH328I<%RO_NP5Ce~(R^*0#|yYUNP!iIL5g~yelSR< zfrwxS(L0Rf>*(d*mS=4Q6W3+|GTpO)!}1sajk6j7dy!qe(l_6qw`dP&kwxHB1H8b> zUiH_wx6~a*`<7>z2>gw}KM4GdcK9^sF1Fy0l{E#x*-!lM6Y+kmwlK~coOSv;EG+uV zSUuts&WGF&yT$=88}os6z>5jKLg#pmCK?CmlU)=7?jYc3EX4f{jG>WhkoeO(jD9?4 zb8n_=njv8vE{3QSe8R8`--g9H9Fa$~)&sBV{$B0uZK_AwiX}iWA7OX&+LV2-mtr2< z06!W3EA`~}sz8;G(R%Q#8_%kY^A9<9S`r+m``6$T-UVTosH@MqEfYTR zIq-4Q`#p@L#kz=@7;9L)-$R{~&ej1x`e3j6ZQPSih3?6aF`zSyDVy|v2yZv=4gqhk z84nFW!z;y7Kkc6YV9x-rb2Z|-!p4ohmzeHN7~54)7Zf+zD`wx04?y2L*5F&vi0K<_ zT`zY@R@7DDwlG%yjdps6k^8A$^}~>d<71vE>&a=XC;r{FE`omqe*b(6_^Ic_O>%Pp zLcc`cy#{$?$No)tr*biFpl?(EQAXcM_gPi=bWfXQajbJ)vqxA!v%cF-K9Y@6_fx{Q zWgA+=R~=Eklg%B$^LAWGeq;S%KIm*fZK2=ShJ(bA`imG+;nTf*zbNjB4x7&)?=4{q zCS6cm_I2yHtX&yo-?a0G`nw^@^C9g(-i5fzpB)XUhw^hz^K-ITJ;6R$;6P&k-}P*1 zAh?|x8UO8m=6uEd^!dsGL*5bOZ9ty#ln!_1$LA|ulxOkV@%{6a7m)sh)QdE#>wF~_ zX&lm2q%x#xq=iVUkk%n>MtT})FVZ0-`NE?v0AU%ilGScsn z4j}y-=?qfV56@RFM=C&~UnQ>9NcSK$Aw`kyLwX2_e(Q1FjIPLDH zY1EG(57OmGS0fc5-HcR;G#9BBsS&9KX*tpZNNbTEMS2|R38W{Ho<{mH(r%PJ#gm!ps_M)D#}M55pS)!G05>TIr35@~G@m)Ey6M#5PAQ_>c$kA>%jm)1v{ zTHC_6E6T4Bzlwxo;gYuISaU;tWL~%-*4)~HQF3WZEZmlutz6|*47qoiB&g2)W(WNMR|{MtLochrEP7kZHhA5iT`J{HZF^ZauFxWBcX~L z8{0@QWf|qnTiG59FRgA~8ZK@pndnY=%~{ccverkM?=zM94Ln1sBdrZf!i|A&LtA($ zWTCY$hWyfwhNk+KMPcPFV{S!@27TID(H@vyGOr3Hn7%R=F6~g1YhCznxrw1N$yL=_ zkMc8}pd_`fS<51^W2%MFgu_wg^X}?MdvOFM#H^d{sY;YD&>a*uzdo`otUN~lff)bqS@-uxE2b*ok;d zu(357Zc)6rYme1~-gQbS(jE>kp*UlRsvT=;L+!D)#${1uF1u@PXpKaarAlZ8%ruO7 zg%S!a3dfe#H@DQcEoxU*;QyryLJiB>LQCtR?bY}{8VZG5mN&P-n3YfP{}(Q8Z3On0 z@Lzj47OIa%L$Q@nf77HZ87D~N~j?W3s;m6Izr*Lww6}Rpd6uvQJ7U2x%c3H zStJszk2NV@bA*~(7bwa%acytll%8^iT3Z-_A3H-)o_W+s>^Scm-SrDv!KcKHhRFDo z;;ywBeUKZ?kJZaecSE>2f{3x3L)`9O*g(x#QSNjvTo_r_-h_z0dtsy%&0&FiA=BnU z_rmD1Sc9^NUE7raW4%oZD>N<`!2g%8da3OpV%E) zz-x4eS|LAhfbJK@s7Wh-rTYksN_mU!Vy*BS|K@j~{x-i`7EzASUB|+Fq!D=bc@wbk9N; z^?$$rNDVl;=O}qddp6He+`H!dN9y1|wa{l!aD-9uL(mPsx*Wgqsp}DUIyha)!2d_W zl?tsF*oEsAxE=^kSH6L(57)0#{^UyKL0pe`DwGmjgU$*T8$X~_C^t9|->;mm)FI#h zTA6|YmEu3QLb(w6ohQnbllZ^$B)@JyRnGPzbmID9^R~MxluICZC-~Om|LzeL$^sYU?f0x z#fksr4+B4xf8MMNQT`*d{K|feJ2r==Gdb#TrM;y-toHjR(S480Q8$X012Jy~=g+KA zenxQTS11!9SKVlSRn8OFpO-6R4SXr?;Ze zSC+u@0gv=7fBgAvm&oVQ+lm#oH#rXfLlfll_4A7{o0Hf)Ch1iAZkEpv++M6)#!Z6( zar!^1m(P{jV#PN;5&wxdET9L@6Gjv2FW^Oj7QFsZ;h_1f#M z^?9#Z8e7&9zG+doCEV8BfPd>Te!r=CO5s%RHH*C0%$e-HrajhpQv;zW?xoF*&6DdF zEx5KJ8dYw)p>{@dtbEymTA8invaLzH^v(=-W9za7h+H_`BQOxhg=RgNGCDIxL|R)G zdD$QK#5tlJ5sjFa{&9hyGDf+$)2+Bdp@xo*Nt2YZuV>DL_gYZj-rPWQOUzV@FpY1U z=mqc%H_&Y;)`n?pJIlUVnfjAiHSndc_8OnG6JpmoL*WtH!?IU z6jfYN%26`tv1@7Q&e`)~(}WC@CL6g|G|Y=lnq=Tx(NH?C9)C(FO%4^)tSQul>3>^E zL20mI?#y}Dg%n!RMBeV)X^TfFqlnL_r|%h|D4qw52+A-p4HT3vSLlEi#_HToRv($l zD3s~;kFRqpj`Q6A4vtV<%O^vKtkAp=o7;sN>ZA1y&9Rlr`O5j(u9dX9IU8Ex4{65d zx~~MHQ=U+4xKT%P-A@QgrkB{v`y#O!O*t2|L|fYxS6v709Umh#H`d4MqnHPyrY|6` zM0vbULzs>&!qnUqBTV;&Bwt2Oh=M4bC7tynArX{VCBM{d@e){8}b5 zZhL}QXU?Ph`QKspAN(NhYu2&*J$S+Hxct;(7vbO6gxz7AU7MLM@$8ijHZ9!ou} zPK&|y8imru$_P02Y$VQs)zQUF_=iDz=HTsHlYY5EHrxo(i;4VF6hOK7<0y;k(`iXP z={gdd!Wqm$V8TRyH)y{s#TWFyke=jb1}Yeghu9O4cmYWHT03z4S9%4RWFUeXco@_l zB@V2PSq%^y#8I?*{(5?hNMkw~u<8YycjH-I0+{A@u2{wOWSXj@B1tN!nQz}ZLb(S! z2^*2P;)o4Gs0fQGuJ5H)8nRkWp>*zI*e&Yj=OF)UV!<@GlA4I7zQ*QuI2LewIxTLK zd|)SNpgq^D-%n;~v8mO30gEk%4kYEGMx3n#5Ks0k%M`*I* z`c-<&3Q2e5r*vASiyIUNM)#wUbgrXj&4qa|E0^Gk>ri@3CmWd(yC_(7TClsgmAccn z(&I7}ToC${T~2EoYO52L@E0)pyJV(t7H@+^8ZK>;6_1C(qZkViXCQGC&!zG+%zX~UYblNO-UeCx|nhQ5W2Yn zBvv}knOYZTPlb&%FGVQbb#a<}sj!R$__3m{2eZv)h6?dQQ(U~P1uq_4m#4{Y68R`e zrj*o6UdoO266YiFLNd*M03)7;Rs_FYR}rgpx90m8zW)q4wViU44;wF|%U5IC(f}y-U*LLSqJDy_jSb zgp@w8cn^v9c%Pv$DY03)46iCtn3nX}a1z{gE>`Zso1tY$hMraPo!kL@a6X*6t1Uf- zUId>%n0YCeKa7}*YgKw2Ffm=OIL2mUHyaWwhVmgs@DphhbR^w!45rGtJULV@LO~nB=4l&N zMt$u}n;&&cg4u`*6y*a*yj-l2GfQQ$2VSlhhQdd%iGanbcs2$O-Dz`JETSilS3vx4 z8Ut1Y>YR-F>Pef+9P%9wAL0QwB5_qcPR@fX=y=+M5ha@1tSwk+!+5AYhA5Fx+SkDB zDUIRLFx;a-X{XcX3i1UZS^tPKUxZ|^YXc88Z62gD!J-M|J2V!gH(m_y1ICb<;=0C} zR#(EpS&mo0V83L-V_IYG#@x^~B|WP3^Qab3?capFjtMv)aVnCoC52O;l{R&>K~DQ& z(0x>+&4!?yc1_x}1?9hj>S2vC8>?~3i_@meS^`ta&3FYM)M7-_-Bvu+h_yCbAi1H`Xq}<#AE%Z^7{52@{NUB^DV;Jej z)6)c_{032;@F9%w9U+Jh?t5g??(%Fj^aL?*BS81 z948B;g?%vdp;FBsG8@ii0XQs;<02R=8X5zPHkihq1MS~v^z{)3FU7q>V=|!46^+NM zKO}At%;qjXM2>1*dQ7j6V&g}Mc`J;h9tY)bXw-QNdKUvW*O$|yJ~b`~&Q$j<$Y)$B z3&33qWAW{wvCsnqAy8keabV%HF^t1=L*u}jrED|1!QhV?6Do$W%`kSq85%pi&G@cT z6h9KLb3>b9Z2mT6Hr%Ek1Bb80aZ$M1`i90pwwYs~eNMhCg`v%`Qk*{`t!_&i*nn*& z3a!G2^Msa@`)pT5q^&d#wpqVLfN5GWumEm9{+i3)B z9x}5sHde;8Yluws*7_Q_^~G~EG+D;%#i29fZMJd(cJCn>6v+ovjLqvqWFuSdc5wJb zJQqXrWvq@4ja7;*Ni-z?L`4ZA@lxMN4T*PE&m#QfI`^EkJjoR8y(a6Xwt#+@#)B1? z{wQM@k4uNdBgN8kuW`*aI8XygFRYMZ;`<@Pyz4_;j4p_XDA$3l%6bw z(bgy*dtlOk@wM0ifW!)_jJcU!Q{R>zT|_COk@^ODOKtEvD?ogmWS~2MQQY+~eoKaj zpL9O|1hPw z+@wdE?!%sTB!l16waa}0E8mMlz+!k!|BIJi5kqatdaBNQu(1i39x+ z(ye^|q_degFmZneQQy?W)r1_yW)QTJR`kiVq8!nR^n{*N5KjmlylAe~Qx&BJiEB&m z4D{A&kU?>+Pm@2b&esPQT8H}^a6W9oUGHy+I}h(Et_|riXNwAXgivEVbDE;ug~W@k zvm4775`Tj@rLmHrfY52-Engt*cp5BTBsT0_0dq2%ij~X1av_Fl-%GDh1??;9qos3c zYUs!3E3Mm!QxM8vM5-H^Oa~Rx~$(vLHNHV zaeYL<#;D2|#_NqC@#3*F{sLPCqw{Z&#-xSL1fOF=;=>vSnWiHQ4$CwydduLY8JUr0 z!(f_2Fq`ps3H1M{@t`q*(AczW)J{&CJf^fUCPtM_z7dN+k$C$vsEjchKQu-LJ9r#C zzNhiglx8f|+NjKv#HY8p|LoN1_7yMkh+$OVHjNlNs#7cYu%%0X58Y+=7d}4SQ z8tU+ZTd?2($+`|1)9(!*Q(lD+f!||hp2`@;?z!P%Cu^_|j9h+MKtl~a=fH#RbeT;v zw8NHGQ#Ce&<4TRIp&A+2zYZf;LtVZGhJ6}S&98~}$T+_}%$#|J;1mbLm4xcyP%XsFe1o`+gJKV6Pf^>%4dsW(qol-WqUn1)JaOufU$)KI5S zgWro9&%`=qjISMD#=J_eE>V;lkW5v|Sl%*xEM<*;8O*+|G1O~xZ2K}C+=o*J=A_AU zDknhuhW02Jp1A`!D-Ma5OrK1QxoH$kXF-}=rpcXMJRH|aoLazTrN)WuMd3^y94ZqT zU1n^42NrK?Y_tN2=rSwSreR_T9Ws{XrD*U-2|6@|ml^Z#4wX5Y184g)uzXo#ON~J& z^o60a;QN1B*~XPA%JoQzWizn*>#(pBy4(O>-?ib#U3bQZM?#!e(+Wf4@D;;Gcm$$K zOP5!Nu+MQ`Y_e}*Yuk$YwnlBIX((2|NV#SPM*K)u^o+(ql$crYNwF5+8}+-{Fz$?2 z&#=ifQwW7Po}Ez7L7+cKp{OPdVF%_EnNV@N_$l1OPPw`IX2rEDJ>}6h__$L9;uHwo zh8EW2s8@an%ffPu_>rzSdYQH2E}RL)S*pC7WTzWYna&TI#5PbLitE`Wmc}JLkB<|&$xN0z_#1)>7hQ%)!1Q3 zqp9o2;b-zFS5(q}qo;yc9!^QcF+ew86sM#D%EjDl_PkK4ha{B-aV!d`ehWlL?V?3J zbI%)zvsgf>8Oue?^qU7wznM8vo%pCu@Pi$Uef=3+u8Ol|$GFP;5S?#=^QXd#!fi#k z-mEyry)qj%{9$%j(zst;J@Xv&a@a4zfZDKSomp{xORQ)?Yinepmo*NcjW4~m3TMh3 zL$q%EjPGML^h6JaY2(XjVe}<9?w6e-#1F0-nA8n7ET%IBA**sbWFZ z0$*uq!}0})lXXJmlSnGkxjVp8xwsy?PsYzliXIWz(;)p#NF4i`P<528>Ap4;;)}as z45SxZJ>U?MFjK?rvxcY!Mrklwm?X9EFyvQ}MnHsff0n8i2%WS**f#;QM8h^~!J&+v zPYi+=z$(UnZ0K})!OSwj=sYyAjleV*vOK+^ag&>t(~`8h*I z6t?Ga7r=ihQO@lo^W`iyTd}}U4Ex^!{+>h`^$M|?M7t!%f6au#H$aAzv5=ez#le-) zwZP9Yc>3|MJ4J!_rx0?q9w)km8pE;r=199T{-#kQt~?J4$gA1}GS|_4UKL|Cc~Z1F z+z?*Tj1EKZRANjQUm%wINH}Hm-MBBX09Eql5J`t}RdWl@#*Xi`9ar9wZ`qwYt_+<7 z);T}LADHL@oLASz&%`$zF~K^1$CV9N%mF0pN|pRJ9K7DTG*sU}C*vnFTQrLxi7<9Z zg;4qh%N8kDO`b}JgGE=eb3Tg5R)r4`r_b4wMC%wm+cADFlqE?Nv4iZEwKU(01|N;# z#JXsdj@7&22KpNcFULvtH(xC%Cj+}#89kS8?8Tudx{MW73nrD4tH8ixs=aijg%YPC z2!G?#xmC=bCd8WL)5QY0Nd=AJh4nc49=f6L5zrwXIKkt3{HgYZFanz%qF1WFTs=bh zeo#@a2~||Ef36A5;Wwk`k^aY^mv}wkkiUi{P=i(O@Rd5Tn2c*FD-$mc29#4nO8zHC zC=cE$ibMaf;?T{g_&>^csnH@Fo`}js3B=9t2|bllWUpSilvCMRBI_O@!Ju${1BKJg zsQ5ov{VqksnKNvy5RH`wZPj_s$LGP@#L*gV)AsU>pOfKXwdSWuoT`oESQ;bt*|790 zsxE^6hFxEAtGLk(oZTtPw0!^N^G4wE>GLrXo0k8`6!za|X0yv@TiIp(lkD=jm)Pae zx7p?M=V4kfEq{ZLUA{1vT^?J)E?@j8yKL-YmoL4>E{`j>5yF?RW0y_0vdiYB?DDnG zvda_SW0x(zXO}0>u*}CG4`jfnAd)g8F!6P3iC4@ z*Rbokj#=zF%F)KIxek)2F#j?KX{9j#atEoUFn^4L6jGQ!Uf^9R@UCKb`G%80`Prk2 zW8^=_-k~THA4YFH@+}&8k6wpCcMe8#e1JFda3{K#(VLKYJuTH9*~`1^gi}W4O6<8f z7^6XX6VRhJLef!N@JG2Je6KR{m?hri@VyvAH_V$HVxze$<^z#%1Y#tnj67wH7>OAX zYvU1bq@c%0r|fME<;F1=T9^ni4KU^*fG3E+cH+$bTnD8nd180%$WhjW$N zB#E@npG#r5L`7jtJ4Y5-6S)o#F=i+B<0*{IVExvx(*yu3HDPf2Y_1!wU)s8Gq4ME7 z*F%*#wCOn%Q^vkTC*_VN9Z$R3_Y0_J+SQZZV3*0~)Y5;~jc1oB|7Mq|7u`*n({5&$ z!bR+ILx4TIaVfhLZDg05o@bX^e$PPsPq54M-!jrA$Jixs5d$eLV3!#V22%cEcBz=l zE|vGQOH~a6ne`yM%>E9$%-PQ_x4y+d=KcZ~9H<vGc8ElgXrDG9}AXEZu~lEzQ-(FtuS z(cyDc6l(@B9o-=GXN?#u8Yi|qRbosiMl0`b1c8vanwVh455yU;GU1~sk>$q|ML=tX zq<3y7^rhTF>$N6yrbI+8&!QUq5y(6zNsS_DmFYKOq05AIAeDkm@;DINhVMkhWgu~b zEWwbsqu$VJ*+;J?h~t!#nK~P9o6uqTv#D;HG%m{MF=(FCCd^n(Rx@qF4fJoIEgWlV zYh9r;S0UwPAN%%1TfNr#Lxsp#j>U> zB#=fLU_sB`us7@?cCq&pJ+Yo$RPHRCX9pYhdLIAZd-KiLcDAG}XZd?Ke7l?P{a%?q z^X5$$od8x$*iL)kJ%n8pe$plI;`K23!$PL`X)R^=A;?|*due&jPv+= zx^V+P{l<@{r{ed3@hE@KFy7|p_Qu38y}yI;1Ap&g^x^MajWRq7a?3uW*&5s4|@}u%6epGdzPj4<9!jDD!@T0nxA2sLjqxLp_EZ%zoz1a}vN7HHiXkNpQ zmRI?)^fP`e>r_c^F7L^YV+Qf#*eU$@OPC+WAIpyuF5$OvqIP>sRYW2V-cpL1Ft#=sJL*bq1BQ;0I;+ zfmYpzE;eYYRU$A;8QNsf222TE2QYMrK^3^<$KLpSv{8yuS&~if9B4EA zIf#sqrC46|Fw&F;EeSfig~C^kEit4(C)&~29iL2~HLU}hS<=E-&$+MyI}>29wIt;O zaR763D&*aLhTZTYE3WY9(GhWlxxknkkLEHeb%(fJWb|lfm_mNfe8DlBmF3O+J)#t5kHOs{ zZIDMT=ytFqsos#2QhJe|H}tp#I2=uvVC3a<4>s*~G~RimyUs`1;=4QYMoy@_;h-mzyi>RqWcM%WcNBpyuh##QxIgX+T=9R=CwgUQ*m_`9iv^SEZ zo{UH#B2Vp}=~$c5^_aF1bjvK-!J#7FVimzJ!*Hgm=7NfCla!L4FXN>=xxMz>sG_1-b3(IbHw`D4Vz^&mJ-SE)&&5(G#aR9N zV*@MFbL#WNhAUAv4!R1Ng#D$td181E-;CiseWb*|&?I?#0(&-?dy3mvuc#7YKiYzU z?dp~lf+O4rwy&3h=Q(M3CAbK&#?5z&XAUDF*&zg{AO-_yz%G#HvCANX^^cfk5vaBL zgz?Q4c#8$Yd)L`#?EHEv=~pnK@Hv#uf)RUfM+>?m#vDKkr6b1mTSSlXL-{db20td& z@ng~oeiU8LkI7H+W1qMAG394GQe>eK*?aAO9LgJ6H2Vo(;UXbsH}Yl5A>2fZ`wNWh z=X`Up-HrQ|ODmR%?0@)Zn_86ooPtZ#UiSU87+vVVg+dh8V#JyKhFovYm*B&Inw#f@ z8-}+qASMD@fucKIF*42mR~)iY*1Kr)$#)=!DeLR7s#J{SI{Y-Uq8Xdz=oN~W&TbmP zAB(3h#U9qu+#K~^5&tL1h+jd1K)4#3?1o5IHI^hF#E~R7*YSE$HER+eK&omm;P)sh z1C=O+uB)re4uM50%&mqOfGt^dHT4SV*60x6q|NVPyl{z7K;LvOjJ$8v0*cR<&0(Wpw-nCt+T?2^15b@ zgGQNCRD+6vi^rA?5v3rxsQ0QaR0 zS+Nr!2eiem_4Pm`o9btGB%rJxkwEZe+eqjiGHh;L>|JrA%~SkPRr=3YTI zFc+*_2VGN4!(|itbHM)SfgXYWEqk9f8H3`~E#;E1PrVSckfb>d<7zMm^QP?SZ2_*B zYXM&c(3{=Bg-3)N>O^)`Tabqntyv!mYpQTZeN9_nhJn;V!FBZ6i`#}$y3|BD-~lpL z=eD3s9A3P%1*55wIU(^6^dj#f4R{1|)3Z@|WPaQ>EFHfcjP)k^ZNOJu5nJd4dTp=8Ych?8L1Z4~;rUk8Wdc$NgP@-%c%PGr1Sq%|~Jcn;W>ts}R|9 zeDqnQ^x?6_pItq(YXpGqKXRy1)S5v7ca5oy7!x4*tEt=`z+49#2%! zppn>Ta&|OU(Lg+0Usql!vPY6Ec30BDCdicqz`+q^k}{xI6P(GB{6}zg=GJZRWST{` zf-uWTV7L;TR}**26logS3QkI<2(q;_Md;m(^?xLX2(rzXnoJSQwuJ`Rg$UX#vS%ij z8@EBkZ9rCcR62~02Ztsh|EW@nApN1W<63J_d(KhrLG3}P^J-8_pW8O5#bBPR?O%cL zD3bq1DxYl{(Ml-UKL(b-8ybn>0{1XxTZXg1_OOfA5{>Mo7wvA$e$wS!Qz&jXND8CA?O+#4V@%WYSZU)vJ zHeOrCuOuD~UdfI>0ZJ@W#ivb!R|!`oNV;M`%~5de7`XzIhpi;y<-of}VYY49D&eGv zp91Q81!s!bmN6@-%^`|0sw6Qd9f3J8lB4FdX~0Uds?jRpUIxtTJ-BTdtrD&rsuJo~ zK>Wo*eG$KixosJ$0@WO+66%yAv4DakZNR8)8>a$R4N|FCwP}zF46_%J5N#NtG6Z)G z1ktA9DIlC<(?)a#%aQD{DYe@H&Dga54AjgZ@tu*qh*vYQh^a=KxOx|7#H(uRtKFkj z_4fqVUbv6augu_2gXF#A>hkeH*$&TbETLv!diZiU(qTm&vLwh5Yf-Xa%$A<~<}8kO z%$4Z2YQ>3+VPW4QI72yByNZgID);Ro^3{cOSQT@mX^`Aw>6VAZR@bfU-oIP@oY^M8NYqg zI83FAFzq9^%(|KyY#x$ji}xiDce1H%X!im#&Kw{cI0q(%if2!vcwfkY|D1@uoxga; z!?`>l=Am&Oe+*0;o{nIr_($w!XGLpDWkf5(=R0Do6K9|5S|EVwVy$^oS=E`v)wY;* ze>#o?6vJ3^h{U!Swq-Cl#;FXl$>7keaDA>^LijdQE+JTr`GEhLzi?9_+Y8}37b8dZ4_bT zcQ&>HV@_>zjmY1@_z=FV#z7U~T9F^*F9b5LdpEfVW*oc@TNM_zg0+O>Z5p;dVCSOr zALM`~q&W^`<6s5LJQT9wmR6x%0W_D8E+|{VLu?$3e{e{UkDXD|=5Fv+|I*5V2-e|@ z-5w5}1n;R8aGlug5t{I`>3QZqBfEPF?H`+#8J17u7i?WAE#`ypP=`&r?%-16FlCXh zJ2>^gzFl~M>s+j-=u19a=ubGgqYAwV+M41{B&wjVf~o@{%n&61ibM;#i9ki10IEP- zX8~A6Jfw-Ni za~4uzt1^ywaJCIUwWG3)AzPDa`Bs)41Qp*zdI!mWexg~%^ps&lzBBa5Mv~-KhFPt7 zp$L*8o%v2wl15G;yJ2Vq>1ltilAl1=Gk^Ves8?9RfgGV`kdER(0cHC^`wk z?2F{TG*LsF2xMvkI1$9BTL6}+t-7%(MFh#BPFoX%YKy)Ct;^cZy0Iz6Hn&wbfT+M2 z$I-BjJ>A`~ZAx+GonbX>z*U)NB$#$?` zFhfulsIDt5N7FQ=u2zGv2oj3z4Ovr@) zT)kE13U?1q@4K?q>cN?Tow2(Yj3rN;!$ZY&jiDL15~`-IA}msm#h7Gd-CCgTfIIBr zE+q{9&=eLv`cQ_g#Z>x$XF5IWnwvyd`hb;$+H#RyN`C_+W)2J)A{R0mB1Zgy_5ikS z-Fi%!mx8u0kHvpWP3a&Z-(W9=TRH1YDcpj(x{9(Y?4+<IJA);hn>lf(2e{!{BeGizRi!apZHPUqn1!A za`+LR%8$w#epH=|N0D?Rg>f5jmMo?pDaL1~VtiU*UXX2)i1b5g9ivWq&@VU(nmvYp zUP1DQYtn?DN~V7?J_&g3HCB~}EW%@X@>s`vC%zv*kPdh+Q8=Us1Z$4_Fw;Q>zonfZ6aH*|hAF1iw<*YoEI zaJ5G81~K}4`lKu*4OG@VKc0;SpJUs|A`*K&!K*#FBP_RFm>X!t<-AHz34cZ$*sp48 z#9;*UGy)mh=mNBzk^HmwFm0*PK@^qqD4bk>$B38l`T{z!YuxH1KXRk8qAs+gibiL~ z=+rkbEgpk63}4qY64kdD{~U$n@4jc6c~?q*^X*FW9>Ng}56MRGABxH)!p)$%*G*Z~ zv@^V{r%85bYfyuYGgIkD5hQM@A*wVe1oNw^!^*+8Fu&p=d4{i?eintJZb8Z9Lw)>1 z2zTeA>Q{)uLOS@8I`+jKuns*|8WbTuY(#l3WM*ywd_QsY1df8Cn&zhPQn>aR2c3(2 zLGs@P@W6w_K>|&P3PdNb^kCNk=w1u#Q44V)IfoS%>k+MkVNc_IV>8IVu^?U~c_cN( z%t4{>Dh6sk3C6^`pNAMJNKzT%FHde2PSX=Rk7|{HuEwGzI6<|kRusxZJ`tRU(_hI! z8d42&^*dF9jA`5rw7*-3NYgElWfRbPy2$$&`?)XOyz?i}3Z6wS;>_k#)%!9%jx z>3nQ`BT0p+d@u_W{or`$RZ(DT0lD0QC3z+6WcZxoG*6*tVw@0{HNx^Ibg1Lwnh6!HqaGI{gq25=J5*!2P-_CR7| zh{>|jEYOqTIC(WmN+vT71yQ+0I3DqQ?7AZgtMmx6Wxp6?*CeA`7b)k85Ga_%z7mBb5ii|JD2w}E4 z+Mp;3#+v~B!~&+D0*|1Yh{=u1Faj%Frk8Ix{Z-|BU+A_glFa#ujg@svYD#ODf%+&t z^(gBVKyV%$YLScJ1~kdtu`k2TvDkYs9ogdV>~X#K9z$?SjY6uSucEW%;?HF-;iOWh zerV5)F+*Mj(lGiwW{6}y!a8FTDs5O0l7|GJCw}R5Xk|q3QhXlndP$7QAs9qV3Wb^* z!;SJ-og7&iV1<)qU| zbMdaf2LuVw$tlD}`T~r(mnj8#Z~Rq=>d@>ZB;S&dyqr=PUAv|QiDymKg4(){#2L@6H;$=73~Mjuc!6) z8MMTHc1oJK2+FKN;+YejdGKF^U*+kCT2h#;u9c35bX_yvIgn_w;a3@pAqw7~#`m#kRjjtsLo$H{&wdr&K z?IA!urCp&u*vQ5}O^TiQJ>fP49X)v{aj6->ZmT4ufik< z$^RW2ua%h;hl3}hd{=i8U7$|Dj}Kz6R%TQjUarjQ0S3#6Q*YvAWtNXKi;U^atS!h@ znOQgoZw|`j7&l_{={T>;jwEdw1V>(-b!5oVqfrYtFaXM&tCZD zA9P{5YdmNUaV={W#~I42Bg%A$%09j9ZR2&VO|$}z-_g%%tRRbi?l2yg&Bs0goohv( zyBg};crC7jLh=uwQqy-b`n=4yJ1`12=F*{Prfl!Ib@kf9H|b_fi*lzEYgZ5vcB<{aP5O6Gi$)`E;wVj z$-pVER2--M+$RRyg_#O13JnfT$rUtj!-qUU^V|Vtn6uH6_`DB$SnEZXVP|IkuE8^(w%`jDoZ~O2-iMkJ3 z>4vS+R&^595TXIezazegCL7V)O*^zJeLz^dG4aSaU~X#{{ZS_Tfb1Mn~s}V;#w@FRREQ#biMvrDP!OT-BygYs7IrGwaf*LE;#mnTykCs#3yBNJpp9aJ__Akd~*> z+EEEbKg>KX?HPcUeAEq}PfDwQ6rXoP^Okvf+PhES^EJrJ%yZISeG;Fqrt$3sX#>9T ziN5F1=gZQx!zQT!X>RML6glscn;5>~_v1V#lD~zDv()$U z->bg5C7#*Em<)`504~KyY=wYkm4_F71P#iEQW9BtvB+X*vZ@g5{wy9K6E|f%{ zEc2$2ujxTVy+QIHhw{T~MS<_`MeBGb7bl)7-t@aHPLAm$&6bb)s}Lt6$r5w{X)jBQ z$h_nCSyE`6k2_|}*~?K632__}mjg@nyMLEiZ)wgmoEG#eM^90&237&~TMt*2IZ4_AnkK3~s#5eq@adc{CiLnTsVA@mPgO}WJnQ83)19oOO zc6R20#@QF8pf99;_h4hx#VMlCO_}(7sK-4Zcequk6_L> z()tv~%5!x*9mu(!<~L)&dFtPA{u#;SOaXc_&G35m7F9YP-+AAd4k{S%BNRt@r5PGS1c}nDr5t}bDRp(Z zvGECaT|M4eS5Gr$VNxRN>gh%ppZl!JLVm6Am<=11+?==tZFh}bvQCJjk^B!{OEs2R zpR+Ak$Edg4J%%IyNGwy}Q4&nrrh z<@3k`z+cJQQFCo09Q(r+*$4!o#+63qfKJ!{9g(HrW2k1zyL(r~hq-i0-r~tZUg;QR zi9$}5?A-xB4>&t4jM0CP!($kFNM{dT5FkH@0NoB4kSnm!SI3ek$b<(hNvi7@R4`_ zEyN}y{|7Xx?!yMU!_!3nG3_&7{P!K$)d^_8V(0K(<)l9@Iy2o${|=}_UP>Cig3jWZB8Rof0x}5?{HDPB;LuU6`m&ZBoRA# z^UxTPM@G_JGuVNn|4#_bo*1IrXC`AhPMgT*e}teD?7$NOvTJI_(okcN|IAwA?Y~w@aL--si=LKi1*Wa!A@eQ0zt^ zaUT2G^UQOSjmo4mc2O4k1{u>$29)Hh&m0?KZ3<9?$xheIulC)7wU`Ynu@*CJ9{$Pv z&X_7<2Vkio0js5fZnIIPUl5*&;?4=H#J~KV={bZ$9#s{FOd9CQ_e~D zzj!*lWbr5e-G{;ZapM0f@uzOLMPwRYZ$w#CI>G=Hxv&MgQ)trBpCi~blGNSg-6_Hm zwBwnRv+{jlyq1`=;*O)Vlt0ZfcB#I`bc7kWLmG`DI1WkbZ@GPoARRMy_8Qzp$WCAz z^%lG!a7gEM-=m|TN{9m*e7Lc+^LMDC^&x2k7RC&rTGp8=Z}1c7gFj0fI{#kTIjamu zJ688VDO=Q;DqhK+bVRbc^P4DnATh(`IB9A-Uojs?Kr;H^y~Q*PbX3aR2m=*&WUD0) zD!%oF5bq-S-=*3i1!>s!f?y1YM!xDxH0?+%OyF}{qQ-46(F_vkCkWH&D<#mkB+%R} zEW2hNxs3(Fxl zpy4v45_2Z|EPEq+L;C+f>WuG{cN6NDqKGdV?btq3~^|aqaRbYGckeNs=<1w3}FH30C$;?W*>*O4bvr#<5 zie&d>Jodg0qTPYSWz5fG@4KlGRQN&vbs9l zZRtD-GDf8Le)yZ8(T-lQJ7*LC=3d$=50n~tasRi%=0Z6t>BLP@WB3cE4H5O5iUqIC7ax%xI z9wNu3LmTk^I~EP0P^_6;&Le!t<6|WvNI~cG;kj5*MS*5ZtIcrhd|pbP+K856yAR5C zyk)D2J2Z^lj!zFkwGzHn_JnlK4i^{Gv%&$;CN`&}KWVW!HT`)iGcGo?VrF`gM8$_5 z=IIobQM;Qe^oOC93S2-6#uZ!vLyjCjEp;tFr>Cyr=Q*h_@blc%P5eABHKm^3yE?Tm zKUbyZ^Yfb28T`C9wUVFLrT&GV*QZ{}&nHsv;pda7&-3%C)DQ72m@sQAKlbmtn7$o6 ziXVp@fXDWHt6tWmmmn+M?zg0_p#oPAHi>M}X%&h{de-E-w#u77>P6O+BSFF!u+)?` zE)rSA_v3G=>GWb&2~W@9rZJ(4Zj~_aHKF#krbg`gU+nrI`HurYfKUQNDkSOjXrwLT zP!N?XLeX>YuHZCN4&S}3%3I3NT2jS77X%j*(U~M}PpV*9bF@5AIm^T)cY=qZhiFOCfTW#G;?CF8 zRE4l$_Jb)qHOQZImY*(WKA5pUnkr|+7!ke z*>w%(o>^&aMO{s(w45)GR(}gMl`a^REgND!iANvzkiSs*-@RoeuG|^a6Rq*?M;{Ab zN028p3pP?=phwBWjhoTn%c^FU%_@@<@i4$}e~|FMFaZPO0bMZ595BPY<%V)=*R4AW zL2Pyn^~Q)F+U@%E&O&UaJGwfp!{5@gPN_^WMB3k>J(pnE>2#|&)C?ZyMnyr}ZoP+W*#}6| z6q=1uQMaU4WL*{=h2pC+H+mB|X942^7Y+|svaX8?VU0Da#B(70hawKp)ls4+m*u-b zO&d`fYdFhuhR+Z?A^HDGCRvAnL{Bcy_eZC9mg%XWJ-|bM<_^e=N21f$B8arAPiKJk zA~*d`>fXsQBI~i}C<`1lYdsJ)MnNguTVy>I9R@eIS;AzYyzPqQFvgLVFo1bBI?MtG z%pqRcgi;T_P?UMJ+o$^_<=A}&G#L}zvu8i;PbAjoSPB&vU7i9 z6XIDkQq+$NfHDCnQwZrbE-)17B+$v) zqG0uUGR2osD5_Yj*YWoIiXuRbi|AR1MkPyb4|r@?Qv+>?QP)m}=!qm5&1DK92U?LN zlG8-IK3$PWPILcCDy3`b>Zc-j2(da6G$&bf-KiMNy@Dj{qNS@THtFsH&BGqLaNk3Zn0cka8*@88#6i50fJaYpP7b-B3<*kQ{_Z(ec%+UG*Gs!m~sc zGUt|W1VxGByAj5N`&E zFu>|9!0$+yxm?c*mHl+mL{>Ml-W|ZXkFb7W7_M&PV!?2&I}KL&1XN$zlw60*iK5no zT=`NP&>5#2Vht-NhL8@!1bggg| zJa&!SLydLvK@~+pTeWZnI&Zl#dV;(mxS96CJt2LQ-Hw4SO%C7 z6QD&HAK=|j6uB5zN)qg2W`~aLaJp3w9Rl$%`a{WqNd6tcTa3#hVwx^dUh0TP;BNCO z8q1NG#^IoxZ&6RCTi)|##GoFAHx%tXdf(mcr6{qob$pzzRR_baHm9&) z(vKEeHW%pey`bKCsz@dv+Hg7nfHF$L7~^&@#AGD@pBRj~qi$EX0*rg79Uzi%8c@!* zktnWIw>7Om;s8-TLV6V_Z`erG(RI5wadUCMHw5Y!eY#KyRe3m$f!4JOs7g>NH|7Im zu>z7k(m3h}ySdJX%Mr; zcMpCY?SAJSb?yFz=-175e>pxfC+h3l*zVJSK&}UyYe?L_p9t3R+IJcTaQn`ScCp%b zw}#xxZ`=v3JSpgEQe-g|X)7=o0ko*%F zGJfl?h2%cq8^SviSm)b#@%sQR9`^*B2=8COdf+Y{*FXx(*RC}<4sVTfT!GN0r3 z2hrfF1l0}WC_tU8;1cZ>w3ysCu!t`KZ-c^2pl^rPfY!gSmT6y2rVf06RdX= z0FNP1Dm8((V2T9#2|XsQrLf*l1Md|}P>C78x6p!epTUCmK?5@o$suS0eTE*CR&Q9+ z1^^ySpww7M?Gx!aw5Z&FaLsudh_ARswOjQdY115yg$9( z{;^T?y3IGZH!>%bD|+4Qlf8DH^NqYvZjtyJbjO79M6dhwujCbpF^n=y^txaF%CI6a zhhG^kdOe_jWq6TT$ghkLy&lxRGNMQv%dg~%US_nFT#vZ_X)z<-PK5-gn7hcf{t zel*HUuyG&x5{$eq%1f~FaQPC<{8*HiVCNF~5)A!#l$T)XGWilrZH8=dn;P1|*30Be zF!qyCkYMezVe#jpyabEyEMJ1f&DbqoJLSRRx$-4g{Dmk;u=o`D5-k2=l$T)f zkbDUie<{jKuy~7n2^N1j%1f~L3i%Q&zCOxJu=r}c9Qp{F;wnPmK7r0KZ-u@#{$ZdPl^sqwwof5x?f<;OC8zKU0(VP2|s{-gYP* zZuN3Wzy6UwlY*lnee8j*3b;FK(`C2n(P4XMlvU93oQOKjUq}7pd-_8^eo3<&90ElgKw6EbAIV&gDDn zSGjxNTwApmrx|eIq_DiczNV_8YV>IOH-vRC3ZTnNYZc-7rOnk%@VcxHH-&XC!WWbN zCyb%h#wmZoT;)kDVh*k2&%w`N7``8A=wc)L&-hI1U_+aXXOG98%a2%t8+x=+dKf;_ zg6`1c`Qo&__QV?6&?{-GORJ!k)TS=v%a7l4Z1Iby7nlm<=GY1p;ichj@r&WES%m*a znwWIQaDHf^2xJ@LE~LPF5G&9hpM(@*&9)O_W=RXnF|MCejzCWmrq^1s&I3slCO5}+ zl4hgKqcx1=@R_1WC>jf zU~3aNV_y`+UX|D(+V(1uo-llMH3~*TIt|^;tJ5y{Wyw<)yLEbnzW2iR;{QOW{Q8Ev z1r4P&i76y@_bT)SDYVFwNjW8yNuQ&WHpV`K-P*y&Qy<=r?mCycYxcGT`qhy#(>Vysi(KC0Q0*K-}fYf`N+ZUu)2 zEH3{#413~|!OI=MUXB1vO$e)OYFkhB5Bg?3Qe z&ZdI)*n5_sey;|Hn~2LlXoT30c16@F(zV@2xw&9rPJHRK)ibDa#w%d+Ia1Ul4 z?rKM!MDK{cg}98&#r{8%toeh9%W(!bFqQ4rMxIXr7j9!#fy){j4eEws^e-aYsf6t|6{-4>T z@_*|}AoKhDwx)(v3F%AsP#cCHyvmJf6<0d3#Axh%R6OBto#+2*F z;pj^zEVyN4%$PC?zW+Sqo%RBh%$P+#h-t5(KggI(pHrtj2xrucIrKR_ovG%^PA|`C zi}tNE)5MpS*L}^GU48WGqge

Cu#>#W*ReX#(8Ze&^qos_73Y0D-3rb0vAqfrst$IQ2 zP6ySsD56m5J`-Y`PoiAUqI`>)P6l~iCrcEXp}6cpdC_ezLu4UIA<}onBs=4%q~#1Z zHYb7I8N`yaK(aKQ#gmW&aukbN{R>#87n*DXcahB{@8goG?n2QFp2u2zIb@FHds0&Q z$~N^YF!t@OlqTj*&3PR$Ddiqk=ccp)ypOi{%chjen0aJ5IT=2KZW{UwY=5v=KVs*% zdCoa`tflE|#z1u>F6IH^Z<_u}THU-dX_?P0^~YfRtzs@)>z>O|;xn#K8sA)s$YgC~ zFBCMESv(f&Ur5phvg~ifLU6{d$rFRl?K(D(<;L+(5F&sPnG}RXlhL2XzKZqqjQf%& z4V_poZwQw*g+q-P(qgX>v8Yl`g(&A+;;>zKG)$vkdwV2!C5F(cEj$+P=nax>Gz{L5 z+5#_@j15#4@u0GRM>*_|2lc~vP#;)KstR8Q-Ab47KAFJ?t{ji80`kwu_<%CZ$RH2) zjE@>H+AuQc+-%0Cqxmx(%FEc??>73K4%}sYvK4n=85uXzJD>U?fssK+k}^JUt&TBqcXNo3;`p9$0}dYSj7nTnt-i_8mcy1xrF4)yy%#r0r!Q*d0?>; z$^RrZP`fa*fLxjAXl}AuGCEB(L{}u{7+7z!-6=V27n*#J2G121U$VPo-j{XeHDoMs zbg+eq`G}Ay!B%kZK1pfL68wvrWwwD7gAW&XyciGwJ(6)On4fM5@Bx`#vfVE^rssap z_zoP6BBl1HHrM@=!$$635dW2dzbOO61fw#ol1@b*o1>h9#v=Zrf(w=tE7 zJS$66Z&Rp~A<#vZP%D>OHQ{Y)ylzZ_QRHsaR`3k$V;Vya3V1iFiyRbmXx)>`x+wHN zAkU3u;Ch1M9N>b_8&nzVlAtVajVaDwgX@1S&RqB<=YJ%JbJV<-hB$5t8bKu4_)^x3 zzQ14=pRqX!;^fPP+alQr3S%o6cPUoPt@h89pr`bM?$A=8B#3Sm<9rBor6m+wq#qqS z&)78y@>s{!UqJU`blKMsJ&D?GXPlA@#v?FA(s-g_`O@^Q*{D z#yA54oU4Qo{ffxwZzm%<&BBx)$t>QN@=8K7IFEq4FSDid{>IT@ zdA!ATEZscVWqijMGRMV!#us4sGqJQw8K1t*$dET4c4#TX@!W;tUaH9pW*edbDS~Y; zV%yP!E9Se7_`VIE9}{01Ze%wyg1lxrqOWWVRj0kg^p?P!@qFP@+s-?*qfN#8m3aOskWj ztK!P44n6?!ACOE{FQ(z_k;zb%_qbaGIWQWl!0-lQDph5s$0tQqlPh_d8kq+gqA!wE zweKd%)pL`e>Ztl=T$9IHYj(xwg6rkPnQN}>g_-^8HJx#D z5=2#g)U-3R^I?YA4M}Dwtyzn{2jPrS#sf)UJfa-l=;V-OoCB5@DYl~TUgSg7wNuB} zRs=(zhKW}-+B_@1sah8H^$ZzecOWxu%zc3GD#&-UB_kDLQ^u9Q1{vudNs`I8PX$7MBFWOtb&X~1vmZH@+>(q; zrIxCut|e@q#ej1iE`4swEuhmOI3r&z1HzZqHWiogO;+L)$oI1)Bkg+z-l01(q>T6d z&t=RhDiUXfvGs=}?Z`3?n+_SXp{!h3k;8ZC`^4(2V9$ltcz*Qgi1B1Txp28NF3LIzR0)+4r}p|TPn6Dvx=(5`clLg`PG%& zjCskZ`PANfH1Dv&i>@`qcc{tBn|;q$8R8?P0oP)%9JSDPD>2&A!r=vP+>b`yv4!k0hb06p6K_FEXx; z)S5%u#MW9Hc;luq=!pj>q2VhqtH)!Sc|LdRv!ruIC+&F`#+dh z*`+SSf~)2#yWaR+U1i7b$GOT5>sD=xa>db*aW0Mrd<(^G;QI)Yf8ixqz8sEE0TLzN zMR@IIkBzxKj=&|LNX$C!C3@}SsJviFH{%*Gy^$C%hR$&>;AsSJr*64#mF#t(& zj(2%q`#77Y1eosy;CZRVmoEOJa?Q_aSBpkIC&@b#ivAqdMAiZ z(YlGj=Pm|;9f?Z<(Ym8?@r2>d3sG>hg<{UpN)6&^>JmQ;cQh`xLpHpk@XvH2Hc9k* z92|)XqAU?q4CUc(V(E;Ht1ILb-uA>}aB%v_r{Is|4sRPp7J1+}6+{6f|0TrF6}L7z z9rA^O+KmHUiJ}ecPXMlfv{3-$>BD7;P@X&#K~mEl>Pe$$#P~(?4%K)>TpE6|9dYUV zzaZig+tG};L_esAOY~VqT%ymhA};AwqEJedpLWO?E0j{jv4&WP5s`z)EYprR#B3yq9wTy4Y;bOs zRs!cr!eVR02niGyOIDN3!1_kv1>Pa`W5oK23%XE@J^`Umkyt<(IwVFApZI`M%(H=b zl?&A=t2R1MTxhPun*sWb1uQFZ^sqY#U?Rfw-Y3HK9Z8yClWkLNJe`Cvr4=p&=yfh& z(+XFjGU@~6#75`J|0xi^SE#Z9h!FuNHlR1O4Rsn*Pl6K&lCwla4{Z|%KO$xgjk0b9 z`ok6n;u$w)O&m7*U^G3^Z}vlSXu|6yG&JF0G~AB{Pea?Ng3)L?;lw-Y4S82{+!x9n zgnuC;;;|!|6g;R+#DkixV&Z7Ugdu5+;2Owj#)Kh*sF*NEqDfaVVNT-DGA2yXvvy3F zvFq)aFa?l6#e{jDKRaT=m?2F?#Kn_g?7N(1h@Fw74Z7NJp1>$^;aEIV`w9RyS)e4N zQ*lqkleowvs0q9lpwC;tB&`$J6UrnmFy`-?7$q3fn%wq0-4J^rN!uK{$|@ha7$vc} z$>IM|06SR$Mh`v`8%jnMK~dNV>KiWt?hOSQ;NqZ-DiRlyivvUUKLhhJBuA0Zh6{-c z8P&d0^wYrO0*eosS04u?HXjvUgDN%6&;AX>FDz89)Kv5I0X`Cgj;@C>eT5+oLb8j9 z&_?cv&B$7WdKXQ*u?l!MC`=R18iiJV59=%!bp3Z-@NLS~URT0v|?4hQfIwO^kD>qi})Hw-}nfEx&K?D>Y+7m1B$ zmLY9?f!Kh$zyX&4_F6ZzE^t6>V0kj1b^jPx-x8i}OKn7eL;zX$y)H1sWF&{~+N1ul z0jyFdMK{HRR(>{Um43J*3h0~ssPYkX%!DU^9`2R>!IPC%W*l6yP?KBpx z2HX;Y~S1Z;58aT-QLwm@)gQZ}5vJ^Y5G{m2fWLB{hY*_8-@eY+}H$Y#QACP?35lDX^ z`6saGbcIrxvQ2*=ISzQ#lg9IaeYnCG`jf_Su<6bh&k#|<)Vj05vB6t$=-1h*`7QwCt1RYJ0DIk)MRs{*X@lj|w%Xy;hR?x=v2?7Q+ENhnKsmKdLSxYmr?yJB|5^^VBi{t4 zDc7KVK7iVxuW8A8uRRe!VCWioUMM%;x>GF#AGoTf#$ve1L;{hk8ey$;VFf-Szyw^? z0O;{b%L&b_TqrhKa0G737vCq1pCnt=ml z+L;yy%hL?M$o*D)aQc6_O9FX*jlCAzquXzc@cgPQk~@!^fv^S1et>jG$MW0)wA*vbC1OPFb^X=!*81Qk+gt0?>&RP+v^Kr9 z=m+JkMW2!32ISvM=LEu! zkO{$V*T-~vQldohZh8)EtAJ34jebE& zxk16i@K-WXxF+R+U?dT-A~773Y$91u7J;Zm5eD8N$zymR+0+ZgwV=A)qU5`QVz?SP zC?(_fLHdo0+{s<*Q)FXs!QA~uL*yc{&H*l%QUL9pT~1<*CCforYY}g<_11a`xrwC$ z*Mj;U7lNq(#)wheflL@~t=i_KyBsdB6S~={vTHr+5=M60nvm*|f68<=3 z2s}EY4&gyn8xN|K#l*#O(jf_q;H;Z1CmqsOIq6)7CP+Ey+`*rvlg|Bb+fF)}|F)fU zj)VltNoOm6);Q^)y)uH2fuZi6Cew_vPy7WhrQCx2+(Ly?>y2Z-FN>m)i}wo^P2fA5 z(BpPv6VkjEjk~c3R7YBr_kp)0*ZSAE$SX~RHK1PSM)(B+IAJ`u71=O=KwUJx0Qt`r zMC!@VTYKM-O(+*x2d=@=HxipFu$@ikag4F)n4u~7d{DMn)Wp>(v&RF*h9K4^!VRFl z-$Ee%P6Usui;bXNOROZ17Rr%d0J#0FN~xnCv2sM`(PDzi@lOG$CMe42t}7;u^ihHN zcZA$VBj}bZ+5l&P)qikyt=PGf8<1i0-ub z>D@UZD6b^S4?*>*%~I#LVG=3_4QANq4op9gq=I?}4G$f&+aELs6CIngpVkvY32NQu z!L~UIR2SHkT9*sO(Y%j?>7D}3^EREvl!h2%`5aLY*suN#^u9*zs;${efgFEVMocCMi1?)KR{cfM~9c(Cb{_q&$)+PXyK3 zL`e#o8Km_hFe!EJ0)Gent8NUPyMT!x{Q_8Q-#zFAksSJJ{Q_K+EOsHN_904JY^?); zNono$Guve#JVvn7D&cMMN(}spaLGN6gKFmq zHvZVfR)*aQPTe1HX=@MUw7b|Q%S5-UWll=dR)f(J7c>24b(>hw#ylHvJvhAU;=%<+ zzb9?7Qk%~-L)!5{wEsx50MMvgzxk}Bv?6zxfM`EOsNZEaX`*c>R|?@vp+w!&9r6$?fy9=GvZt#+G@G}?+1CL2 z;|Rm*HDknoR$!64v@p{{KzT|@6DS9`c8}J?NPIKaQD=SGYYnj{lFWO`3hh>G8+jmT z4_8{>$zXAji%o!eX?I+eQ6eKCV;4Er*6L^A@QcNT7ZIXwt+t5^?V--b!c;kuG#PWT z_fTm$Ozxr5-bV0aETXk?57o79m(4q>p502<8*x|m+OlKn;?REXkuh#u3^+i|PLEmv zYIvYz2B^7~RtPi!YS!X+6`+RSj}xGV_rPYBU~P8CzrpJt$^R!BjK4;ULIf5s#EjZ! z+Yh7%DDt>rVk(Q2$2{>c&IIB4L_CDLopE&OZC;5CPOmweRas;$e~_5QbD&&rQO66{ zQdwlKWiV|z%)0}Uvw+a6odi;dx@*?Zt;tZ3IBc)n!o-U+9J zxo?BttaT_Vw}V08dwfe|Cp;P)?S~WMjV{E%Cj^_oez*ne*%i-4tQOj~7mq1HiHRv~ z{C#l?)?St1HC{pL?n{^tLHR^>$1S8*?2%(=Mwxo2#mCwsM}GI}f{TP>M`N*g+TmAn zFvmdn6_^F#SF!-Z;a8}*$ZVv-B6@Z10_Z1WIW$<%6j!kkRiwPB7>3?=I{wMSZI2YI z*knV|O0n{MiIW+}s5=e6ESUt>4&H%(=irhKxqX8_JNN(1;!hj@B~M_><+@D~8~;iw zxz(`=0G}iI`<@9NuI+!vi{`p`4?t!k@yme?WCoWPfTECV;bYaCmM;MKxP1YnxDj2E zU5OE^<*{e&-TwohqTPSe+1&l7AJpzYeO9~w^f}hfcELEM(fTMvVsUj@50s4-lB_P#1D)tV;4-t*GZ+LSv0(!z+IG?UEa*X`g-e05 z*o9;&b|s3vK9q?Ej_cJ8fVfM+$hs9Hc8LeU>&rl`=8phNMUyKvjOWYX1@!tf@VH_s z;101MiAh|q1~0DOyWv`Z-fjWg-VM}M+udVuCR!5Z-H|oEkh13HTdOnXu zP$Z@eTy6t+oE#zn%<60$2bdK=gHhLwduXM@1TyR~Xk@Gg{cQ>(z?r6Xn1}@7Ok<2qp#R#!;EdBcPDElv zwUHEV{7dNnkZcp_T_`-j4p$1;TNtMR;$jPi>kE}}y)#7=tmu*&9|8I+3!XAk?^fYK zw$>L8Km|4iy^PDDkXY3y?iJAh$_bzb#Ek^-BB`czv+&THOBBrhW>DT?vDdp>=*gM+ z#~}T}Vy<<&@KD?C7e|uxd#=Y$IFhOUqnL7L>_@kLIj#pV0QTZ2k1c?rXZD|h_A8sc)-A)s-;RjEHXQN_*1wTV1+?xN9t0Hv zgXwEQ+ho&g-84M(4tEV!;tr7CXG3V+H9QcO+Xe%C1>zrU0Il1G2SBaSQMEMkUd8wy z$+UsieZxal=h)%F(l~e&18RnXi{jwnL2-tnV6&_M{dt5T>xb6S!-L^=_;>-tuUY^)hYt_A z)A3`w4VZT#Iqa%+{O~}q1ISn^8B;-gfDXXx0HQ-EA%rtQy^;{f0A|)|9YH(@I){)q zfb)SHOXm>cL6VLktWK|g;1XaYhds28AzB1h=TJ~r5`wLc)WM-p0p zG_D5e^)`F0BS{qci;4an(0*jG*E*DV$X$*l+*IuLIzrzg$&w&j37u<6bRa%OVAKF( zsfF_YX3MtHx}da|ZS+AS%aZ*jvL4A{0y%orxudjK7C5FT(_ajpYutRP zi!klach!5sD|nbE>Y$=>UFLX~w{ZRq$yQhIqS8)gbkdm3dklD;<>D8hVy1Ui@i0|R zD>Sct3K;7x9GdayomM;$c8G-ZjoAM~V#W2&D<0ZtPAt@7_znQn0tLq%nK`)7xv_YF zW4N>23djdsuv&K(51=;c0ZI7_ptk>)%58RRk&TKf(|aKpqa-(d`E!sSr&6D z4Ng1hoLsyT=$&2G19PK=%fmyhvx^rks?*DWw+-=UBv~}{fegH08s`_1#8(HLV+hL{ z5yWtQ@q$J=!Ejl740tan`2xeJ`shLxc+e7ehfx{FHP)B=Z=C-@k}U*fo7N$w-Q38Z zjnI9o!0b*J!vJ&Dy2g0eYN8M1#deYD`VO}KkfeINpcLIjrd`~~Rc1Ljo$2Nlaou-S zQ&nko)e+%V-`6OYfIZ$R)|MkXA!HctyN|sJ(cY4}e_P4Xma7!akp*o8j2tyvEg|Q!Zt@^NvWmCA!$Pop0XCI_-VLIz-|v zeEK4{@MWIy?tQlNGI`g{?XmBxPb6o~bKlD9(PHB8_T&LB{p6BrE(GUbqoN2Ys^NK$ zz!SBjjj^B^tEobH*g_gj@ zY~)ik$;bVC5nOp8^CL&uh*Bge%rofb2X464!T5(W=!s;%B@B@(l!WHspxw zL8ln4ZE+Sj2U!w27Y~PH9ij5qq`9F8+>g-r48XwV`)v; z4K^s-EM7p}HS|4D|D5{yOw^FlJxZ_#ga1x28p89#4dL4IaA-+GX?=aTp^#|G5MM^5 z_PWdX`eST>&ixEIX*BM(6WpQhHR{i%*+m}Q_Imx*bc@0=ng^x0z=WYPGK=oq3vQ{}f#jyX@BFH|*6+m50$h(q! z{TGCrM6YK^zIZU?YN9~i1Hfhrgqz8CX@VG}0x>W&t#nygIE42@4dKS->ZVZBvih(p z1cS0$>Z&SGe0ohe{u8X->)#fei_TTq46d?M)S{FwWrx5p@LPC%^ zG(t%Jxdjo@c~~z6#&TkPrkizsXqLAsp)xkY`IbyEzqGm${(46j5^4;TEwcIOHPvdy>dQLi^X2HY^i2lbi66(1a@cn_a}BY2!5K zSmS*Xos1e)bW>_ve4#at$F{Em?+p_4=0_o@LmF>1DmkUFezl}BuZ#TT3)`!hJ_j>D zx4nv~$g0R%)FQScvX*6L*9DQ}Ry+a>Z$t{@p#BG@;Zq`R#ZmFdub3c>El$Y5c*0G< zuh_nF@FTwPOZ4s#PnG?6VvBd z&csUC7~aG)zB0uANd7BG)fnEyaj|3txDr@5DZIczQw39e2ODbBbB4c!qjfCSfN{kc^c|W$B(_UHR-EXb#aa~X)ieO%XbWYF zZ5yp?aTFA(+giXp?}9RQyAl;Z?_2D}<0{tadqZ?ZV#Q=Ni{V`Ch46Y8Q!{!1(8?5| zc{yhENNEv>TKD3cf%jJnleolnFOC4#`xkdYZ_yP=W~}XBOx?BJZw4<#yW;dOj@u1w z_-+Ruc@dLs@gRfX@dPBtgIWZm4b{mkeRg^nZzd+v!l^49D`7nUE>H4o~VceozJrnJU+%6*2>9vEy$m@AV?XB zqjfTlf@66Z2Yy26V@E^taMtRAGB*3DRhApMLl0`V#f zptlX+@iW!{un4A&{{@K8Ef|u+sgB3dScB#8H2(9?h8Tq;%|jA7p*)_(8mOp#2BxT| z0(+%}|2m4QiLY}t_TtOY<8vVYhk}sJn9kSOOKdqC4}?-i@IScv14-)1S*LY2)}V0K zG00p1lv*I1eOhm04M0e1N> zBEcic7EbST9EG0Qhd>)9dSa{APnL~1R$-Tbv*V0*q+C1$QwX@s|}&`JoZ8;*JB3Q z4B{;|fY$Zc3*hiQW<3TApBRQ@>Y?>L_F`Dh$Ba-1>Sh~3>wN4*Q2QHHNsZeiv(SLZHQ`{eUBZak>j9Kpn2= zt8+m1f@wUEp9R*-3NMNWvKPkbg3PAr>hp=7NM;smU68#XZXe`Xpgzz-(D@*H>77o< zXM^%mqNX7gk3#g(=e!u~g?u%YkoBP6s6+62A!{Ml5JZm@pU6gHBS<0oYTb~%5IR5P zG62=M!E}DeUMT5^%o?o*^=*V;+eGV#tcPHYJ_7kyZXvXu$X*D`6`2JYoQnF7WHyA_ z+v%brupD)uZYBiT66)@w^8zSmWR~DI(B5N9pmj!$2EYP*0pjm$0d(HT5%d+MO{Lhk zp=CrPr;|_QBbg@9x+6yeIEnxkgLnx6um!aK$X@zLhva(!@Tii)Y&CQa$uW@RX5vwpmja~P0-`yL z-ZR;YVY?<@1L_+Ug5EXROB~ZT`Ro5*q8tgiR*`_90+JC1=Baa{$L6+uK0c0rnm zqO75%0hA0CIPh{nrE~KS>GV#c)Hm~Dc=TCk2>UJGb(wM za_7h~OnEpqQFlbs(g`^UQy#JoAyoa&OTFWnB$|}xwmNY(P8>=n5JCbZd`TZG4CYjvYEC zK19ZlP?K{m3Kbt8q9*_|&E1E;mY<&s)+`>C!o@?7Op!)XR2xng5 zd2bF3s2tIPi51}&cB}~B+p!{ipDI?wWjRH($WfaG)EczBCW@RQS|kH(dRjaOY_HiE zLr1!3ed9$kKr08|!y)WAG~qL}MzAK27)giKFJ|NfU_aBLa1tpbjTuRYUxvtwHnP~L zk&kgSI(jtbRd~*74CMTe@iP-O@-v2z9W}xckiXI9*b00U@lfFvejx`wY$S;rIRoM> z1+oUTyz?nrs<@HN@v?jW3Y?D;?^S{~MdXMN-h|i@io9t-Ky87hF{X$eNkk~Ke*s|k zC%j0SB6`FZUJv7Q0e`uTBXko*OA$ZfiwM<8f#7`yNxBFUeD5z_>ANzm}PF(#Dhc zp!Sa_*$;Rt9o%k2372?xrOkf`cE%e~lF@>-{D)~ktMiw$+|_$ zPr5kbB1?R6*s&#rgW&a{2@fHPq%rx%mL$dGM3*cDs-t4jy}(%}+-aO~(Ivh(AtB>S zZUd$V9Bi*pLf`ljUmQBZbk8EnhWezfOCUlX_e)^6hS+jHfx&Ai_{EtdQqcbAL?D=9BS{`o;=y3se*#21frqo8>hD4uiBNLTtsoA{zhsxKwDhZY6Y_Go$$epN@ewgRhWS2CdDc?k*orvz2mGPL@hN5WjXOz1VIoh2i3Pi`Pce#$18O*$QH5{LyHrTFC6a0&SVknG3*WSNi8$;C6k+2JfWF>k!#DF?aumYE zhXDWFWx_WGB@u-ig(525Ve5dJfMz7IFdM?{X-9?~haxCe0DPs3!Z!{jkq=KKib!)i zFx=~6@r^`D#Nx!F2#y~CZw+&V@r^}E#9>FHZ~|M?0Y1}3;Tw&Th{D~=X;*^gc%b;5 zgT^-=B@v4!BIOalK2Lc13$j-GN2H`cg4KaBDgCwys3J5MQ%D;rznGLnbUslj6~I2;H39wzKOFJVDM^q7IqYr)g1-_8m0|kZH$Ej1iC=_DpAiAo56y$lFG3{| znT}Brx%LG7LgH|Tpl^&yG9)6`DL`<3EF->ADv3y(I2B>$CE(a#Gm|7`UJ4w-OX0Sd z320`(`9-QEqOfCCgo}d!T|o@u#TWa=sw77tOk4`^t6V1hqE&n_n0OU#Yg%st`hAxP z-*}beD83?!VI%P*fTlI!8?lmzAu(oU8Bnwljf@a&Rs3RBQsUB4E4Kp8UmaB6ix=e= zwUUS`CT``wKoQu^?x0;6JJD}>OxS-Sf{sP5h)1&=q>ix3g}4}M+v*#+(gl=7S3>zc zQ2kFVWl@@M>`E6Nqq#5=0T&S@nxv=>m3i{ECqN9Ax`!A4A&p#C_{M zCeqJ3f<-9Tf@*6FW&a44&eLNIi;#RBe6Zjb%y1 znKYWE7T8uh7$urT^XeDPk_vAJ<5@lg?VlX_93NhV`NgyNFySB3GINK3nuq35mTyFh z55lA|E$0K@r8dqaF)cp$e4<*|d4Ugr>RY07Jf;*;Ek1aY#I;Nxi)}_UE&0cA@O+2I%$*^&2&a!J2a-_b~y&QkP^cM+u16`q+T2c46yNk_a? zN0(GE^G|iOF4~G5QcX?uwbiv#r}7n57NZTVx?ONt+3qi0vicp)q+DPy%jWm?a+D9r8A?e=*(k*_YENNzC-2-m&~lLB7=V1(aD~K9CS62R(v%+ z5@~CRwnXGF<5cA&*nTvv)r-zUR+^mi5jXi;YfrrPH-Rag93phgmSXfG)f^;$9F$J?Tw*_TouJY|(egFn1^bUJj(>bJz={TJ{YAOhQ zPC|n}L-2l_8<656t9F_w2B9ZZM4cM7Nj z&_ookx*EEQj##!^LE^6tnZUVF+hP_K>wjgJkkN;<5QhV+cv?V>M)QcIY5lRQXo*N9 zbu!4D=a3SyhD@bcc1p_5&z_c*6;0Yk27UmgzL=+4FquOqSf1&bl?7YcCCczxuq(jY zpU_+j#_17?vtTD$uVL46rMMg_|5vaEUN2@Gyci#67Vf-cP(+5){!0eVKm=eV9Mk@1 zX>U+Gi=}=Rd0g3ie=n7}61Ar~Z+bxOi;AofR`)#<8#bM_=TlUU1!zQ8A- z^(~srB#v?2M_1yV>kBCH1^8FT7r;MG=sAiL{JhLhG@vrzL_d0kPe7 zw*|`;$K{Exq9v};rNDj_T3$U>H(e+^(}fho$l?WSecsG~Dnt`fVrk72pb&kzYl&i1 ztOl*)9C|V|v<6@6LQ?ZM{@wwV&m2mtsY&g@6!|aO9hhC(V*)J>yM<*4eL~Zk6v;Hb zZM%}5h(>Y`g48n(IT25LO?z}1IT{b+IYGw2ggp?>ilzmWnYK2@bMmv}neV5-LiYcA z*9`(L1hX;1%dj(r<$!k%5$Vz&26!l%vnaIpgCoUnwKJt<;~Ej?aw+3-ajCHVt}S6o zPqGdKfS>z3do z_6lyL-hQy2!;fD=%e#^XJTW(KSIPiG9op_N>>t0=EGJU)QmJZ=9@^EsQ!8nc#82Y-iWVe|>GR>+~8^&Nga zY#f$CZg>k5S_QbxkH4gybcsT9(|c^4HE@AD1Boer78GI6z~4ZCP9y}VPn?1K1N$Rr zq0@jm)P#4L%)mrMoPor(`95A;p=u(`U7`>+{$gE=r;uvH%UA|2pipo;rj(*$vZdQ1awfTMl)wmS*=Qb<6 z(%NJZHa7%8%d@~z_y?;#h-;KBFJa6TULzlJTkgUkcHySy9?Hnm@jYPqLA1R8Q860H4d$W8{W+`0Bt;^#qP|RRv=A46(F9c}kH$me`%8BC z=E;mxi-D@bLD!$?wqQ-YhH82w&8qM&qUG^@CVa}3P#YGB=#Ic@b_cNEM+$?8eB+}X zol!fc2u~)swV^{Nh~&1N+nrPS+O5zjms|fB*jF{7zD=t>(3J8>s_;^4DZbAi0?3oC zt8mCx_%Dl4DjVTfV$O&4DCkdHTWxOz~$EUzX17f2YPQp7rKLnb3_%sn+lN( zlpPi$2_BkouLn!<(M0ueD$w{V&7AXr)j)HcjY?lU9>NUyg`cNH6?c=lT?HNllE-aC z>J%%oFw(HHvAJ3m{+J5WWcznz%M_zA17Vmj&4=rDT%o3ShLj%-B4U^*(ZWkaMRt@Xk6^;r#kHU9W4a?mX zZXbC5fBeAv$%Uf>a!y^t3(AFK1GKDG4R0tHjteZ$;Rjw(E-VQgzbQZD5_M_d>M%dh zJuVy{;LNo0--W!zGWvInyTMgyo80(ePY|nge+Yw?w=V~gT)QpYg%}pL+DY#X0vYgD zC4Re=#aARM5&h_A%XaL3oaEcAAlhPOVBOLshI7S@6 zdkI(vpk@M!<&Z1Ag#&yEnv1|f?g~I$>yXn>1^(m=m=JpvAa6Lt-otK-ai{yro;Xvp=AHHJ6jbdKEBlxNv;(DxBdD=RRJRC6Z*&^q zXT{>+ozKE1U#|6KQDM6cK*tc!Wm^DS{b_4RVf#iv-4#o6$XRr8R{7HO-roHPVE>6F zDwj(Of8z`4+$a@}x2?dgJDM>H%zR#Wyg#=NpfG$GfR+$Y+XP+$EnMqM(7rn=^v(mw zB`!SyJH?-#4JYKD1I$YfxgosnS$M8LxoVx&+zM18G968eCf7R)FYpEN-07@T)>Och z*>E8#gDM9sg;)3j+vg_qkJ6*O%Y6T&JeK+QZJZOsbt9+ScR+)XnK6o^o8gG8r=Wv2yXbN z26;0OpAd`OUDY-BpLB#mvc+B``TzvKjU}r65MG-t{L+_R%w5@$)kueoW+nhLeHVV? z3udlICeg80a62&l#>N(+g7sy{!k;>TlA}O`d>R;EkHM-@OG1UhzWUrRZfH4K6fmO0 z(SRC+rn_A_MGtExKPc?y3-6QhU?ET(V53n(m(~z? z^saD_FNk}e@^1Y80S9Jl?4VRNHm$}!g=|Nw!X13!OL&#d`$OJF@S6=hft39z?EKg> zMD!e(umq2>XrAdIbA+9sh626&FrW%2`E$YNc=u;`1`nJBjORF5seQrK95ov+QPuBm zUw;{UXmfG8Xa9`2)&tL5vAFb{bxt0d_)#G)El0yI7BhXm-h9O*C71aPsWwyrKx z(ZU0PvSpPja40|prl5#&Lk%G*54XOwrPS8jGg8TF3~6;vg5Ym#B$EuU`^eRxOQIEsxYi zp@{i&s>C-6)C1E|#Ky%?^LzOsZX(=9+y92YUK6ffCh(~Oj{@lnF>I>h#d)~S@9%!Q zw=WZMQO3aL*vKhHbHz6JcJS3?3VgP43e*DK>KK$EnJ;pzOr6S&Jvr>k-wHfnNLDP5 z?u+AO_n|HI-=mdQ^Yisb{3SUH7 zes?PHSo!DUn_bqzHs)L^esRe-S0?uYUApUG==1nXa}pz|x2y7(;}R5F-X|A8X-5mS zKXwl_xsYca3u33(1VblN0b0g8yN@ciW)RCo%HbLckeV*+u{Q-sB(spFpu22lyY?x?%VW}VV!0-2Go4CyiIw%wRj!Qm2$T47Me}1 zph-hv-3NLPJ2bb(e!a>W-z_wmD%qwwp$Vs7XhzaiIQhz%+#TIoQo<7meJh)%fkQa5 zZU*H$NL>tcu~6T+TV#$WKb+)CRv-x&nil>TYv=xuenz*@uWGM^(DV(gYe4Z&4%Olx z5EqlPXLqO^J0^8eoCex`J+wKXMxbeJ4>^;B7k3BY9h;*q7-D)&JQox%x2ftiQT7&c z4(krm_SOWV;H~~G*!n}$c8eB8{l*d}pr$)W!{@+SOu}nH^lXRlXc9iw;%P^YeYdPj z?Up_)0bwzkIdHG?(X`Y;+;JSoAY@w6|5EvMF0)Lbkk(1GhA+z@b~~$rQ0mL%0LYq9 z(Uxh}gmg}yj=lN}gubSnH*(J@!$b;cQQNr{_5!#2q2=Ap#~b>%I%rhf?|cg8y_}c! zT5)RCIiafkCmp&P@iwBj4rd)mE2Eo;ptK$5zi4@Hhz>iNh#+H%+E=|66#rsV4N3Y2 zEo=vJ6J$dgGi2CGY<;1Lz@cwlsb?4`%DwfTHoHT{q{WlE1XQneDF5VA?mz~Ej48uI zN}tqV(kg8IqiLNaTs`bSQiII+^IT&?&-8^4{Sdeah3QOpJkLS4w8dg`4DO1Vr$k#18L=txj$$WKkabHz6BNEnbZSW zk#_Wc2Wf9Z+MUTpkO6Jk6WHd6fEtCCw>>F#CP6`F6t%^>5EQSrscMVYnVba~lScOr zJQDE_Xd%nV5^wDQ#Cw z_JIq3<76KYe@UtGTAV{%GHEjzWJ)F@590!`u+Cv4 z!NW+K+#n-nJnjS(Tn$+_#efVoQRqzb9b`#Jc3wG1jwB`NMQ4ubBm|rYj_-h{t_c{v z$&(PIn`PPLem`XPV=n3&@N37lHni z<81LWPJ7T{^5TPC3xYSGBCOEe?j)4{LGuWl zCZ9rw>Dv48?Azai55c~j6hU@PGfj$+61%hcj64a8aQLYy9Fg-T`o}pqP!ppK z6_w1QxnvqMMk^fekGTCQCtJfEWNX+cMC$Xqm~0JQ&03dhoB1fVSu*WZCr`uDQFCxS zh082Sj)dIU9Czh$6Gf*9E^>c}#}Mi0hMZeLTaUEHoT1x+a6j}|zp;a~!gy~7X>EN0 zgS2+WH|c}4d@_e8d-@aq-&zN>H=^Y|cM-IaJ}xUe0{Yl_lQXdYg(fsYC7_l;C|34_ z^!~1O1StJ3hFa*=KRF~*MPFr4QU_dp3Tpq0rMQ9GOdoKS9Yw}i9dKqqRikNLW*B3Y z9U0j;wH^eKXBKf3xIhLN|8cBc>eGipSmnn%inru76&nIF`5)s-lNrJpD%cLMCX#@U{VI`Nu>L@x4 zsSPNOK+C(5VO|-AA^GU8%#J*I6v$p6!W)4wM90nJIp&jn?^#{i$fE?EIL~|iCpBGy z;{Z`ehl7p$Byr=5&b$yw5YX}-6XkR?z9QH;bm8}C&M5(BW7qK3M-g1b&?-J`k;7}@J8J*-+U5r&9O*#}Qrn;a2iqFK70pvN) zgUUaJ+;4+$=xZvV^ep1HA{ZOM5E`@}Ot4n9%e$r8M6nZ7eg-Nb6p+5fPGk?*p5Fj$0Ok2y93l7awv*;or%;*N>Rik!R;{1CZKsl>`ZWuKS9r754tdL3Fuwx z(3CmnS`~BHBTKQ$Z-d?OuJJMPIPsPvCIsiftDF!VaU3!X!SNBmjk;NUML->lmN$Y! zA;YMf6c{mq5NLBfFy3xs4VAj6(+1k4Mvc&qjjlxgKQv)DG{r@oHnt`OYT0zD0MKJ12IHL& zeiCn5(?y)b|6ZHo%HTdYEh#63v_U4>NUS%x7<(}2I%I-G%ew>`53S>PN)uO-B^@0~ zvIZnhb;#UGGHF6dvLWM))q8;0`W$qAaA=80X`)B6q$MH=sROSMs3@A&o`{tuU?dw- z_Qogs>0wZL&Y?u&>0(5(p_CXFvM0vf-e@{3gu`8jg>cM^v=SD=6Vt52LK230Mv)*1 zP$u}}eK2VUe@M~gOoKn-5jy*Si1(O@lN>wV<8kQP*@yKxj+ZhXeh270l)v{%U`niQ zF~~WTf2h?yl;?M`hw@1=CqJ5xdtN|@#4^YI|&d)>Zd zoEw88@SIif15`|A77`>(?(rr^tRO$tuX7^$ghkJ6T>t)k+w{07<+jl@@*h zBS1nS9wDo%ZVjlT(G0817BgUrUy@=aoZ=3$I_x$qn`nmBYUIEhU~&`wB8ET{GbkR$ zV1;ME<=@GvOs!(!bl5LwYJt;Q#WloQd3!(|i>4>nF-Sm#Lfaez+y8>tt*^HgJK_$k z>u5%?!fEL*i&LZ6=fUL$o70e1Y!!^bfT_Qz$yH|$gk$bR5FnaNuF!R2W$)$;oBj(@ zZ_{32S7zAQyYT#vrpK*jx0UKG`z5KjzSdUn8{qXL^2-&$sP|INv+`e%dh5$=^`3fn zKwW_5RWB2P519W;QZKTI*D6`Xro(S5NAsxHb@cOT>3p8AsIb&zyT_$DSd~va|8_|6cgDy0gI5Vj1rg6plYUcemi=<%qOkdv3 z@ae7ahJ@&p@^?h|He^;i@th&;H_GNAo#cReKn~% z+S1zGxVjuFC@X7UfV8Re7e&;fze-8kCdI*_`;9|GZM7W9UdP&nzfuS?1(%J0xUH&{ zm}f7m#lw<~-PCJd@1@Z`13mZvzFGO%eC##LD(}(_9mR4zlS^+S_x9liv8>@ zUCH^*VwJ}{2n#-XD7cR;s3`+g4y&wT_{nx?c{47-&tmFT+P}gn6DpKhbMdSF?O%re z%P|mBS5tnj=R%xzvQlb70nZpe#(s)xdL{jVpA3i_qlGa;QuFLTWB4V^X;23E+Cs!FQP8 zTZG({;b?XYO7d!z>+NXQMIwE?4Bjb8j4s zn^>cN1$O6bg29--20xq2vV!5OZpBDZqq!c18`#JBm*H=lKZ>2i@QSnyOhI00<`7-f z6jQ&i0V4bWH0luu)kb+VLdIlpuONl;lVD^A?geZMi19aqk+s#rtJ7oTje;2NDB$_P zbE$(XcNjpEo0P%KmP0D7PU zPDj9i|27kF&B9rLzcL1gmk%j_o2rMP1ep-80mXX`8a*LIjVACVc^?Y~#2XEI4CjAn zS{G-#x;QffVdf!XNy}3O6fH4mj69cNWe8uE4wp=uYXE<fn&+z)@@vZ5~_i#XE~_M9F!y{s_FcI=F>Dso5CbCOKNC z{^BgmOzRUse`RCPg&V_T2<$20;$_N#UP$OsaIMFoqz+?vVsg@^7NZc30&F=! z#X4zFDH)rDuz?k_mjmt^hpd!i4DXqotXqq|G7-2lP=F!y6{w>kL;_f~!s1pQus5kS+jOX0Ba6-3~k z>3U~~Q$0*s<$zj5NOpIK$dm@ZEdy{^Wz zR55En zNx=PTW9|=$o;`rPO73QF7`| z*-Bxc;6)?>MKi{W^)AB)ry%Y?3;lfoc@UvxsVHXk%5ZgZ(sl()p?n75&T}aXY+VY< zHn0$W7Jx6hgjxPFd_)SurUoW=E3ME=0o5PPh%B&cQV}-5&k=YQ0Ot}|B$gtY;WLvH z_Egd|Q~yK&pJh`wv;JSy+f^B1nJ4z+86bGYL9$*v4;FX~-km!OZV_I)bJTY@uI!`N%w;Y7GFRgtg5XcF zL}})Y+F8#te1tv|^;3Z<>X~s*>s~(Om4I4`<_fCIe1^~MI!Xwx(!`zwq1R%F$+%Jy zbR51eIYm2^m`{NHUWNTf(;V|@LrF2t@SVxw-HM!Z0lCnIRzr5;erEr^NmsoR)(w=B zk={y}+8_ij0G@R=E?J3;Rd^O9c*0vI$68tftai2oFJcw=6!^Z2!Krp(Ny~n3CFjwp zMroI|H2h6@4Lc=hT2tJ>F#Jhr%LkG_Gs$Xcq-O2OvUG#WwjSmoEASUkecYkU8Rb<^ z4P2#ugHpIk+-$PdQ_<8EZK!6&(A*6HH2_VEBkr577`k@~R7uN*E(WH9ZEQ-4Wrj~? zmnHKcO1T3TTQQS71TF)T8;OYPiWbwYm>Iq>`6!SwuhC`%$3-eyBNUQbbt>=$ko{^_tBPlnhuRM0ea2fwkWH>^T<4%6?oVaT7-NV)lc=B?Hkm`v!DmY)d;G2! z&>X3&ZE2kb6w?#0>r~WK@(xVZa;Ewmar16(+rdQMoSE;R<>bsPJ=^5WSTr%<9G1c_ zIaNEaMYJSBKKp-$ottCF!QNXE<1(i0l$+zjKXKkQIf88xrei4?7umC-Wht&gS4Ybi zwoYiOsBJFCMn(xvZI(ovqYZclUD?d7l;-kD6b4RT*<`!`zNNKHo|*0CwegdHYD3GL ze+B5r#FBOkNo-b4!5aHMNPcOP4IM1BXCO9dj6{=Fly^OC9V>C@r^x?`CgSQ?iPNm+ zXk~3fwZ4l@T~5=F4RS3aL8H@*LUGI5)~Lkkk6C3s4aB40#o^SL)swzNM~}l%=|J?Y zIz5Jj-Hq^9ooTRO);V^EBb1T~7S8Op5kIif~(h&^m!-&YET=AMH9szDF^`y+(8 z&Al1BmV+K%e_3DEXYe90I%tsIn8YNC zsJUbOJ9$2cd_54qgyFrLF?GfNhFN|W!Gdv-_}N=r^C+_cLcN8S z_giW&#@2QWCng?tTdnb51=Msj!5cbLkb8}32as!Pc`}fmXCv0uayHkjF>ARSh9|YG z*B)phg>Bz4V%8>_lnRW8GGz@!FNh`Ony4j|MA>A$p++%I`#(L>)9LZlh zG(1KRT~blkg7P^`mcezPv9S*S^b9$UO>88L``X<|;Ct2gJ1n2MgYKL8onz>qycg?+ zG4$8%<8SC=7TO7s7!{RcPG;j)j5Dr9CC9s7=${2^ucGDMfJqxVn={vEQRp@%=F!<$ z@SlJ>1WogB4SDccK)Qzqy$FDbyFhjWn0Vh|8wkSrPRwup;soDxa32kJK8BMC+pU z(T3J(POkY5nLb*d@fZIU424fY9LxAixGY;cAZL@}%|A=_?ZxNqBhGDW1?r7y&pMSS z-5FafuwAq2C))6ZP+L`NNehnejNc!D9Fe7X{EFf-oZolx%fQ^m>PT}%LrqkmY8z@2 zsmzQnsc5Tf)jzA2-zENNesgVo6x2yO($rX2TeUjzM|_|Vo!hyybUr8VdW9|=RPe`} z_6w@=Q*XrrVU=I=g#2^(tMX6rKjojZ-{n6!^FGHvqqoN;ih@T=zEElfjwmbJs+LDv zTU5bhtMp0?vl`x+C>UdXbQ>VtAK;j_t)W#FOtG%}0mE?gRhUNwj|{hwM3*!+%b=rdi7_QmQ}y(gL-juAOE~70k3w2DU9me@M3P zq2myZh*q^~WU#1$`zf0h9*Ll?f+y%_TKP+V#K7h?lY%EXh%LGNQScN!4J-e6{&6T= z-h!v^$B~GYe=fe*EMl|O(u(Ucs^H~Ez*PP<_|xb9~l`*tP%UGh*dWNbnRtPRr@| zFMDbX`Ouo=$P zq4uYMYD5!lsVz9KB{-$7uCYoL%+u>#FB^6xyiMn-0h%hqP&(^pBmqIo+l%va@n7Ma z6fEeS`eB5MS!-=`+k$8<&b2#LP(6$$S_;wRtHIl0GpxYevFc?*$wd7nC@Yx-Tc&=n zf+`fS6-bqOwP<#`{!fc^Juopx27_t`G(9QBe2H6PdldZ{vfTR?P6;#AevsA#X6ODG zO8fN*s!BAyHp=W2ooV;0sdp}raBGZ`+C@o6_6@3I(e#oi(Srrw>CpFHHt!@RI5q}z zR_YkcJ92{RaWrlIBeY&_3}%63QtWkJY<5~?c@y8vCiOi{oVModt_lb|DD1YgfZM1lmvgk_a@swtG&C?fldf7s%r|B%5(!IJ#P`!($)mHD$yJ&A!%)Euv%a^|j zs{f*Cz0h$m_UQQDsuxVZS;mT6{rU%05t=({ye9j?-l`%F9lg$3CHX-$1I-;jT0=#$ zsQtdTE^05FibU^F?$o{4vF7F4gHX-(dW6DHZf(%ylqPDu?^a6qs0u5>&Amc1h|x~xwI z2GzG{T4CZ6XyrWIE5{m?BQAmHCbuA{2B2x(h+Cj_^FnXcP11R&2h)@-f@&t3JNwko zquqlxRq$A^bQW|Z+v38Ysz4Lv=twq6Ls0NyuT+X1@k0N5#g=&VMbqPkb5Ca?D|n|@ zPBbUS+A}!9w@%$EsLn-aChjQHuf@)_pqZp=oF9@3Ch`nsfxg;TfJtG&b1GWjO2%6ClQ*jn-)H^lg%`z&}s%>~s z9f78Gv-R@m>PU2jDhT&Z^-R?A!{#u;)jD%TP@Ruv22KlOx(4)4&6Jdy&>9JCwvF;% zwhgK~(TswQ;-PdwFICV)R#Ng)(kml_>Yr#vNkt5Z-l~@>sl+HqR`z9jW&duwpbCyM z`q6=ZTkD^xf}-B3r-@pAWGS5x(MlS;eNc@=lR*?Z*4?WZMh*44_#*R2zD#{UJ3V!D zP?e$SjU29PIw^d?s9vcYc>p!4*>_A(9fsynjil-?*r9jMEN9?+&4JUhLr@)!re~FB z;Oy8twPQw(=;pMsL3JUTM>l%^VruWyjlF-dOo@7K7zft}&8r^S#F)`L)uT5tjB=hT z399vIUgeyRv>OFwy;C_QC3TGz)sZC?EGY=*`2^lD{Gv3dzCjZusp60LbTPk|>Sssi zGNHcT2_2ovbK=b=1l5*kT1P`fM~kSVUgz=PdV_u6(x?b=E;Dh8HEGA7+6_&w@YI?r zuHx?4Mmqa%FWUo}uml;6wBsxX6N@L}{1Z*9M?21%7T9bp@JNL0?covwG$HDk+JsXhSA)x!AAgSxaqDS@%r| zswdFA^GwuK**nLPkDg{PgPz`>8dP7Rd52Ru@7*u!wM)O9S0Mt+9bHwXFS^HM`kQFd zBgtM!V7qBSH33blkazr4v8b@&|5T_|Nb99{*8+=o4yq++?syA5h(}+wHSQL1sl+uv zNk_qyimKq(y>znjc6Yos5alUqJO=|TaLM$bx*1KYkvj#l2^XhJt;ChRbQrR7h$xY2 zu(ikO8Q8E(P?5rv-fVadspw(u8%%aY8OR$F4+z9A1zM3=C}enX65uI)M%(}Y-^GHw5s55 zy;H;E{Ip@~H!**WnAiXAj;NQJG3w>_vPzk`(AH49qAhAvJ3Oa1?g2<#AcPg3VH$fYzywYV0XGfH1`Jp|6>J>Dt zW!hhH5VV$yS=_4Nqh6_0-ei{&U}f02OIps#dFT#=_YA5*Xj+j>r(n)JMTdcndgJWR zK!-@A;OcOGIgb9&v<{g}!K=e(d-*stCNp6@O66q99KU9EP~C&(v7}t8%p7O+%7VLk zrEcxQoH{$6GK{Xvsk3|koqrK01v-TVlXf513s18Nb`q7))GDD~K$`!;jx{$vz?F_|D zVLeGNoEKDoMAK^IBn`<-m3p)VwY^i9RdrDm7eNMp0L_vVNeZ5bCqY3iirlF z{XY$+)F2az&0B=1e>9HM0QvNFbd)U6P;LJWgY~fa@;dv?oG&|UOIK-{UKeLdDlyKWT|&+?xEnibf;nX zzE7A5#!Dy3?{i<|_tW)GflnHbj#O|do6ov4I9UU+yV3IYU=f0y@GeC9Y8RooTi4Qr zi?P)KK~%&v^wh4r6j(Y7R2aU}C8A9*SM4Z;wHB04Bel7#0u& zq<(V9?STUEs&rmgk~0@_g;*4$YB`#gZ4Bz{NX>?P=4^>6sbdNU$P=tCaMPi(5h#P} zLE#qOlIp=H=Kc??=&HDDbDv@1iWnYr2?Gy&*R!xD_ZZa5vJ3M@SwSg=1*3+JPv#NO zfGG@N7abd6g#W>zyf*h#RyP}oQroe0?&B(wPiSKept6k)399YC!LS@Y>nGIS7|pr` z!}nwvj)F#%{;NQcrKXtyzNu5&E9!8r(7L)Qs)o-tJU91BaL3Pr zNa2su@H5pKhgU&v*peONzAoQuSxIS`2#$1q?dnRvu0zZFbq+33?hx54+92G9cZkd& zqlWc~QhEyILmHd4~Bz-+gVe6`f=6BAoG$)>-Sggb#CmkN= z#xvJ4a&b&0tf6)htPV|3DQp!8vFxFqh`g-~wQm0g|;R&IojH zmU5nst+Os&0-BFv^%>h}aeC%oMJ8A%er&775}&WmqlmSY5%g$p%prXFrqRoZbp{7FNL}E#k4nGx%gE|EZfP&!5vS#9xXb z9(sTrY;`a7aA#L@P%^GM8YBLE@z3|Gw#BD1j1GA3f9!^?=D@vCuk%h)-%peL_I!li zDQ{yCeYdNLj-S*b)r%sH4X75?fYQhf(Pq4BTFs9w@EfaIYa1I_iV%q&IX)v%o29Kn zTY#lUyPBmb4q+Zt%w9MS&%ER9XWn*>%=wEVd~QW)w_jlfp_*Hh_}=p1`V-jG53YLD zN+n%+aNVjM#h}Lit*hCc#DdVxjjN?HRGH16I@`;c%ZmH3tc(#_MSn8z|8bxv$n2@yO;_Suv$b5Js%hU-DITAP0rMDMp$;B9l@exI}eMnl*LW!9e zhqM&3umqsb0aS}orRmEK5C@iOlNHfCrs-1aaHM07A3NnFd#ms{-b**r!6Zr{mRKw2 zWV*YB>n3Utu}@n=V{>)18IOqd6-`=>OrBi)D>x?B!SjEL>bL_nsHid@`;()YqPnPh zbG+RMTct}?QS{yA7>H7nuxL4}5nB2G#Wzxuu&8dee6dL#zSyKcAnOl%3J)*&L-2)^ zg^V_D0B{#8oGrZ=iPjHkW6Yt zUEh;Tf=3jywf1TVs{PPJS)nIfW^U|BW~9ci^$PfS$KmC9@}hh1ww~k#ieaZk8-uC| zO)JLJOZWFMGg7UE9DNIJHfpjtVpn+w&l5e#7W>QS=@f8uiNh5;&CAuRJ;{~-9{(hr zg&Y%drp@z=6$to4(|s#h$dk{?w|Y_pnhy5$JK*l$4u`^=X9&L6(=1J}HFrdFP@RsZ zHRmeL!`yd0$y~}Vry@tUp!5S;d{=uI{^=LPu%qU;!SR26xRzx@SeHcSMLFhauLssv zr%e6JnBvx&)}T5SO{-Ow4b4%gM>&#pLm1ku4MkAUG()m>Xoj}vQHGL^M5R28p&B{SNs2RhHsO2H>@dSC%#)kQziXuJ8i=9bYn3;b> zP%TC?%-n&vpQ7O=F1!c&2yrRA(`)vi8LMQy4Q@UqN1Rfj_rxFzTeEteE$>lhvS8>* zp6d$mV6l}hBW~T1Nd1MT#h|XWEJH+=r9Cf8^5xmu_b5CQplM$9(##0>qAz=nO(*Q! zJzUO2<67C9AC1r_G*{XBibK&i|DOtwcss*8STyS|fbG{E);V+x>l>L0P(3gUjqVrV zOh&J<~ENN@dCz0)n z`?A)%;OisuCJUsUZoFvM9^}l;@y(54tNmCM0YWpYBvXSGE$G3HGwI4OJ!Ng_yC$gm zqj{&N@U*xGRb=M~15@HjTnL7)CR6OSkhU4j5mr>)gG@Pzxok@|?zo`Z3C+tDGfow) z=(+BT&+-J`BpcS#;Ok}bCZ3MgnDExtgBtT;PEt_S9v@V#XkNwrgX8{$o>N@yDnzS5 zTi;-PfvlL^|3tZjhQ!kuH#HDbLqB&PDTyjt5ytwMs;WKkXkU`R*AiuwofcH{(ey}Xo^j_!dQrcgh5PC_aEA@2m;J`{Zq>(@w4YzN5@1cnVUX6 zFh2DajrHK`Ei_?CU(s;xqRO?W^_TV4*Gu)y)A)@yEHrmc;kjT$E4WL11!T(3g8_x@;LR;e<+)=#qh_(eNk9N??cv*Z1jsXYNbO^0J zOb6awz~ytm`vB~4ce_k%aVNRG@(~MeiOV3ej(tD|Ea?f-~9+xX~0;`!MP<0U7 z2<0McL(W4EKx59^r$Tdy01jPTf?AgIwF9s`hnmkwr4mG}@3lIN*v7+V$SdI}6T91? zVBJx?XnSJrCFi-?=bemhVbs1M+#yEIUE)2?cNsk(fzcG}g6A4Wcg4F`Ha6C!TUv51 zu=zhO_iRqSj3?PILVy!_Jp7t2$_5gF24t)iEP3}<_x@y0MY9DE*TeI&E zRxPb)=42Rg^WX5>(VxM?8ga|8c}k7m9P9mv+nz^m%F(?1H{$lU7T_-W<#;?9amO-T z{Tnt4&H=ykF$bav=K|m~5}9s{N&ky%mKt-u0&;(rbXEaj?B3;o(2U*hGWLMW*n^iZ zvKf1*-DZs6C1WfSsamzFq-5&U%8HiSD%?G9u5G9hCXGoUzg`8|*Pr7g8fwe*u1BWF z`AmX4u(d6bC3vW6C2HK$hl;l;u!=4Ys`IQqyb8Zfp*0YnM$LggB5P5TI)?8?Ml@yn zZnh7DzYI6b@*&30PR{7B;DE<`_z3*f3eqT-&;|KH6Ga zWwW`bbvxj4$Sc=K!{IWkx(#^r821jPIlpy=(usI0W1QcyTPm8Pk(P@3 zsGGFGgLCxI7)l(u+eCf_jqP+_RG5zT8IukmRI52OQ)Q=1-KoMv5UF5gipk=;!hu?z z6O^1A3Cp`f(G|duyukr(%-M{<$62jyO?A--?qk+R8(OE0Lw*LC-SYLcs-~v;+UnY= zQ?YvEzF}P*?rGLXTUsh=q7nWf5^V>Ry;N66msB7DbEF#BTB8{PPalf^DXaf;eIl0y zRSnZY^nanxW9MU&9_b`%B`1R(0k5BcfTTp<#E};IHDaCvOxo*e%;9A8fpUNRpy$Uj zMKje&<_d6s=}J5I_pY?(os%f-j9I?YO6Qnl;!Um1u996ErU#SthWkUq3_Q9=!Hibe z@|>aD;Sn%X9aOi~*RRfED8&EX)yE;1yZX5M!bE)>^haNPI3u(bxxN}PI9n^c>|kU; zysLrR7B6zO?|5+VG&MKYG*{I74Rej`D-W{eX^^F!HX7e04 zF_fIWGd$>4`m=|zuSwtQ`K?}^;{69W{@Q)Wxze_$UZ3Hii*0*)N54yqJ>_>kGI*z{ zroIzU+P#uwmh#Cb+CFt^>uL?s-~pL=k}sV&FD;{I4H_T}lczn{B@LOM7c2O&n}cFM z2!4nnp^I&sPt4uqDqEtpxpV0{jWmm$*V~{z%ML#VKNZKT>?k@}zqKC4z4o$`^}*1D zSg`W>Rb?gPobMXT7F4P20pErex`BN>CNjUZS$k`vH$|{a*(`;>rJ2McNir*ua^$+R zcEHu{tUa|AL6UJcd(P3=o9ql93Dyb=#fcuGlgB3#J-xF;lUu3aA6$92z95nGgCy;B zCOCoQxm{d_C8dBjF2hpe=J-A<`HwQ~K5(#}n7bO7^z2!iyKl0w=6XbBhyMovX2j8Z zPBY@>#1A>7L{OIi#YbqN_3oe(PuvW;I{a;9WTwtd5J8&IX|lfphBrBE@VcPdhtEus zxWInmhgLSJ)`Py(3s}xf+htRn-}t)|nl<>S0~iHH_c zFP76Y=K%Yj<6LCrdrvZExn!I>NSXI=LhPI>2eZtaNVXB+Hu+k^_ET%Xw)5g;n^v#2 zxuR-$=DbaDU^F4<=W?6S`)7fW^P(0wN{w?KvLqQVzknM#cZ}$+8F@TNJ1>6Af^?Dl z;8oYyP~*DVT&Rpow-m!i5BJ8LQS(x}0&aJVGSGkUK-1ZRNs=uwd%4p$&)v?A(~Nkz z*|=5cxIP0u559R*P@Qa5f!hIJ1=-aqP;(I;!3TUNpR5`_gb(;JIQtY>k{UjS5BQ(? z3m?P>1p18nGyXDUG2+4oIA?P)oR`n<@USd(Z)e~8N5p@k<-N^`T_p5!?E|i^rpK@# zYwUFhf<+UGp(4;$#WS#S4d_p@66V>ZbSGdlAc`Dtq-0$W!Z(xnXB4;i0K5#?tb6EI zljt5KT6{eYLC_4*b+}x33vV6X{swTl-~o@FG!gaCp21S?cE$x)4N6FroBK|#?+xVBJ~TJ z7FiQ+rO0L7BXWJME%Ga%`VJ|3M81?GtGru8t}nMm-Uo}v!DwEQ8CE)Ae)ousnATct zsh$GW^`z{IEIVbgg5`6*#V?=wF_BB&t+#lVJb4s!mOOrPmb?vk^mLXy{TH+3y$uX$ zmb~$xXYz`y{Buwpg_gGoa}uRqxoj|;Dz&^D>GqJG(FQ!sRZ~YfM9iSP~VxE(kN*mOWJ_KM(ZBRoxwZ+rC({Ksz zmjZYl5wNF1MNpzDPHDI_JnfJ0{!hSu=rEw+Q`;Xc0nLaoP=rxE3eC$vN;9J2X$^#d z2w;~Hp8aM7NNo)SflO){Z@&ldb+HV{q?S3FPab1-JM@_&<{3|#`468u*W`W<4B8Dp zF4y9qkGBMR$Hw@-pWNXw1}C3_pF75YpB!WG@!KB%SO3KrgV)|k>;F#FB#k{d;ntu! z1ubthxl3&iJdk1%9s=C6Hf*Q}@6wqB8-r~X2xEnAga3n;H<>J?u?jW@?I#kv9H8@D z@YIIE2G<@R!A}I}sV;a*+u%UcJj?{Q)}w%Yn$V)Y)aJnhU8^`StUk8~RX;R=&cs64 zD8xtz1G4}=*G7@XpYlK(295{h={9smcEZLXK8^_S1mIsJ4t8H?w2Xq3rour%Yav2} zG42b{v>r5y)YihrA%2IjF(2>;5r>D3lm^2=L7O3LoCWYpRsubOt?u<00d7OMMu49jBT(Qq0{Sn;2rL4IG#-Ix|3l{+JMB*7 zKS0adFTEXz1r}5AEI?ni!9&~NT{=_XL1J42f>~SKh4^2zys8YWfd`2gggL};AfOL( zF{CyK9t>g=rW3=(fWE@TkkTf25zs6&(k%P~!0!-&Xfw50h(%DVzQpLUhTM&PPc%W0 ziDmF$5#u0iR0G2@8%rAF5R1UDaW#N%vJqrtA3SKpM2Iw>0L8aN!#)j_$mmFEBD`2= zB}AIidyx78P3uHsNo^%OXv9zmGpm8(IHK_|lhROlvCvisGxr0-lX1+XwiO;UVlHH? z=3uJkqj{JSb0PCSpZxlG8P47%eGT`6_Bbkl&GtCva0>a^8~6F6{sE7*;ij-_ZTQKt zHq~BhqyJ*8%@x3q#@aZ0(c0GhNc(gjgl;E)seKMFs92iG0NmXM4xI^9=`4+d!nQL6 zvW@|c-xANUWFn2%;h+#Jb0^{d3fMk{;2r5A{^i$dx7n~O6R4{Dq z0?;yoiw;v;7%zCO;sCLZ2IyKFJQMrk;1KH~EIbJK$88*Gtcw@iuweZac{$NUf{bj7 zgG4Ng$gu|y>_a5%#n5FCEzNU^i-UGWhb5D1R7z+%8Wn%-yc~C$F4#) ziD=y(w`fk&w|vM}!}6Rb?^S-Pme5i;dY|R_@pb-DO->Mm=Xi%p#Il^@Jiv0glTe&M zpYOQGOwz|5MRaK~70Qo73-ymdep6_H#ZK}!c^F^tkOzepa!}W%6NMIH?%9lsIP7<# z{)e{?!whCj2e%v0Lg&yDO~kuQGNyMwW0GSX+#LCcm$%R;GMGV*^=@D=E-U*CWM!WM z1rC1HW{|2h8cd&+y&D*GGu7cb&J5&n2S0x%cbC&u$e_qWH}I52=67(+sZ)jdq&b$% z@0?Pxrr%raD<(jvTW7ljW8^$;+q$*6r`!x%7a#c3jVxfV4e>-qd#Mo?(F-|DGG&nn6)6KSN65JQgA2ZF3?!C z8YeyJsF5Wq(x@?AeS?Y6%z2qu|glwf8iN7O!2$2X4wXLW3dxJOxS0uHHrzaq&t27Z^F7b` z31aZEzTg=n2A}Rl41OEpC;cMN{+Wrv5!cmY&mLT53EXplykv9FtxR_m@l!}%G8{#+ z17QdHb;+EWF9$dYO!K_vq-TxFd2Az0^V<+V=@&WFNw0EeQu^5lu_>uTqNc}LkW4@r z?(Y;FOozUQ%uk0ysF21U5r2-+njuDOhJ^c-q11$Jw8|q3_lzhWpU-^W^xPx_c1h1k z1Ry_!>~~#c?mDbSG4t>lY<=y{Ivt{~R;+bmWiO3@*?`$ zA+o5swjImmVirSPgj%RAN}eKp9`9oxBgJl-Ai<|VOM4KFIZK+cCMS7-P99Qjo_sg^ zn~i9Bj$Eiug2=498T3&$v4`fjaSywxXIeJ+Iem@B{^tnVm~;KExHgg~DD0wsNZ;=7 zu2Sm!uLu|iM4svQ_xyZ1#Qe3`JJy);_*h?ebGkST~#mjV14|lj6Qx7L6ZtJT9qjj72qV+EC;?5=V zP6+rtM$Pj*0OES=6ri9 zBvQMieE*hatWmh&h-*XTvlq6Omf&is_~&vbU#czQr{#+xj9#B?|FNQlS1XCc_~ARY z)4C$n%LG=NMtVQ^=4zn4lWBLPq z)!{ZFm&TUZ&}>AdY?P2?Wld3k_7g+gGc`7dbV3{ri8ytk*A8BVkeG86ti+St`*Q8BINBa_t&A~q7+iKpO<8)HxU_TgV|~@WuN!r}O@qo8ucl&Y z(!KH_>e!Ras74>}tDdr{R^nJJ#1{pF(nZ!}V{J`#j6>LUrbM0g4_|Yk1~no%6)S4* zymRNa#MFQY;;t9AMptxTHw(28v74ohUs)w~;S+YqxcFi*x3G-y!ZuXpeO2H**oc2Dy8{g@`zk+f{r?5YY}} zbGzQV8(9i`ml*uoG4ddnO{H|0 zU$-&Kw$dlW%$FF<8Ln9#(w#HeNqNRj0SDFTZy9aNB2)bgc;*Xt$}6g?kwLMgafv<# z_B;2rPZ9=>6}P9v15}=Fmw*1E$O7akQ{2bTf7=Wy9zlfO;?BNffr>kAftHH}YKV5X z1$rG)_*tNRBD>r5+3gFw&Zapo^3E3BjdOqGBn4zabzDq^rc4d>x$lUcN)(^GrH1-$ z_-3U%hwHn%5}h%1Nn`WMisovY8kX0+NMw@v-XXwjk_-GFC1@7(vxFkfzi7%$LOFE& zb8$W7v22dEGq-F5-}Sd_@ro0};@PbCRi3-?o}j*yK?k<0L~)}-)b|^Fvr@J_^i^L8 zm5WiI*(!-spi3k(6%y|^ntGE^4_*7bj|v^j=_=7fDd}7`3&g?i`CHC-HFDp?cwT;^ z&lu7K?L7(fqCIXbiT3WtH!EeEDI~`9&`W7Kyn_$RZ2Z`>?Q}*%j~I7&-xvo{3Kjp2 zG(3jJJS&5aj}fwoiLt@`C9-*R48G6kD0+>J)(oB-2DqJIqe<5T{{(8W0C8)Jw+FDc z$bpCN;N?1#p!nTCFpB2}peTMazF8^TfgxFItbH7ViP=tKlnq3&n$BpUE&G>IbKFAfdA`kjlUQ#SCh-J2y!f() zH=QdU!Diq;Vm03D#lEWWLo?LBZ=+*HlvECDxsHO=4{T!xjTWr!xRT1E6?s@o3`uO zWkOrKoE?hv96gkm@Gz^@z<9>t>2PNb$*yYakW;t|;{U|dA<~KwI_p0%YGx=?Xb59= zoONYPXP=_ii4R*}+gTrW=A!!As=DgdB~1|>2d_VjGoP4w+V>%QW<))U=j!`EHKRQ4 zA$y;QNnhi-oSPiXVKdqOgX?p0-mBneMp^V-#dXbCSsBfIW8tz|26#lL24t-14%-tZ zw(%T)BeVqz1<%CsUq}0=JfiAv>Z2l`oBk=j5t|_A{S&(7i`gUaKg5k$`u2`X&m$gb zCMy_r;kJFVui5|xWP4!g@u1BYH3*qS5sIbHPAZX~hmR(aifQ_;Y&N}_dQszEZ~hF~ zGAk|hr-{I;lCsXFe&LF~s3d}@ByPo{Iwfd?V3oR8Gju)PK<`Fhws=8c=M zNZwH;2vkE>NTyfGB#Vy7HV`SnG3X$h3*6a6HaJl*GMw|JcLAeRbhE2#TUslSuwD|u zLcyWKga?GR*?(c0H9kV_XlDg7S=)FRRkCAs?GiiXy&QjGH;?~sEuGkmgSE!`NJUi@ zHW{24yGUzAjk@N-pxWUpu=5gB$~5r#73%6>bUQW znG*HrlcK~C?>DrZy-sHnH`__$?u_zqOOJmtg_u)KGR!fPec ziBDLm*SiCMLMQMMK|0}Qyg3HX2BE{aXD#n!(c1|r3)FxOvVX#7r+PX}^5B(rFQvoM zrGCP%s*B|#Wb=6fYSXm1;L|uDZU3*=7S^KWKu61?o z?yhUw_51(NeRc9?=1oF$zwh_)<7Mu<<(zZRt*6}kN~i=>yaj$%pDwM+XlYdjoU7s` zl%iesBhA~0J~_k^;@@Ox2)A7e;W)(2vv?I|Z;P>JTd*EO`( z)PV(>=5jdTmy2-HeY=7v!)O80Vg}s5;rh-FIjImFp(-rnIX=hu7Nq)EB2!Is-CpFx zHCOC;FMv|9`_N{ujT{~)oq&}2`SA3NI?*M=L5l2L5pff_J>4rjOMqjX8r3`2sY-2~ z@+<}}q`kE)oZ8Plfdn#7IZ{$kabl;nwt31a+*3iqF;A8BcTLo~gE{@4Dml|pFST#G zG*{A5j|3d)sPy_1r$4Vf!3D~%GrGW8RthS>p7%fi&NkO=Ew zTH{2&$pAO52E?SW&bC)|VPGa3RaXv*RZY=u_A8*2Y1J{AG354&VVl`js#HIc6cg36 zRt?gas0hp7)@7Qy@Qon0;Fg=3oZv^WBK(@Dnphv#Ul)`!hdF!)SdD&y`>oL^Up8yW z$Wpp>Wb!6*K0hx0z@t5PuazAL4TSX_bFNWb+^yq`SgOv*D^MST_&s7}%}M>~!5v59 z6GOaXjooOJd}%r&pXkMe+f{c7Zva^!yn2j`+Dp?-)4P+=k=VTOssuZS8LKwiQf4SR z2l$cD4fU7l7wX!GdJH_!=+n@pS9LLPRbXpG?unsmc+~8MG(p&xXvnw|S69b`BkrX7 zNRu^vY;=ZE)sZqoq0jXAMj8)8a?x0b#Y@qqRkE3jPtHv>t_hDVtX)YrS;cPecpW*W z7Jf(yQ_isj=JWll9Qvh=%`pJkA(&5d?qv>*Adnkp-C@KcWOo=^h87Pp8R4-ENrw?P zD(@XQ`ZOBq<~p)1#sg98D#uqZ0HfV*7G+?I82y$O0;WDPHav z{TFDqDctD?e2K%kq$fw7``EEuHmz=P-nE=(;5}FPmx@f@drVwXGucUaFYbx#%q>7L z-kTej;M{kJ&cc7v)UmnAe^M#@7fM+r(&sl3`b{l*4s~5KWX-SLV!P!}pMmF`^2)X_ zdG0aRITNJ!x*>h$GZ@c#E+II>?P)%BI`s5*Qu7Wdw&@dY3yrKyF5Gq*7R|E)-I!G@ z6Jxp=h1SMvwlq2Hu{KH##P#2MH&P@|{P(j>50gNCF6f=9VUl@ej*&?tLPQW~}=fj7(+#Q0+yHLirwBn_W<> zcwCtoD_u}@38L(lKa7{#4AlJpQM0P|YrT;NxfdDtKW1HBVp&(!Ior57Px~pqC#>zK z@@3&|XGqb9yA(dGR1U(CXgZI6h(@8tt{UwkMX*qqhh5vRXnd?e|YL)s7hYQ(?wviLTdr z>=KW~c9iJme~LE6{U+9dlTu#fD>Hm-N4G{_=6cTQ&Rj1OzWqmu6O_M({->x@DRePr zP5STT?vd8QWn@L>69_-lS8zG8GD*fp+I1J*p2+P=4rHyvbWYIV{JI3z4Yjx zcj+IDmj1zj7*(A=qN%X`;wJSLPrBCubK(Yj7BteVW8KbX&$26=iX&k;dtPd9mV7v1nf@Xcw3B(5 z`|@+(60nO&3P0Hr8Msw}ivrszHS>Qp5 zw_sTsLNif{ory{5&#uT#=%I4j&cn7mA2OB%!bo5MpU7Bu83rWuf z1ye$BPJ2GTD+Wc@GoCKz?~L-ao~XAJnOP&kbZe#-nn6Zg+{L4fXSkzrEwp%?I$8hO%P)u;g)%h&X&YH=%G+6kxao| z6=qeBER}0-ORGy^N+NiEC#5!CQ?*j=ZEjDtC0cn99rrlP@$)%w{w#yW$BrnSi*uE8 zF(lN;e}>eRBZYg#&@}MdI}1jM`*7p}9Ab{V(d=IAbq!Ni(dtZ#BaoVlBO&D#o9#sZ zeYJzUPbPmEmI|6 z-Y_dcAD1_+O7JBzVi&Y+DK2_Q)Wqr%jq&P^IynPx?rZ2AWZsw{(cGLZD+#N3%*ri2 zR}HBmSYroV%-XDmjK;`fZ-?p31-GmQQN=EZWi{~AVF_Gifb19>__9%Paq%c!jKib3 z9;L)(7kOnb94l*SkIhA)Cd>}1@2&;P*f2V*eut-UQ@lm)UxI2~cN)%GO~FOeYRLDX zMXrs>PRux5lP>fu*H>ly0HFD(ZQ<-jaH;kv4+J&j)4;WyUV=}e8p5JVz&ngENo*na z3~I;2@%=!~h&xqoqg2~ML`9hw+lZQmc+JvST~!Sntz>$v;Y49KML&H306jLHjF_s% zNH+4fqI>~9T?bd~O?E3#ZvgM^vHb{5?Y;(GxCoIHJ6#G5)3wyJs9Sb)ZeTOGi(4jW zdWUFXAC5I}XQ5@eaZDFspV1=hGoUZ$vqL1uxj@9cJ}+g5sflA>Oyjt3(c`#%U6A!44!4tg?z#hG$-GdSa85=ppRV5+POj=(U}amHeuW8^Rs$iB^Xku`8Mw29du zm)U+mdOTX5G|*_2(2uNw)(N@+9X<|qp;tIx>`IAsf}g0+^aC{=tBbR1KR%L)R3Mj`;Iqj$QP-h1ZaQhHweNc-&}tzXPayscKW(K_A+McPLKtjT*1pndq}`Xg>5j(alc#UX@LPd)KhF}?JLO*DtA7m`*WO!}!L zji1&o@utiqB~+pOBqx`}5FvU(#}4tvUR>{BX{N-I*7_i7pUkc1Ra1Fp9@7ZtlLK8l zOOyB%vZm{dU@>N*x$rwxStb?0Zp!|~ncEH4@rH2}_B-!wGBTr@e#92=pB%kp(xrJS z$`X6MvWZdyKh|!WmaF$nmyU64ZAViRu3m!%5MRrIMv^oEzR&8^yi`%iFX>XZbpxy&1d*Hq;?fSj^&sezML{#TpP}+3v@EaUck6e8Lq^G?e z+6v_U)jo<3ienA&Mto{OeAF}2jW6(|B^*@Ay@l8qb+q#9Jx#5R;|r$&K- zGxu>5y5qapzLLh}9~5FPMw&@%D~S!V7m(Eh^K+0}8Uki>=I$WCwW~xfdknC#drl~# zD$ndT@y%p&fy=>d!dVuA)Yos_=uwSipNJdq)=n`O9LK9ewEMQF*-y9TXdSNCmn_Tv zy8A|<49*3%23I{Z)okZ9I$D=6%umHOOyH@f8GZ_$<*b=i)V=6i5@QuGeOQZ)7@0d!a#y$VYmWmCgmrQa{c`@@6N)u_PM_+ zm%1r1O?_BqH|}TVUWV`tMW)! z-wAN3`IwmcO;v&V)FQ#bzBRO)u5Ye6Zl=!aCx_-Riue3nw!NvJRMRe)RaC(6vuIfU zA+et>v=i;SKFWsv7^V`EEm{ZOwbuiw$eCVfpM z&h8z1UcPz(;Dyh7QlEP#V%*>5ajMl7cq+f4MQI9h;Vv{z@tOiPYleyVdf<%kPm0n+ z@1=d@Ek_zRl_lyTz_O)nN4`!0C?DSGBE6X+um$P^3lv3MN3!L|vD__BVMeRn92VIv zpZQ~YX3O&kNxF6~M!PTHze~GIX6p^e5o|r;|=1XT+ef@&aDiV z_BYYeo=x*RyF9G+YpcbbyUDof_iL?KouIX%DDJD^9cSi;0`xg3WsS;2{f$i9ADN~2q&pY@gw;@Ot|-CfFO)8M_KXAMh$rj&MZM$rpGH= z%$PSjtPaN`wHM<@;j$)-wBHzB)~g{;=70bLxZ4IK;~kLmpROdin#QLKZ(G;c(cX}) zi}kG)m!C0`*|#^r&}SLRFrwLQVGgDi172_H5*T97K`*uO?`WBaB1}rL+XQk%^(vN4 z9g~jCuC2;km^!H*_$FIAYDXu9l@k^lxx=MYZlBSHB{>QqUr~HyZWqH6tumtLlMg_6 z|BzqK)gPJleOas8Jzvqxm85ehE+Hh7>3zcE z(oco8%$st4(otWF4%`&r-o7GFZOfY-iaT6aN2RLjEJdZ)FVAu4OU+M3|G@X%y1?Sn z?&<+6ZN94P}#<+TRNI+ zGmr9I7?9&xn_z59F%Pt~w&N24i>ex7k8f&~D-b5plV|AgU3VGox?&}Zu6AdPT7J<{ z-^Y?I^DZ2iTmQ2Ujn&0)Bye%>r?KOVi_L*;8DA9!JBmxZ>?oVJA8%8{H)&?jD8L!B zod4nFAosqAq35lIW=GLhA7~fYQFh#IJEac%B@P#e`^)aP- z-CG&mH4nTdJ_o|z^i)dLL{`OI6W>7&s~Nvs@3-;EsHD!;vKb3!Y3yvO<&E3^L#{>p zAKbP55#`p7zepW3X3JdKKRXk;)g`j=CvY#xN1gHyaaUb>3C#g!FB!~QLpdmFz*tMO zILxKh4E|H=gt3Hq@|R&J<5mc(X5jQu`dZy_#3eAKFwb6JLlVejM|Y2n^u3~c7WB}q z1!~x#F6Jni7q&-~dtO-0d10@0n+CI*6uT+zv591J;C{wKqO>1pEisWiMq2gsRY@|P zEimOgtxa|Ll=%!Pw~U$QZ^FK^UZYHdEd90A-M;@(YqJmG?Tn~9{FobYXI~d%23RiE z>gX*}ZG3D~=PaAolqhxEU-6~*cym=XeKqRJ4s373m_ihsgWGGW8mrnw9)Y(qHd-C= z(&e_dzR2&`p5jY62OMtFO6!ZzdJI&v5x(g^-?$OlrT z%!SeVIk}cuQ&k4NojBz=YP+@?HaVQ~sM|`ftaIJ*XHlNfTO(_=GJR0QJ@_%OQ&?7J zeY45sG3VA!Ql*n}Mwd+ur4#SSoLg&-Pe+$$e+yj`J#k`kWtTxW$K>p&cqM1$X>6z zJCsdjMj;|2nOWS7GWAY}&KKZsvsHshC!KF1=f3SrXKPzaeOpyiuDl$ZzFa^~jw~-w zcUcrJB`3f2r!X0BYHb01$tst(RYndL`xXgE*GZtOGGTHqM!F7r$IUqzueqq!Q#vF@ zRjL}q*;4xG(=)X<2AVn1Z2?0`;S!T->d+(Sz7MLkOS8v}G4e+>W5oVepneCsQk>Xs zL{t|uhIVL9BU_Iwbw+Y=S2kr3%F-Jn9CzkfFf~*yb(1L~neEPeFw#tS=3241$SkjQ z)#+_WBRq~za7`-MLMtGPUBz8jL-j z=5V&!_d@J_#j1yxO=f(C-n^|#+5bOpD)|vCA!_UKeZ>w^>|)GMrOviZ9i=>m7OUK! z|2>9|Cz`Rke`FrWmtCK2=$OKJ)?V}@V(4I;+SaA0|Nl+C3rt_{YSUM-i!uMZO@2>n z?oF=MhZ)u^w=&LWH8l_5bjPSr_f>(aUFKc}M0t1^qTI?uF)C=~VUX3zO>Ym^DLVZY zwz&!Pst)szW~vTnP<5!Kk<@i6uuuYB4w!URkfX}R0Qjw@A zZ)nJ6$$KghQhT+jEi8PA{KyNSQDEW)6FByQar3x(HC1Nt$z1W1N~^u}G77>b(EB=P3`iE@(0$ zadcyx`X;R~>(9wC@~}S=%vWaHc0pB9o^jf8YDDH;Z6Om*enDh=t;QkuybXi6i*5DQ zjBC>l7V|XMt%(1roJpMvZ^U`!&{(U-V7u6TC3YT`Gqq#j*KUuU0-X*8QK z=W^RKwk=MKG24DZp;50=WRiNQPaidQmD%ze$O!&18>4QcGzQE{?$)>q*fO=oX?AP4 zc$IR9C6zzA%GoQ7qa=g-_ff-+Fe@wvR>UA!WIKrOcLn{uZNV5WtJ=7fs!eo0?3F=| zbr$PwU^2Sc!u#^YVDyJu^>GQ{)TaZN&z{3RMUVlHGrD-7pwE$J{rD2uVr$}QN;FTh z=L*qvM<3!)8;yjrC34vgPyE1U${{(&f*^`=8KnGU9=+1K|I|h>wf+J69hza}s6Og? z8wrmxeTqNsqg~m&|1bf?Spj`-1C3VQ(@<+wM3AvE`sqHZ`WGf^`H{ab?Y5rtGLZjE&_s5ujcl~%aw=f^8IY8-5Cif@&63*+v{f;k)-&t2G6)c_*Hw@belFW} zd*To;uBU5gYw5($s?{znYDNj;>!!i%Y$Z0vo9mMev3OfsOPiXpKa=W`IEiAVw{#?N zv?kV8)m$G}Gs>A%SFyE|o@j)6j<u)%cB?C#7Xwy_CNk>L~& zE=yFm92-{m;whX#6_}8yfCN(`eZ{zTz2}8$hn$E+)Wwd-IQGn-*mNh zO#67jksN#p8vn2zDJ`wW*=Fj=d3<>8Xcpp4u&mUu$O<{XnYgcS(nc-gd z?*FSt#JVLXxJSe#=7{jP^Q*GU{M*%&Ya@?{M~)Ax=kOF(`i_X;{W6PVNf7IYT_8WHo*Ar8l54+MK?=%x^@JcosAMBrR3mcbwQ0EjBP z-jBDKu05n|tZoVW+sIg#lO2$#N_iy&*OlW=1k-q4gJVd~;q%~s_hX-MP-W+&;fsfq zjm;PD>S*zW=M7SLqQ(vXdiR6#)%f9tFSm9^)=Wv@uxH;abcWvVS zMSaRX%E+I%_>{2fKNL5Q=I_niMt>X-Q~8tmsb{1Ac3^(~6pHgk|4VVJSN>Ff@Y(2J z$?(3*8J_&Hf$&@phl)pgkip$q4%Zv|{~Bupp2Fi$Qt6WOvzL^6Gn8=xO0Ka=M@|zE zr{@9rqAVcv$>T>*^f{||;W5~_sVNV#9*a@D+}WOVB_1Egt8K%@G^G_a>RoK$jDaVI z)i!tv&j%5a?-6B!Iy5*axc_}lScd>ky$!1`2vk#c!!f=5nk=;%Z_4rU(k1iCmuteV z2B6;)7#SRS74Rcf!l!%Ks7+ZQ7LTlr+d${lEK$ZcgoO^G;j!)lR866`SN^hWwH*&> z4i@!Q@Qzg^#wY`N`5|mKz{!Hru=;UwfuauU7T6InybN0oI&$N9$AF!z_Y z1|bGs{gv3zYssDUG8r_yEnc0#(O7dKC{KQM8v56}&>NR`Jb(@;js!9|ZRM^y-smD5 zNO@~JmtG5NN|r?CxkucLPfR<^OqLhh%98=Pa?N>={4j{O;)A<1;pQ3$d$%BvN!RXm2NsKtXAL=W}B6M zgTs6bk@rsDPOKkt?Awv!(F$t&yV%br^jg(hX92nNvxc+7svVE6x<+2Z zVZC?nRlT(l*rm6<56oW>yQ}K!*xRP|TGgA%tg6?X6IRW5+*MDq>ZQF`bsYCHtNs=+ ze@yJIs!xULcTidHVdphdoKiV^8`W6o8t~{TRTKnV?(~NSu+dUcHc`6uSDTT0Z57^h zoof{q+pfI|Pr1Og3NMlua}_Su@*G%R;;*s%xwT>UBi_qF@fb5O))^ti(GJ^Jc#ZtHF3o%z;

f1`!)jAJx~X$qi@S+k7GtUQalm|%g*}JG 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

2ekX#8kUbTncR{%;Awr|2-1 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�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¥fR4fj< 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�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%0A*0FWCEks&L8vIIK9qxGY;uP@r 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%0DTBguUP` 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%0D0Amg^sr zL&y%;+=+s)jz(G8_^-?f07B!?I&{&fgS^ntg&WJZEfy^S`u?EuGe@~ zFTUgbe|Of4+H1t0xV>hKAK|=6BHj0G~llG>;(dLf-ERzHWfh+pNN{2e-ETvAg0kJ{_N zF7AV?q0uyn+G}=-_Hs0Wv!Q{<_gR3X9&Xda)3oWyXw!IcGW$stPBGl3E{FW{MT51w z+w|-&aGU--uuWGS!)^K-!yYo6+Vp>yahu+5c0h2bP5U98u1%MzHl_SyUeu;fUdC6A2WdIs9G`?U0J>T;+}r^fQiORY{UVKAz4ak5(I7hku zp?Llh`a8X=;9a{rp5H%-DD<7Ep+mM?^-#<&%OK`wH*p@@ z3hU*Xk@MIJ74sb}2>80=>*kA?A2R~+Q|3m(P}5w;zOR5&bY_y{AYTL0Es%O3IcMvf z%YdlasUvkWkY2(0K9Ihz>r#&a>8jC@7lFj1I`SqEH8*xN|ARPlbk4q?L-Yz{Jdmyi zol^>=yHQ6XK(gk#h#O8VK*|Mj36Ls*TnD6AAj^UD3FL=B1_bgrkU@d`0Z1sOm;Mou zEP)&}67~pW0+1?!oCYK=kn>2YS+8k~q>_kR(?vjfTlCUzC8<^&Sp{TJAkPBniR+wK zf%GYa^M)H^6!SSphC)?NtMPx#yg@5jdGUHsMU>??*?B#reU}=Pe?z?F1@JKc1 zmbcJjuO8~7IBUJXp1`>UukBba$}LUza&Gx^c_@SllkcM3l9NGhDO9jBak+51`@i|4M55}bmT5V=IO{fAU*SSbl(9gpg0y!MW7-3Hiajw$!lmh7$$TxrtD1`Hd8$T5D z^y4v?I`!rMaNbbfaDJhc@4ElzyrH~0ghmc|ao*S%aihuottfL3pYq1te^Pm)Dip#C zY`%-;B>4rLH?~uN8W-h_iobE*_z>n#H8_+v$}`9to37_M$+BUF5&)Dpit!;kH*dU* z#EORTU6ePToyd9Pb>Ch0uQVs=%OG!zY3IDrhq@mubWLca4d1c(t5GjXs6#_Zq~I$2 zRUr922B*Q`&@O_ID!9PlV0r4}R2rP62ImYvCvO1uin8)ODXL^EqBnkZGQv$4tLGpt zhvGQ~DH$Fd&*O0iCV^vZ($E6xDJjZ|v?tmUt+6`AQKh>GY5qCs$G_girRM;0OCROk zO#X>`%J3>z+Wkc*@{7C{QSZJc*c-(D4mdeC=sDp&@}WQ;1~RA+&Izt&szv@`*wGb5 zvxV7JY~d`l#stBkzdT8rzaMNNm*sM(tmoh^=`qst0`v^@>b3YiklqJ$qj~=EDLjNfn)*c#RmgDscazG59`PTl6pi(CIjgfNHLK3qdKRYkRR*F*MU_1 zgb}WL-wz3K>W(EA7A@Mv(J~E~7ppSxIrkO;c1G$U-L%9K7q?eeHMbWQ&X%iz>ctjX z7;enhdmI(>t`yZ`1@y`mkHogCvdo#VTULFjKZCMdPU^BkwOlVtSv85)`M6(&%^4>SQ`F1lP+9W3 zsqCdTv|#A*^m<$l>A{fX#2;&L|Gx-⁣ty*3CLcMS?3unlA*0+FM?CLu&AsNei7@ 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

BPu^?ZkMNDJj=x$4ExAGQ4HN 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 sampleDict = AddSamples(config, sf2); AddInstruments(config, sf2, sampleDict); sf2.Save(path); } - private static void AddInfo(Config config, SF2 sf2) + private static void AddInfo(Config config, InfoListChunk chunk) { - sf2.InfoChunk.Bank = config.Name; - //sf2.InfoChunk.Copyright = config.Creator; - sf2.InfoChunk.Tools = Util.Utils.ProgramName + " by Kermalis"; + chunk.Bank = config.Name; + //chunk.Copyright = config.Creator; + chunk.Tools = Util.Utils.ProgramName + " by Kermalis"; } private static Dictionary AddSamples(Config config, SF2 sf2) @@ -94,4 +94,4 @@ private static void AddInstruments(Config config, SF2 sf2, Dictionary left) { diff --git a/VG Music Studio/Core/Mixer.cs b/VG Music Studio/Core/Mixer.cs index e0a242f6..0ca5ae82 100644 --- a/VG Music Studio/Core/Mixer.cs +++ b/VG Music Studio/Core/Mixer.cs @@ -1,8 +1,10 @@ using Kermalis.VGMusicStudio.UI; +using Kermalis.VGMusicStudio.Properties; using NAudio.CoreAudioApi; using NAudio.CoreAudioApi.Interfaces; using NAudio.Wave; using System; +using static Kermalis.VGMusicStudio.UI.SongInfoControl; namespace Kermalis.VGMusicStudio.Core { @@ -11,6 +13,7 @@ internal abstract class Mixer : IAudioSessionEventsHandler, IDisposable public readonly bool[] Mutes = new bool[SongInfoControl.SongInfo.MaxTracks]; private IWavePlayer _out; private AudioSessionControl _appVolume; + private DeviceState _device = DeviceState.Unplugged; protected void Init(IWaveProvider waveProvider) { @@ -69,7 +72,31 @@ public void OnStateChanged(AudioSessionState state) } public void OnSessionDisconnected(AudioSessionDisconnectReason disconnectReason) { - throw new NotImplementedException(); + if (disconnectReason == AudioSessionDisconnectReason.DisconnectReasonDeviceRemoval) + { + Exception ex = new Exception(); + FlexibleMessageBox.Show(ex, string.Format(Strings.AudioDeviceRemoved, _device)); + } + if (disconnectReason == AudioSessionDisconnectReason.DisconnectReasonExclusiveModeOverride) + { + + } + if (disconnectReason == AudioSessionDisconnectReason.DisconnectReasonFormatChanged) + { + + } + if (disconnectReason == AudioSessionDisconnectReason.DisconnectReasonServerShutdown) + { + + } + if (disconnectReason == AudioSessionDisconnectReason.DisconnectReasonSessionDisconnected) + { + + } + if (disconnectReason == AudioSessionDisconnectReason.DisconnectReasonSessionLogoff) + { + + } } public void SetVolume(float volume) { diff --git a/VG Music Studio/Core/VGMSDebug.cs b/VG Music Studio/Core/VGMSDebug.cs index 91bc0919..c30a6ad3 100644 --- a/VG Music Studio/Core/VGMSDebug.cs +++ b/VG Music Studio/Core/VGMSDebug.cs @@ -88,7 +88,7 @@ public static void GBAGameCodeScan(string path) { try { - using (var reader = new EndianBinaryReader(File.OpenRead(file))) + var reader = new EndianBinaryReader(File.OpenRead(file)); { string gameCode = reader.ReadString(3, false, 0xAC); char regionCode = reader.ReadChar(0xAF); diff --git a/VG Music Studio/Properties/Strings.Designer.cs b/VG Music Studio/Properties/Strings.Designer.cs index 7153b371..6882a807 100644 --- a/VG Music Studio/Properties/Strings.Designer.cs +++ b/VG Music Studio/Properties/Strings.Designer.cs @@ -59,7 +59,18 @@ internal Strings() { resourceCulture = value; } } - + + ///

+ /// Informs whenever an audio output device was removed. + /// + internal static string AudioDeviceRemoved + { + get + { + return ResourceManager.GetString("AudioDeviceRemoved", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} key. /// diff --git a/VG Music Studio/Properties/Strings.resx b/VG Music Studio/Properties/Strings.resx index 8e4ae4a1..7ffdfc8b 100644 --- a/VG Music Studio/Properties/Strings.resx +++ b/VG Music Studio/Properties/Strings.resx @@ -373,4 +373,8 @@ VoiceTable saved to {0}. {0} is the file name. + + An audio output device was disconnected. {0} was disconnected. + {0} is the audio output device. + \ No newline at end of file diff --git a/VG Music Studio/UI/MainForm.cs b/VG Music Studio/UI/MainForm.cs index 761394fa..05b1a4cc 100644 --- a/VG Music Studio/UI/MainForm.cs +++ b/VG Music Studio/UI/MainForm.cs @@ -1,8 +1,8 @@ using Kermalis.VGMusicStudio.Core; using Kermalis.VGMusicStudio.Properties; using Kermalis.VGMusicStudio.Util; -using Microsoft.WindowsAPICodePack.Dialogs; -using Microsoft.WindowsAPICodePack.Taskbar; +//using Microsoft.WindowsAPICodePack.Dialogs; +//using Microsoft.WindowsAPICodePack.Taskbar; using System; using System.Collections.Generic; using System.ComponentModel; @@ -45,7 +45,7 @@ internal class MainForm : ThemedForm private readonly ColorSlider _volumeBar, _positionBar; private readonly SongInfoControl _songInfo; private readonly ImageComboBox _songsComboBox; - private readonly ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; + //private readonly ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; #endregion @@ -149,17 +149,17 @@ private MainForm() Text = Utils.ProgramName; // Taskbar Buttons - if (TaskbarManager.IsPlatformSupported) + /*if (TaskbarManager.IsPlatformSupported) { - _prevTButton = new ThumbnailToolBarButton(Resources.IconPrevious, Strings.PlayerPreviousSong); + _prevTButton = new ToolStripButton(Resources.IconPrevious, Strings.PlayerPreviousSong); _prevTButton.Click += PlayPreviousSong; - _toggleTButton = new ThumbnailToolBarButton(Resources.IconPlay, Strings.PlayerPlay); + _toggleTButton = new ToolStripButton(Resources.IconPlay, Strings.PlayerPlay); _toggleTButton.Click += TogglePlayback; - _nextTButton = new ThumbnailToolBarButton(Resources.IconNext, Strings.PlayerNextSong); + _nextTButton = new ToolStripButton(Resources.IconNext, Strings.PlayerNextSong); _nextTButton.Click += PlayNextSong; _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = false; TaskbarManager.Instance.ThumbnailToolBars.AddButtons(Handle, _prevTButton, _toggleTButton, _nextTButton); - } + }*/ OnResize(null, null); } @@ -220,11 +220,19 @@ private void SongNumerical_ValueChanged(object sender, EventArgs e) Config config = Engine.Instance.Config; List songs = config.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 Config.Song song = songs.SingleOrDefault(s => s.Index == index); + + // When the song isn't a null value and is played if (song != null) { - Text = $"{Utils.ProgramName} - {song.Name}"; + Text = $"{Utils.ProgramName} - {song.Name}"; // Reads the song name from the .yaml file _songsComboBox.SelectedIndex = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox } + + // When the song is a null value and is played + if (song == null) + { + return; // Resets the music player and prevents the song from playing + } _positionBar.Maximum = Engine.Instance.Player.MaxTicks; _positionBar.LargeChange = _positionBar.Maximum / 10; _positionBar.SmallChange = _positionBar.LargeChange / 4; @@ -317,18 +325,18 @@ private void EndCurrentPlaylist(object sender, EventArgs e) private void OpenDSE(object sender, EventArgs e) { - var d = new CommonOpenFileDialog + var d = new FolderBrowserDialog { - Title = Strings.MenuOpenDSE, - IsFolderPicker = true + UseDescriptionForTitle = true, + Description = Strings.MenuOpenDSE }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) + if (d.ShowDialog() == DialogResult.OK) { DisposeEngine(); bool success; try { - new Engine(Engine.EngineType.NDS_DSE, d.FileName); + new Engine(Engine.EngineType.NDS_DSE, d.SelectedPath); success = true; } catch (Exception ex) @@ -349,12 +357,14 @@ private void OpenDSE(object sender, EventArgs e) } private void OpenAlphaDream(object sender, EventArgs e) { - var d = new CommonOpenFileDialog + var d = new OpenFileDialog { Title = Strings.MenuOpenAlphaDream, - Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } + Filter = "Game Boy Advance ROM (*.gba, *.srl)|*.gba;*.srl|All files (*.*)|*.*", + FilterIndex = 2, + RestoreDirectory = true }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) + if (d.ShowDialog() == DialogResult.OK) { DisposeEngine(); bool success; @@ -381,12 +391,14 @@ private void OpenAlphaDream(object sender, EventArgs e) } private void OpenMP2K(object sender, EventArgs e) { - var d = new CommonOpenFileDialog + var d = new OpenFileDialog { Title = Strings.MenuOpenMP2K, - Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } + Filter = "Game Boy Advance ROM (*.gba, *.srl)|*.gba;*.srl|All files (*.*)|*.*", + FilterIndex = 1, + RestoreDirectory = true }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) + if (d.ShowDialog() == DialogResult.OK) { DisposeEngine(); bool success; @@ -413,12 +425,14 @@ private void OpenMP2K(object sender, EventArgs e) } private void OpenSDAT(object sender, EventArgs e) { - var d = new CommonOpenFileDialog + var d = new OpenFileDialog { Title = Strings.MenuOpenSDAT, - Filters = { new CommonFileDialogFilter(Strings.FilterOpenSDAT, ".sdat") } + Filter = "Nitro Soundmaker Sound Data Archive (*.sdat)|*.sdat|All files (*.*)|*.*", + FilterIndex = 1, + RestoreDirectory = true }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) + if (d.ShowDialog() == DialogResult.OK) { DisposeEngine(); bool success; @@ -446,15 +460,15 @@ private void OpenSDAT(object sender, EventArgs e) private void ExportDLS(object sender, EventArgs e) { - var d = new CommonSaveFileDialog + var d = new SaveFileDialog { - DefaultFileName = Engine.Instance.Config.GetGameName(), - DefaultExtension = ".dls", - EnsureValidNames = true, - Title = Strings.MenuSaveDLS, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveDLS, ".dls") } + FileName = Engine.Instance.Config.GetGameName(), + Filter = Strings.FilterSaveDLS, + FilterIndex = 1, + ValidateNames = true, + Title = Strings.MenuSaveDLS }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) + if (d.ShowDialog() == DialogResult.OK) { try { @@ -469,15 +483,15 @@ private void ExportDLS(object sender, EventArgs e) } private void ExportMIDI(object sender, EventArgs e) { - var d = new CommonSaveFileDialog + var d = new SaveFileDialog { - DefaultFileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), - DefaultExtension = ".mid", - EnsureValidNames = true, + FileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), + DefaultExt = ".mid", + ValidateNames = true, Title = Strings.MenuSaveMIDI, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveMIDI, ".mid;.midi") } + Filter = Strings.FilterSaveMIDI }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) + if (d.ShowDialog() == DialogResult.OK) { var p = (Core.GBA.MP2K.Player)Engine.Instance.Player; var args = new Core.GBA.MP2K.Player.MIDISaveArgs @@ -502,15 +516,15 @@ private void ExportMIDI(object sender, EventArgs e) } private void ExportSF2(object sender, EventArgs e) { - var d = new CommonSaveFileDialog + var d = new SaveFileDialog { - DefaultFileName = Engine.Instance.Config.GetGameName(), - DefaultExtension = ".sf2", - EnsureValidNames = true, + FileName = Engine.Instance.Config.GetGameName(), + DefaultExt = ".sf2", + ValidateNames = true, Title = Strings.MenuSaveSF2, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveSF2, ".sf2") } + Filter = Strings.FilterSaveSF2 }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) + if (d.ShowDialog() == DialogResult.OK) { try { @@ -525,15 +539,15 @@ private void ExportSF2(object sender, EventArgs e) } private void ExportWAV(object sender, EventArgs e) { - var d = new CommonSaveFileDialog + var d = new SaveFileDialog { - DefaultFileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), - DefaultExtension = ".wav", - EnsureValidNames = true, + FileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), + DefaultExt = ".wav", + ValidateNames = true, Title = Strings.MenuSaveWAV, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveWAV, ".wav") } + Filter = Strings.FilterSaveWAV }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) + if (d.ShowDialog() == DialogResult.OK) { Stop(); bool oldFade = Engine.Instance.Player.ShouldFadeOut; @@ -563,8 +577,8 @@ public void LetUIKnowPlayerIsPlaying() _pauseButton.Text = Strings.PlayerPause; _timer.Interval = (int)(1_000d / GlobalConfig.Instance.RefreshRate); _timer.Start(); - UpdateTaskbarState(); - UpdateTaskbarButtons(); + //UpdateTaskbarState(); + //UpdateTaskbarButtons(); } } private void Play() @@ -585,8 +599,8 @@ private void Pause() _pauseButton.Text = Strings.PlayerPause; _timer.Start(); } - UpdateTaskbarState(); - UpdateTaskbarButtons(); + //UpdateTaskbarState(); + //UpdateTaskbarButtons(); } private void Stop() { @@ -597,8 +611,8 @@ private void Stop() _songInfo.DeleteData(); _piano.UpdateKeys(_songInfo.Info, PianoTracks); UpdatePositionIndicators(0L); - UpdateTaskbarState(); - UpdateTaskbarButtons(); + //UpdateTaskbarState(); + //UpdateTaskbarButtons(); } private void TogglePlayback(object sender, EventArgs e) { @@ -655,7 +669,7 @@ private void FinishLoading(long numSongs) _autoplay = false; SetAndLoadSong(Engine.Instance.Config.Playlists[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[0].Songs[0].Index); _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = true; - UpdateTaskbarButtons(); + //UpdateTaskbarButtons(); } private void DisposeEngine() { @@ -665,13 +679,13 @@ private void DisposeEngine() Engine.Instance.Dispose(); } _trackViewer?.UpdateTracks(); - _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = _positionBar.Enabled = false; + //_prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = _positionBar.Enabled = false; Text = Utils.ProgramName; _songInfo.SetNumTracks(0); _songInfo.ResetMutes(); ResetPlaylistStuff(false); UpdatePositionIndicators(0L); - UpdateTaskbarState(); + //UpdateTaskbarState(); _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; _songNumerical.ValueChanged -= SongNumerical_ValueChanged; _songNumerical.Visible = false; @@ -713,17 +727,22 @@ private void SongEnded() { _stopUI = true; } + private void UpdatePositionIndicators(long ticks) { if (_positionBarFree) { _positionBar.Value = ticks; } + /* if (GlobalConfig.Instance.TaskbarProgress && TaskbarManager.IsPlatformSupported) { TaskbarManager.Instance.SetProgressValue((int)ticks, (int)_positionBar.Maximum); } + */ } + + /* private void UpdateTaskbarState() { if (GlobalConfig.Instance.TaskbarProgress && TaskbarManager.IsPlatformSupported) @@ -760,7 +779,7 @@ private void UpdateTaskbarButtons() } _toggleTButton.Enabled = true; } - } + }*/ private void OpenTrackViewer(object sender, EventArgs e) { diff --git a/VG Music Studio/VG Music Studio.csproj b/VG Music Studio/VG Music Studio.csproj index d2e743d6..4ec138bf 100644 --- a/VG Music Studio/VG Music Studio.csproj +++ b/VG Music Studio/VG Music Studio.csproj @@ -1,17 +1,8 @@ - - - + - Debug - AnyCPU - {97C8ACF8-66A3-4321-91D6-3E94EACA577F} + net5.0-windows WinExe Kermalis.VGMusicStudio - VG Music Studio - v4.8 - 512 - true - false publish\ true @@ -27,28 +18,17 @@ 1.0.0.%2a false true + false + true + true + true - AnyCPU - true - full - false ..\Build\ - DEBUG;TRACE - prompt - 4 Off - false - AnyCPU - pdbonly - true ..\Build\ - TRACE - prompt - 4 - false On @@ -58,180 +38,15 @@ Kermalis.VGMusicStudio.Program - - Dependencies\DLS2.dll - - - ..\packages\EndianBinaryIO.1.1.1\lib\netstandard2.0\EndianBinaryIO.dll - - - ..\packages\Microsoft.WindowsAPICodePack-Core.1.1.0.2\lib\Microsoft.WindowsAPICodePack.dll - - - ..\packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.Shell.dll - - - ..\packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.ShellExtensions.dll - - - ..\packages\NAudio.1.10.0\lib\net35\NAudio.dll - - - ..\packages\ObjectListView.Official.2.9.1\lib\net20\ObjectListView.dll - - - - - False - Dependencies\Sanford.Multimedia.Midi.dll - - - False - Dependencies\SoundFont2.dll - - - - - - - - - - - - - ..\packages\YamlDotNet.8.1.2\lib\net45\YamlDotNet.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Strings.resx - - - - + Component - - - - + Component - - - - - - - - - - - Always - - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - True - - - Always - - - Always - - - Always - - + Always - - - - - ResXFileCodeGenerator - Strings.Designer.cs - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - True - Settings.settings - True - - - - - - @@ -245,5 +60,17 @@ false - + + + + + + + + + + + + + \ No newline at end of file diff --git a/VG Music Studio/packages.config b/VG Music Studio/packages.config deleted file mode 100644 index c38b4791..00000000 --- a/VG Music Studio/packages.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file From de9b8db7a5e146b8d1a9ced22ab6a6108d352999 Mon Sep 17 00:00:00 2001 From: Davin Date: Tue, 30 Aug 2022 15:16:15 +1000 Subject: [PATCH 2/8] Updated changes to reflect the changes in the latest EndianBinaryIO commits, by sepparating the vars in the using functions --- DLS2/DLS2.csproj | 4 +- EndianBinaryIO/EndianBinaryIO.csproj | 2 +- EndianBinaryIO/EndianBinaryReader.cs | 36 +-- ObjectListView/ObjectListView2019.csproj | 10 +- .../Sanford.Multimedia.Midi.Core.csproj | 2 +- SoundFont2/SoundFont2.csproj | 9 +- VG Music Studio.sln | 8 +- VG Music Studio/Core/GBA/AlphaDream/Config.cs | 3 +- .../Core/GBA/AlphaDream/SoundFontSaver_DLS.cs | 29 ++- .../Core/GBA/AlphaDream/SoundFontSaver_SF2.cs | 12 +- VG Music Studio/Core/GBA/MP2K/Config.cs | 3 +- VG Music Studio/Core/GBA/MP2K/Player.cs | 7 +- VG Music Studio/Core/Mixer.cs | 1 - VG Music Studio/Core/NDS/DSE/Config.cs | 3 +- VG Music Studio/Core/NDS/DSE/Player.cs | 3 +- VG Music Studio/Core/NDS/DSE/SWD.cs | 3 +- VG Music Studio/Core/NDS/SDAT/SBNK.cs | 3 +- VG Music Studio/Core/NDS/SDAT/SDAT.cs | 3 +- VG Music Studio/Core/NDS/SDAT/SSEQ.cs | 3 +- VG Music Studio/Core/NDS/SDAT/SWAR.cs | 3 +- VG Music Studio/Core/VGMSDebug.cs | 3 +- VG Music Studio/Properties/Strings.resx | 4 +- VG Music Studio/UI/MainForm.cs | 220 +++++++++++++++++- VG Music Studio/VG Music Studio.csproj | 9 +- 24 files changed, 278 insertions(+), 105 deletions(-) diff --git a/DLS2/DLS2.csproj b/DLS2/DLS2.csproj index d328f52e..4da739f4 100644 --- a/DLS2/DLS2.csproj +++ b/DLS2/DLS2.csproj @@ -12,12 +12,12 @@ - net5.0 + netcoreapp3.1 Exe - netstandard2.0 + netcoreapp3.1 Auto none false diff --git a/EndianBinaryIO/EndianBinaryIO.csproj b/EndianBinaryIO/EndianBinaryIO.csproj index 043c3bad..45bbd006 100644 --- a/EndianBinaryIO/EndianBinaryIO.csproj +++ b/EndianBinaryIO/EndianBinaryIO.csproj @@ -1,7 +1,7 @@  - net5.0 + netcoreapp3.1 Library Kermalis.EndianBinaryIO Kermalis diff --git a/EndianBinaryIO/EndianBinaryReader.cs b/EndianBinaryIO/EndianBinaryReader.cs index 2112f07d..de2ec621 100644 --- a/EndianBinaryIO/EndianBinaryReader.cs +++ b/EndianBinaryIO/EndianBinaryReader.cs @@ -5,7 +5,7 @@ namespace Kermalis.EndianBinaryIO { - public class EndianBinaryReader : IDisposable + public class EndianBinaryReader { public Stream BaseStream { get; } private Endianness _endianness; @@ -37,9 +37,6 @@ public BooleanSize BooleanSize public Encoding Encoding { get; set; } private byte[] _buffer; - private bool disposedValue; - - //public object Type; public EndianBinaryReader(Stream baseStream, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8) { @@ -54,7 +51,7 @@ public EndianBinaryReader(Stream baseStream, Endianness endianness = Endianness. BaseStream = baseStream; Endianness = endianness; BooleanSize = booleanSize; - Encoding = Encoding.Default; + Encoding = Encoding.ASCII; } public EndianBinaryReader(Stream baseStream, Encoding encoding, Endianness endianness = Endianness.LittleEndian, BooleanSize booleanSize = BooleanSize.U8) { @@ -892,34 +889,5 @@ public void ReadIntoObject(object obj, long offset) BaseStream.Position = offset; ReadIntoObject(obj); } - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~EndianBinaryReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } } } diff --git a/ObjectListView/ObjectListView2019.csproj b/ObjectListView/ObjectListView2019.csproj index 2abfb267..bc8d730f 100644 --- a/ObjectListView/ObjectListView2019.csproj +++ b/ObjectListView/ObjectListView2019.csproj @@ -1,6 +1,6 @@  - net5.0-windows + netcoreapp3.1 Library BrightIdeasSoftware ObjectListView @@ -32,12 +32,8 @@ - - Component - - - Component - + + diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi.Core.csproj b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi.Core.csproj index e5fdebe9..252ac39d 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi.Core.csproj +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi.Core.csproj @@ -1,6 +1,6 @@  - net5.0 + netcoreapp3.1 Local diff --git a/SoundFont2/SoundFont2.csproj b/SoundFont2/SoundFont2.csproj index 2674a3ec..8b7d4a4b 100644 --- a/SoundFont2/SoundFont2.csproj +++ b/SoundFont2/SoundFont2.csproj @@ -1,8 +1,6 @@ - net5.0 - Release Kermalis SoundFont2 @@ -10,10 +8,15 @@ SoundFont2 Kermalis.SoundFont2 1.0.0.0 + ..\Build + + netcoreapp3.1 + + - ..\Build + netcoreapp3.1 Auto none false diff --git a/VG Music Studio.sln b/VG Music Studio.sln index e58a64ec..5d96e605 100644 --- a/VG Music Studio.sln +++ b/VG Music Studio.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31605.320 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32804.467 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VG Music Studio", "VG Music Studio\VG Music Studio.csproj", "{97C8ACF8-66A3-4321-91D6-3E94EACA577F}" EndProject @@ -37,8 +37,8 @@ Global {4DEFE207-7C02-4AF5-9710-06536A1EB2AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {4DEFE207-7C02-4AF5-9710-06536A1EB2AB}.Release|Any CPU.ActiveCfg = Release|Any CPU {4DEFE207-7C02-4AF5-9710-06536A1EB2AB}.Release|Any CPU.Build.0 = Release|Any CPU - {53B52F18-AEB4-47C8-A07B-9F3082A45964}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {53B52F18-AEB4-47C8-A07B-9F3082A45964}.Debug|Any CPU.Build.0 = Release|Any CPU + {53B52F18-AEB4-47C8-A07B-9F3082A45964}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53B52F18-AEB4-47C8-A07B-9F3082A45964}.Debug|Any CPU.Build.0 = Debug|Any CPU {53B52F18-AEB4-47C8-A07B-9F3082A45964}.Release|Any CPU.ActiveCfg = Release|Any CPU {53B52F18-AEB4-47C8-A07B-9F3082A45964}.Release|Any CPU.Build.0 = Release|Any CPU {6220611C-85D7-45B5-9857-45C0F0FE62A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/VG Music Studio/Core/GBA/AlphaDream/Config.cs b/VG Music Studio/Core/GBA/AlphaDream/Config.cs index c1689303..a925dc64 100644 --- a/VG Music Studio/Core/GBA/AlphaDream/Config.cs +++ b/VG Music Studio/Core/GBA/AlphaDream/Config.cs @@ -13,6 +13,7 @@ internal class Config : Core.Config { public readonly byte[] ROM; public readonly EndianBinaryReader Reader; + public readonly MemoryStream Stream; public readonly string GameCode; public readonly byte Version; @@ -214,7 +215,7 @@ public override string GetSongName(long index) public override void Dispose() { - Reader.Dispose(); + Stream.Dispose(); } } } diff --git a/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs b/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs index 88af904f..b281c3d9 100644 --- a/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs +++ b/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs @@ -1,5 +1,4 @@ using Kermalis.DLS2; -using System; using System.Collections.Generic; using System.Diagnostics; @@ -8,16 +7,16 @@ namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream internal sealed class SoundFontSaver_DLS { // Since every key will use the same articulation data, just store one instance - private static readonly Level2ArticulatorChunk _art2 = new Level2ArticulatorChunk + private static readonly Level2ArticulatorChunk _art2 = new Level2ArticulatorChunk() { - new Level2ArticulatorConnectionBlock { Destination = Level2ArticulatorDestination.LFOFrequency, Scale = 2786 }, - new Level2ArticulatorConnectionBlock { Destination = Level2ArticulatorDestination.VIBFrequency, Scale = 2786 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.KeyNumber, Destination = Level2ArticulatorDestination.Pitch }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.Modulation_CC1, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.ChannelPressure, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Pan_CC10, Destination = Level2ArticulatorDestination.Pan, BipolarSource = true, Scale = 0xFE0000 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.ChorusSend_CC91, Destination = Level2ArticulatorDestination.Reverb, Scale = 0xC80000 }, - new Level2ArticulatorConnectionBlock { Source = Level2ArticulatorSource.Reverb_SendCC93, Destination = Level2ArticulatorDestination.Chorus, Scale = 0xC80000 } + new Level2ArticulatorConnectionBlock() { Destination = Level2ArticulatorDestination.LFOFrequency, Scale = 2786 }, + new Level2ArticulatorConnectionBlock() { Destination = Level2ArticulatorDestination.VIBFrequency, Scale = 2786 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.KeyNumber, Destination = Level2ArticulatorDestination.Pitch }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.Modulation_CC1, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.ChannelPressure, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Pan_CC10, Destination = Level2ArticulatorDestination.Pan, BipolarSource = true, Scale = 0xFE0000 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.ChorusSend_CC91, Destination = Level2ArticulatorDestination.Reverb, Scale = 0xC80000 }, + new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Reverb_SendCC93, Destination = Level2ArticulatorDestination.Chorus, Scale = 0xC80000 } }; public static void Save(Config config, string path) @@ -62,7 +61,7 @@ private static void AddInfo(Config config, DLS dls) fmt.WaveInfo.BlockAlign = 1; fmt.FormatInfo.BitsPerSample = 8; // Create wave sample chunk and add loop if there is one - var wsmp = new WaveSampleChunk + var wsmp = new WaveSampleChunk() { UnityNote = 60, Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression @@ -78,7 +77,7 @@ private static void AddInfo(Config config, DLS dls) } // Get PCM sample byte[] pcm = new byte[sh.Length]; - Array.Copy(config.ROM, ofs + 0x10, pcm, 0, sh.Length); + System.Array.Copy(config.ROM, ofs + 0x10, pcm, 0, sh.Length); // Add int dlsIndex = waves.Count; @@ -145,13 +144,13 @@ void Add(ushort low, ushort high, ushort baseKey) lrgn.Add(new ListChunk("rgn2") { rgnh, - new WaveSampleChunk + new WaveSampleChunk() { UnityNote = baseKey, Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression, Loop = value.Item1.Loop }, - new WaveLinkChunk + new WaveLinkChunk() { Channels = WaveLinkChannels.Left, TableIndex = (uint)value.Item2 @@ -178,4 +177,4 @@ void Add(ushort low, ushort high, ushort baseKey) } } } -} \ No newline at end of file +} diff --git a/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs b/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs index 6dbd0f1c..6e0aa028 100644 --- a/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs +++ b/VG Music Studio/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs @@ -10,17 +10,17 @@ internal sealed class SoundFontSaver_SF2 public static void Save(Config config, string path) { var sf2 = new SF2(); - AddInfo(config, sf2.InfoChunk); + AddInfo(config, sf2); Dictionary sampleDict = AddSamples(config, sf2); AddInstruments(config, sf2, sampleDict); sf2.Save(path); } - private static void AddInfo(Config config, InfoListChunk chunk) + private static void AddInfo(Config config, SF2 sf2) { - chunk.Bank = config.Name; - //chunk.Copyright = config.Creator; - chunk.Tools = Util.Utils.ProgramName + " by Kermalis"; + sf2.InfoChunk.Bank = config.Name; + //sf2.InfoChunk.Copyright = config.Creator; + sf2.InfoChunk.Tools = Util.Utils.ProgramName + " by Kermalis"; } private static Dictionary AddSamples(Config config, SF2 sf2) @@ -94,4 +94,4 @@ private static void AddInstruments(Config config, SF2 sf2, Dictionary left) { diff --git a/VG Music Studio/Core/Mixer.cs b/VG Music Studio/Core/Mixer.cs index 0ca5ae82..d6766aee 100644 --- a/VG Music Studio/Core/Mixer.cs +++ b/VG Music Studio/Core/Mixer.cs @@ -4,7 +4,6 @@ using NAudio.CoreAudioApi.Interfaces; using NAudio.Wave; using System; -using static Kermalis.VGMusicStudio.UI.SongInfoControl; namespace Kermalis.VGMusicStudio.Core { diff --git a/VG Music Studio/Core/NDS/DSE/Config.cs b/VG Music Studio/Core/NDS/DSE/Config.cs index 25f856ca..6d4e4cac 100644 --- a/VG Music Studio/Core/NDS/DSE/Config.cs +++ b/VG Music Studio/Core/NDS/DSE/Config.cs @@ -22,8 +22,9 @@ public Config(string bgmPath) var songs = new Song[BGMFiles.Length]; for (int i = 0; i < BGMFiles.Length; i++) { - using (var reader = new EndianBinaryReader(File.OpenRead(BGMFiles[i]))) + using (var readfile = File.OpenRead(BGMFiles[i])) { + var reader = new EndianBinaryReader(readfile); SMD.Header header = reader.ReadObject(); songs[i] = new Song(i, $"{Path.GetFileNameWithoutExtension(BGMFiles[i])} - {new string(header.Label.TakeWhile(c => c != '\0').ToArray())}"); } diff --git a/VG Music Studio/Core/NDS/DSE/Player.cs b/VG Music Studio/Core/NDS/DSE/Player.cs index 4fa2b81e..a5342817 100644 --- a/VG Music Studio/Core/NDS/DSE/Player.cs +++ b/VG Music Studio/Core/NDS/DSE/Player.cs @@ -119,8 +119,9 @@ public void LoadSong(long index) string bgm = _config.BGMFiles[index]; _localSWD = new SWD(Path.ChangeExtension(bgm, "swd")); _smdFile = File.ReadAllBytes(bgm); - using (var reader = new EndianBinaryReader(new MemoryStream(_smdFile))) + using (var stream = new MemoryStream(_smdFile)) { + var reader = new EndianBinaryReader(stream); SMD.Header header = reader.ReadObject(); SMD.ISongChunk songChunk; switch (header.Version) diff --git a/VG Music Studio/Core/NDS/DSE/SWD.cs b/VG Music Studio/Core/NDS/DSE/SWD.cs index 4e9f984a..9d62be73 100644 --- a/VG Music Studio/Core/NDS/DSE/SWD.cs +++ b/VG Music Studio/Core/NDS/DSE/SWD.cs @@ -323,8 +323,9 @@ public class LFOInfo public SWD(string path) { - using (var reader = new EndianBinaryReader(new MemoryStream(File.ReadAllBytes(path)))) + using (var stream = new MemoryStream(File.ReadAllBytes(path))) { + var reader = new EndianBinaryReader(stream); Type = reader.ReadString(4, false); Unknown = reader.ReadBytes(4); Length = reader.ReadUInt32(); diff --git a/VG Music Studio/Core/NDS/SDAT/SBNK.cs b/VG Music Studio/Core/NDS/SDAT/SBNK.cs index c0b016d8..716fbd78 100644 --- a/VG Music Studio/Core/NDS/SDAT/SBNK.cs +++ b/VG Music Studio/Core/NDS/SDAT/SBNK.cs @@ -125,8 +125,9 @@ public void Write(EndianBinaryWriter ew) public SBNK(byte[] bytes) { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) + using (var stream = new MemoryStream(bytes)) { + var er = new EndianBinaryReader(stream); er.ReadIntoObject(this); } } diff --git a/VG Music Studio/Core/NDS/SDAT/SDAT.cs b/VG Music Studio/Core/NDS/SDAT/SDAT.cs index 0f4c4c7f..b1df0a55 100644 --- a/VG Music Studio/Core/NDS/SDAT/SDAT.cs +++ b/VG Music Studio/Core/NDS/SDAT/SDAT.cs @@ -209,8 +209,9 @@ public void Write(EndianBinaryWriter ew) public SDAT(byte[] bytes) { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) + using (var stream = new MemoryStream(bytes)) { + var er = new EndianBinaryReader(stream); FileHeader = er.ReadObject(); SYMBOffset = er.ReadInt32(); SYMBLength = er.ReadInt32(); diff --git a/VG Music Studio/Core/NDS/SDAT/SSEQ.cs b/VG Music Studio/Core/NDS/SDAT/SSEQ.cs index 3d97b1e1..e5ab3868 100644 --- a/VG Music Studio/Core/NDS/SDAT/SSEQ.cs +++ b/VG Music Studio/Core/NDS/SDAT/SSEQ.cs @@ -14,8 +14,9 @@ internal class SSEQ public SSEQ(byte[] bytes) { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) + using (var stream = new MemoryStream(bytes)) { + var er = new EndianBinaryReader(stream); FileHeader = er.ReadObject(); BlockType = er.ReadString(4, false); BlockSize = er.ReadInt32(); diff --git a/VG Music Studio/Core/NDS/SDAT/SWAR.cs b/VG Music Studio/Core/NDS/SDAT/SWAR.cs index c7a982c8..3a92651a 100644 --- a/VG Music Studio/Core/NDS/SDAT/SWAR.cs +++ b/VG Music Studio/Core/NDS/SDAT/SWAR.cs @@ -45,8 +45,9 @@ public void Write(EndianBinaryWriter ew) public SWAR(byte[] bytes) { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) + using (var stream = new MemoryStream(bytes)) { + var er = new EndianBinaryReader(stream); FileHeader = er.ReadObject(); BlockType = er.ReadString(4, false); BlockSize = er.ReadInt32(); diff --git a/VG Music Studio/Core/VGMSDebug.cs b/VG Music Studio/Core/VGMSDebug.cs index c30a6ad3..0d73ae3e 100644 --- a/VG Music Studio/Core/VGMSDebug.cs +++ b/VG Music Studio/Core/VGMSDebug.cs @@ -88,8 +88,9 @@ public static void GBAGameCodeScan(string path) { try { - var reader = new EndianBinaryReader(File.OpenRead(file)); + using (var readfile = File.OpenRead(file)) { + var reader = new EndianBinaryReader(readfile); string gameCode = reader.ReadString(3, false, 0xAC); char regionCode = reader.ReadChar(0xAF); byte version = reader.ReadByte(0xBC); diff --git a/VG Music Studio/Properties/Strings.resx b/VG Music Studio/Properties/Strings.resx index 7ffdfc8b..228bc168 100644 --- a/VG Music Studio/Properties/Strings.resx +++ b/VG Music Studio/Properties/Strings.resx @@ -374,7 +374,7 @@ {0} is the file name. - An audio output device was disconnected. {0} was disconnected. - {0} is the audio output device. + An audio output device was {0}. {1} was disconnected. + {0} is the status of the device. {1} is the device itself \ No newline at end of file diff --git a/VG Music Studio/UI/MainForm.cs b/VG Music Studio/UI/MainForm.cs index 05b1a4cc..8a8d0bbc 100644 --- a/VG Music Studio/UI/MainForm.cs +++ b/VG Music Studio/UI/MainForm.cs @@ -6,10 +6,14 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; using System.Windows.Forms; +using System.Windows.Interop; namespace Kermalis.VGMusicStudio.UI { @@ -49,6 +53,203 @@ internal class MainForm : ThemedForm #endregion + #region Folder Picker + + // Originally made by: Simon Mourier + // Source: https://stackoverflow.com/questions/11624298/how-do-i-use-openfiledialog-to-select-a-folder/66187224#66187224 + + public class FolderPicker + { + public virtual string ResultPath { get; protected set; } + public virtual string ResultName { get; protected set; } + public virtual string InputPath { get; set; } + public virtual bool ForceFileSystem { get; set; } + public virtual string Title { get; set; } + public virtual string OkButtonLabel { get; set; } + public virtual string FileNameLabel { get; set; } + + protected virtual int SetOptions(int options) + { + if (ForceFileSystem) + { + options |= (int)FOS.FOS_FORCEFILESYSTEM; + } + return options; + } + + // for all .NET + public virtual bool? ShowDialog(IntPtr owner, bool throwOnError = false) + { + var dialog = (IFileOpenDialog)new FileOpenDialog(); + if (!string.IsNullOrEmpty(InputPath)) + { + if (CheckHr(SHCreateItemFromParsingName(InputPath, null, typeof(IShellItem).GUID, out var item), throwOnError) != 0) + return null; + + dialog.SetFolder(item); + } + + var options = FOS.FOS_PICKFOLDERS; + options = (FOS)SetOptions((int)options); + dialog.SetOptions(options); + + if (Title != null) + { + dialog.SetTitle(Title); + } + + if (OkButtonLabel != null) + { + dialog.SetOkButtonLabel(OkButtonLabel); + } + + if (FileNameLabel != null) + { + dialog.SetFileName(FileNameLabel); + } + + if (owner == IntPtr.Zero) + { + owner = Process.GetCurrentProcess().MainWindowHandle; + if (owner == IntPtr.Zero) + { + owner = GetDesktopWindow(); + } + } + + var hr = dialog.Show(owner); + if (hr == ERROR_CANCELLED) + return null; + + if (CheckHr(hr, throwOnError) != 0) + return null; + + if (CheckHr(dialog.GetResult(out var result), throwOnError) != 0) + return null; + + if (CheckHr(result.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING, out var path), throwOnError) != 0) + return null; + + ResultPath = path; + + if (CheckHr(result.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEEDITING, out path), false) == 0) + { + ResultName = path; + } + return true; + } + + private static int CheckHr(int hr, bool throwOnError) + { + if (hr != 0) + { + if (throwOnError) + Marshal.ThrowExceptionForHR(hr); + } + return hr; + } + + [DllImport("shell32")] + private static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem ppv); + + [DllImport("user32")] + private static extern IntPtr GetDesktopWindow(); + +#pragma warning disable IDE1006 // Naming Styles + private const int ERROR_CANCELLED = unchecked((int)0x800704C7); +#pragma warning restore IDE1006 // Naming Styles + + [ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")] // CLSID_FileOpenDialog + private class FileOpenDialog + { + } + + [ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IFileOpenDialog + { + [PreserveSig] int Show(IntPtr parent); // IModalWindow + [PreserveSig] int SetFileTypes(); // not fully defined + [PreserveSig] int SetFileTypeIndex(int iFileType); + [PreserveSig] int GetFileTypeIndex(out int piFileType); + [PreserveSig] int Advise(); // not fully defined + [PreserveSig] int Unadvise(); + [PreserveSig] int SetOptions(FOS fos); + [PreserveSig] int GetOptions(out FOS pfos); + [PreserveSig] int SetDefaultFolder(IShellItem psi); + [PreserveSig] int SetFolder(IShellItem psi); + [PreserveSig] int GetFolder(out IShellItem ppsi); + [PreserveSig] int GetCurrentSelection(out IShellItem ppsi); + [PreserveSig] int SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); + [PreserveSig] int GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + [PreserveSig] int SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + [PreserveSig] int SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); + [PreserveSig] int SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + [PreserveSig] int GetResult(out IShellItem ppsi); + [PreserveSig] int AddPlace(IShellItem psi, int alignment); + [PreserveSig] int SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + [PreserveSig] int Close(int hr); + [PreserveSig] int SetClientGuid(); // not fully defined + [PreserveSig] int ClearClientData(); + [PreserveSig] int SetFilter([MarshalAs(UnmanagedType.IUnknown)] object pFilter); + [PreserveSig] int GetResults([MarshalAs(UnmanagedType.IUnknown)] out object ppenum); + [PreserveSig] int GetSelectedItems([MarshalAs(UnmanagedType.IUnknown)] out object ppsai); + } + + [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IShellItem + { + [PreserveSig] int BindToHandler(); // not fully defined + [PreserveSig] int GetParent(); // not fully defined + [PreserveSig] int GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); + [PreserveSig] int GetAttributes(); // not fully defined + [PreserveSig] int Compare(); // not fully defined + } + +#pragma warning disable CA1712 // Do not prefix enum values with type name + private enum SIGDN : uint + { + SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000, + SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000, + SIGDN_FILESYSPATH = 0x80058000, + SIGDN_NORMALDISPLAY = 0, + SIGDN_PARENTRELATIVE = 0x80080001, + SIGDN_PARENTRELATIVEEDITING = 0x80031001, + SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001, + SIGDN_PARENTRELATIVEPARSING = 0x80018001, + SIGDN_URL = 0x80068000 + } + + [Flags] + private enum FOS + { + FOS_OVERWRITEPROMPT = 0x2, + FOS_STRICTFILETYPES = 0x4, + FOS_NOCHANGEDIR = 0x8, + FOS_PICKFOLDERS = 0x20, + FOS_FORCEFILESYSTEM = 0x40, + FOS_ALLNONSTORAGEITEMS = 0x80, + FOS_NOVALIDATE = 0x100, + FOS_ALLOWMULTISELECT = 0x200, + FOS_PATHMUSTEXIST = 0x800, + FOS_FILEMUSTEXIST = 0x1000, + FOS_CREATEPROMPT = 0x2000, + FOS_SHAREAWARE = 0x4000, + FOS_NOREADONLYRETURN = 0x8000, + FOS_NOTESTFILECREATE = 0x10000, + FOS_HIDEMRUPLACES = 0x20000, + FOS_HIDEPINNEDPLACES = 0x40000, + FOS_NODEREFERENCELINKS = 0x100000, + FOS_OKBUTTONNEEDSINTERACTION = 0x200000, + FOS_DONTADDTORECENT = 0x2000000, + FOS_FORCESHOWHIDDEN = 0x10000000, + FOS_DEFAULTNOMINIMODE = 0x20000000, + FOS_FORCEPREVIEWPANEON = 0x40000000, + FOS_SUPPORTSTREAMABLEITEMS = unchecked((int)0x80000000) + } +#pragma warning restore CA1712 // Do not prefix enum values with type name + } + #endregion + protected override void Dispose(bool disposing) { if (disposing) @@ -325,22 +526,21 @@ private void EndCurrentPlaylist(object sender, EventArgs e) private void OpenDSE(object sender, EventArgs e) { - var d = new FolderBrowserDialog + var d = new FolderPicker { - UseDescriptionForTitle = true, - Description = Strings.MenuOpenDSE + Title = Strings.MenuOpenDSE, }; - if (d.ShowDialog() == DialogResult.OK) + //var f = File.OpenRead(d.SelectedPath); + if (d.ShowDialog(Handle) == true) { DisposeEngine(); bool success; - try - { - new Engine(Engine.EngineType.NDS_DSE, d.SelectedPath); - success = true; - } - catch (Exception ex) + + new Engine(Engine.EngineType.NDS_DSE, d.ResultPath); + success = true; + if (success != true) { + Exception ex = new Exception(); FlexibleMessageBox.Show(ex, Strings.ErrorOpenDSE); success = false; } diff --git a/VG Music Studio/VG Music Studio.csproj b/VG Music Studio/VG Music Studio.csproj index 555dc1bf..7ee777f4 100644 --- a/VG Music Studio/VG Music Studio.csproj +++ b/VG Music Studio/VG Music Studio.csproj @@ -47,9 +47,12 @@ Always - - - + + Always + + + Always + From 48de784d435bba48dc3694e25f01e0e7457b740a Mon Sep 17 00:00:00 2001 From: Davin Date: Tue, 30 Aug 2022 15:22:03 +1000 Subject: [PATCH 3/8] Forgot to update this to .net6 and the description for EndianBinaryIO --- EndianBinaryIO/EndianBinaryIO.csproj | 2 +- VG Music Studio/VG Music Studio.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/EndianBinaryIO/EndianBinaryIO.csproj b/EndianBinaryIO/EndianBinaryIO.csproj index 45bbd006..2aa1ec85 100644 --- a/EndianBinaryIO/EndianBinaryIO.csproj +++ b/EndianBinaryIO/EndianBinaryIO.csproj @@ -15,7 +15,7 @@ true - A .NET 5 library that can read and write primitives, enums, arrays, and strings to streams and byte arrays using specified endianness, string encoding, and boolean sizes. + A .NET Core library compatible with any newer .NET SDK versions. This library can read and write primitives, enums, arrays, and strings to streams and byte arrays using specified endianness, string encoding, and boolean sizes. Objects can also be read from/written to streams via reflection and attributes. Project URL ― https://github.com/Kermalis/EndianBinaryIO https://github.com/Kermalis/EndianBinaryIO diff --git a/VG Music Studio/VG Music Studio.csproj b/VG Music Studio/VG Music Studio.csproj index 7ee777f4..9c3031af 100644 --- a/VG Music Studio/VG Music Studio.csproj +++ b/VG Music Studio/VG Music Studio.csproj @@ -1,6 +1,6 @@  - net5.0-windows + net6.0-windows WinExe Kermalis.VGMusicStudio false From 6cefed8a06c097b179fcbd0d49d321aea7c5ce18 Mon Sep 17 00:00:00 2001 From: Davin Date: Tue, 30 Aug 2022 15:35:36 +1000 Subject: [PATCH 4/8] Cleanup of legacy .NET related stuff --- .../DLS2/.gitattributes | 63 - Legacy Repo (.NET Framework)/DLS2/.gitignore | 262 --- Legacy Repo (.NET Framework)/DLS2/DLS2.sln | 25 - .../DLS2/DLS2/Chunks/Chunk.cs | 98 - .../DLS2/DLS2/Chunks/CollectionHeaderChunk.cs | 29 - .../DLS2/DLS2/Chunks/ConditionalChunk.cs | 36 - .../DLS2/DLS2/Chunks/DLSIDChunk.cs | 45 - .../DLS2/DLS2/Chunks/DataChunk.cs | 10 - .../DLS2/DLS2/Chunks/FormatChunk.cs | 105 -- .../DLS2/DLS2/Chunks/InfoSubChunk.cs | 53 - .../DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs | 46 - .../DLS2/Chunks/Level1ArticulatorChunk.cs | 119 -- .../DLS2/Chunks/Level2ArticulatorChunk.cs | 119 -- .../DLS2/DLS2/Chunks/ListChunk.cs | 112 -- .../DLS2/DLS2/Chunks/PoolTableChunk.cs | 71 - .../DLS2/DLS2/Chunks/RawDataChunk.cs | 50 - .../DLS2/DLS2/Chunks/RegionHeaderChunk.cs | 52 - .../DLS2/DLS2/Chunks/UnsupportedChunk.cs | 10 - .../DLS2/DLS2/Chunks/VersionChunk.cs | 14 - .../DLS2/DLS2/Chunks/WaveLinkChunk.cs | 43 - .../DLS2/DLS2/Chunks/WaveSampleChunk.cs | 69 - Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs | 235 --- .../DLS2/DLS2/DLS2.csproj | 30 - .../DLS2/DLS2/Enums/Level1ArticulatorEnums.cs | 44 - .../DLS2/DLS2/Enums/Level2ArticulatorEnums.cs | 69 - .../DLS2/DLS2/Enums/WaveFormat.cs | 15 - .../DLS2/DLS2/Enums/WaveLinkChannels.cs | 28 - .../DLS2/DLS2/Enums/WaveLinkOptions.cs | 12 - .../DLS2/DLS2/Enums/WaveSampleLoop.cs | 8 - .../DLS2/DLS2/Enums/WaveSampleOptions.cs | 12 - .../DLS2/DLS2/Structs/ConnectionBlock.cs | 163 -- .../DLS2/DLS2/Structs/DLSID.cs | 122 -- .../DLS2/DLS2/Structs/MIDILocale.cs | 87 - .../DLS2/DLS2/Structs/Range.cs | 28 - .../DLS2/DLS2/Structs/WaveInfo.cs | 32 - .../DLS2/DLS2/Structs/WaveSampleLoop.cs | 33 - Legacy Repo (.NET Framework)/DLS2/LICENSE.md | 23 - .../SoundFont2/.gitattributes | 63 - .../SoundFont2/.gitignore | 262 --- .../SoundFont2/LICENSE.md | 23 - .../SoundFont2/README.md | 11 - .../SoundFont2/SoundFont2.sln | 22 - .../SoundFont2/SoundFont2/SF2.cs | 152 -- .../SoundFont2/SoundFont2/SF2Chunks.cs | 1105 ----------- .../SoundFont2/SoundFont2/SF2Types.cs | 142 -- .../SoundFont2/SoundFont2/SoundFont2.csproj | 26 - Legacy Repo (.NET Framework)/VGMusicStudio | 1 - .../VG Music Studio/NuGetUpgradeLog.html | 223 --- .../VG Music Studio/VG Music Studio.csproj | 411 ---- .../655cc60b/VG Music Studio/packages.config | 65 - VG Music Studio.backup/AlphaDream.yaml | 201 -- VG Music Studio.backup/Config.yaml | 137 -- VG Music Studio.backup/Core/ADPCMDecoder.cs | 90 - VG Music Studio.backup/Core/Assembler.cs | 351 ---- VG Music Studio.backup/Core/Config.cs | 90 - VG Music Studio.backup/Core/Engine.cs | 92 - .../Core/GBA/AlphaDream/Channel.cs | 237 --- .../Core/GBA/AlphaDream/Commands.cs | 113 -- .../Core/GBA/AlphaDream/Config.cs | 220 --- .../Core/GBA/AlphaDream/Enums.cs | 16 - .../Core/GBA/AlphaDream/Mixer.cs | 137 -- .../Core/GBA/AlphaDream/Player.cs | 696 ------- .../Core/GBA/AlphaDream/SoundFontSaver_DLS.cs | 180 -- .../Core/GBA/AlphaDream/SoundFontSaver_SF2.cs | 97 - .../Core/GBA/AlphaDream/Structs.cs | 33 - .../Core/GBA/AlphaDream/Track.cs | 69 - .../Core/GBA/MP2K/Channel.cs | 777 -------- .../Core/GBA/MP2K/Commands.cs | 193 -- .../Core/GBA/MP2K/Config.cs | 242 --- VG Music Studio.backup/Core/GBA/MP2K/Enums.cs | 72 - VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs | 275 --- .../Core/GBA/MP2K/Player.cs | 1510 --------------- .../Core/GBA/MP2K/Structs.cs | 73 - VG Music Studio.backup/Core/GBA/MP2K/Track.cs | 174 -- VG Music Studio.backup/Core/GBA/MP2K/Utils.cs | 174 -- VG Music Studio.backup/Core/GBA/Utils.cs | 13 - VG Music Studio.backup/Core/GlobalConfig.cs | 109 -- VG Music Studio.backup/Core/Mixer.cs | 87 - .../Core/NDS/DSE/Channel.cs | 368 ---- .../Core/NDS/DSE/Commands.cs | 130 -- VG Music Studio.backup/Core/NDS/DSE/Config.cs | 45 - VG Music Studio.backup/Core/NDS/DSE/Enums.cs | 22 - VG Music Studio.backup/Core/NDS/DSE/Mixer.cs | 220 --- VG Music Studio.backup/Core/NDS/DSE/Player.cs | 1040 ---------- VG Music Studio.backup/Core/NDS/DSE/SMD.cs | 61 - VG Music Studio.backup/Core/NDS/DSE/SWD.cs | 471 ----- VG Music Studio.backup/Core/NDS/DSE/Track.cs | 72 - VG Music Studio.backup/Core/NDS/DSE/Utils.cs | 53 - .../Core/NDS/SDAT/Channel.cs | 387 ---- .../Core/NDS/SDAT/Commands.cs | 439 ----- .../Core/NDS/SDAT/Config.cs | 40 - VG Music Studio.backup/Core/NDS/SDAT/Enums.cs | 40 - .../Core/NDS/SDAT/FileHeader.cs | 31 - VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs | 252 --- .../Core/NDS/SDAT/Player.cs | 1680 ----------------- VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs | 184 -- VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs | 234 --- VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs | 28 - VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs | 65 - VG Music Studio.backup/Core/NDS/SDAT/Track.cs | 193 -- VG Music Studio.backup/Core/NDS/SDAT/Utils.cs | 345 ---- VG Music Studio.backup/Core/NDS/Utils.cs | 7 - VG Music Studio.backup/Core/Player.cs | 36 - VG Music Studio.backup/Core/SongEvent.cs | 24 - VG Music Studio.backup/Core/VGMSDebug.cs | 112 -- VG Music Studio.backup/Dependencies/DLS2.dll | Bin 36352 -> 0 bytes .../Dependencies/Sanford.Multimedia.Midi.dll | Bin 180224 -> 0 bytes .../Dependencies/SoundFont2.dll | Bin 31744 -> 0 bytes VG Music Studio.backup/MP2K.yaml | 1443 -------------- VG Music Studio.backup/MPlayDef.s | 465 ----- VG Music Studio.backup/Program.cs | 30 - .../Properties/AssemblyInfo.cs | 39 - VG Music Studio.backup/Properties/Icon.ico | Bin 3238 -> 0 bytes VG Music Studio.backup/Properties/Icon16.png | Bin 359 -> 0 bytes VG Music Studio.backup/Properties/Icon24.png | Bin 476 -> 0 bytes VG Music Studio.backup/Properties/Icon32.png | Bin 596 -> 0 bytes VG Music Studio.backup/Properties/Icon48.png | Bin 870 -> 0 bytes VG Music Studio.backup/Properties/Icon528.png | Bin 10133 -> 0 bytes VG Music Studio.backup/Properties/Next.ico | Bin 140062 -> 0 bytes VG Music Studio.backup/Properties/Next.png | Bin 4026 -> 0 bytes VG Music Studio.backup/Properties/Pause.ico | Bin 118062 -> 0 bytes VG Music Studio.backup/Properties/Pause.png | Bin 1450 -> 0 bytes VG Music Studio.backup/Properties/Play.ico | Bin 140062 -> 0 bytes VG Music Studio.backup/Properties/Play.png | Bin 2298 -> 0 bytes .../Properties/Playlist.png | Bin 3045 -> 0 bytes .../Properties/Previous.ico | Bin 140062 -> 0 bytes .../Properties/Previous.png | Bin 3708 -> 0 bytes .../Properties/Resources.Designer.cs | 133 -- .../Properties/Resources.resx | 142 -- .../Properties/Settings.Designer.cs | 26 - .../Properties/Settings.settings | 7 - VG Music Studio.backup/Properties/Song.png | Bin 2328 -> 0 bytes .../Properties/Strings.Designer.cs | 747 -------- .../Properties/Strings.es.resx | 348 ---- .../Properties/Strings.it.resx | 348 ---- .../Properties/Strings.resx | 376 ---- VG Music Studio.backup/UI/ColorSlider.cs | 485 ----- .../UI/FlexibleMessageBox.cs | 697 ------- VG Music Studio.backup/UI/ImageComboBox.cs | 62 - VG Music Studio.backup/UI/MainForm.cs | 836 -------- VG Music Studio.backup/UI/PianoControl.cs | 183 -- VG Music Studio.backup/UI/SongInfoControl.cs | 354 ---- VG Music Studio.backup/UI/Theme.cs | 165 -- VG Music Studio.backup/UI/TrackViewer.cs | 113 -- VG Music Studio.backup/UI/ValueTextBox.cs | 104 - .../Util/BetterExceptions.cs | 24 - VG Music Studio.backup/Util/HSLColor.cs | 161 -- VG Music Studio.backup/Util/SampleUtils.cs | 16 - VG Music Studio.backup/Util/TimeBarrier.cs | 60 - VG Music Studio.backup/Util/Utils.cs | 153 -- VG Music Studio.backup/VG Music Studio.csproj | 433 ----- VG Music Studio.backup/app.config | 11 - VG Music Studio.backup/midi2agb.exe | Bin 3404655 -> 0 bytes VG Music Studio.backup/upgrade.backup | 1 - 154 files changed, 25877 deletions(-) delete mode 100644 Legacy Repo (.NET Framework)/DLS2/.gitattributes delete mode 100644 Legacy Repo (.NET Framework)/DLS2/.gitignore delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2.sln delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Chunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/CollectionHeaderChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ConditionalChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DLSIDChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DataChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/FormatChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InfoSubChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level1ArticulatorChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level2ArticulatorChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ListChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/PoolTableChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RawDataChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RegionHeaderChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/UnsupportedChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/VersionChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveLinkChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveSampleChunk.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/DLS2.csproj delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level1ArticulatorEnums.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level2ArticulatorEnums.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveFormat.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkChannels.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkOptions.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleLoop.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleOptions.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/ConnectionBlock.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/DLSID.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/MIDILocale.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/Range.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveInfo.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveSampleLoop.cs delete mode 100644 Legacy Repo (.NET Framework)/DLS2/LICENSE.md delete mode 100644 Legacy Repo (.NET Framework)/SoundFont2/.gitattributes delete mode 100644 Legacy Repo (.NET Framework)/SoundFont2/.gitignore delete mode 100644 Legacy Repo (.NET Framework)/SoundFont2/LICENSE.md delete mode 100644 Legacy Repo (.NET Framework)/SoundFont2/README.md delete mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2.sln delete mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2.cs delete mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Chunks.cs delete mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Types.cs delete mode 100644 Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SoundFont2.csproj delete mode 160000 Legacy Repo (.NET Framework)/VGMusicStudio delete mode 100644 MigrationBackup/655cc60b/VG Music Studio/NuGetUpgradeLog.html delete mode 100644 MigrationBackup/655cc60b/VG Music Studio/VG Music Studio.csproj delete mode 100644 MigrationBackup/655cc60b/VG Music Studio/packages.config delete mode 100644 VG Music Studio.backup/AlphaDream.yaml delete mode 100644 VG Music Studio.backup/Config.yaml delete mode 100644 VG Music Studio.backup/Core/ADPCMDecoder.cs delete mode 100644 VG Music Studio.backup/Core/Assembler.cs delete mode 100644 VG Music Studio.backup/Core/Config.cs delete mode 100644 VG Music Studio.backup/Core/Engine.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Channel.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Commands.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Config.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Enums.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Mixer.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Player.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Structs.cs delete mode 100644 VG Music Studio.backup/Core/GBA/AlphaDream/Track.cs delete mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Channel.cs delete mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Commands.cs delete mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Config.cs delete mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Enums.cs delete mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs delete mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Player.cs delete mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Structs.cs delete mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Track.cs delete mode 100644 VG Music Studio.backup/Core/GBA/MP2K/Utils.cs delete mode 100644 VG Music Studio.backup/Core/GBA/Utils.cs delete mode 100644 VG Music Studio.backup/Core/GlobalConfig.cs delete mode 100644 VG Music Studio.backup/Core/Mixer.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/Channel.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/Commands.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/Config.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/Enums.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/Mixer.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/Player.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/SMD.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/SWD.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/Track.cs delete mode 100644 VG Music Studio.backup/Core/NDS/DSE/Utils.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Channel.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Commands.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Config.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Enums.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/FileHeader.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Player.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Track.cs delete mode 100644 VG Music Studio.backup/Core/NDS/SDAT/Utils.cs delete mode 100644 VG Music Studio.backup/Core/NDS/Utils.cs delete mode 100644 VG Music Studio.backup/Core/Player.cs delete mode 100644 VG Music Studio.backup/Core/SongEvent.cs delete mode 100644 VG Music Studio.backup/Core/VGMSDebug.cs delete mode 100644 VG Music Studio.backup/Dependencies/DLS2.dll delete mode 100644 VG Music Studio.backup/Dependencies/Sanford.Multimedia.Midi.dll delete mode 100644 VG Music Studio.backup/Dependencies/SoundFont2.dll delete mode 100644 VG Music Studio.backup/MP2K.yaml delete mode 100644 VG Music Studio.backup/MPlayDef.s delete mode 100644 VG Music Studio.backup/Program.cs delete mode 100644 VG Music Studio.backup/Properties/AssemblyInfo.cs delete mode 100644 VG Music Studio.backup/Properties/Icon.ico delete mode 100644 VG Music Studio.backup/Properties/Icon16.png delete mode 100644 VG Music Studio.backup/Properties/Icon24.png delete mode 100644 VG Music Studio.backup/Properties/Icon32.png delete mode 100644 VG Music Studio.backup/Properties/Icon48.png delete mode 100644 VG Music Studio.backup/Properties/Icon528.png delete mode 100644 VG Music Studio.backup/Properties/Next.ico delete mode 100644 VG Music Studio.backup/Properties/Next.png delete mode 100644 VG Music Studio.backup/Properties/Pause.ico delete mode 100644 VG Music Studio.backup/Properties/Pause.png delete mode 100644 VG Music Studio.backup/Properties/Play.ico delete mode 100644 VG Music Studio.backup/Properties/Play.png delete mode 100644 VG Music Studio.backup/Properties/Playlist.png delete mode 100644 VG Music Studio.backup/Properties/Previous.ico delete mode 100644 VG Music Studio.backup/Properties/Previous.png delete mode 100644 VG Music Studio.backup/Properties/Resources.Designer.cs delete mode 100644 VG Music Studio.backup/Properties/Resources.resx delete mode 100644 VG Music Studio.backup/Properties/Settings.Designer.cs delete mode 100644 VG Music Studio.backup/Properties/Settings.settings delete mode 100644 VG Music Studio.backup/Properties/Song.png delete mode 100644 VG Music Studio.backup/Properties/Strings.Designer.cs delete mode 100644 VG Music Studio.backup/Properties/Strings.es.resx delete mode 100644 VG Music Studio.backup/Properties/Strings.it.resx delete mode 100644 VG Music Studio.backup/Properties/Strings.resx delete mode 100644 VG Music Studio.backup/UI/ColorSlider.cs delete mode 100644 VG Music Studio.backup/UI/FlexibleMessageBox.cs delete mode 100644 VG Music Studio.backup/UI/ImageComboBox.cs delete mode 100644 VG Music Studio.backup/UI/MainForm.cs delete mode 100644 VG Music Studio.backup/UI/PianoControl.cs delete mode 100644 VG Music Studio.backup/UI/SongInfoControl.cs delete mode 100644 VG Music Studio.backup/UI/Theme.cs delete mode 100644 VG Music Studio.backup/UI/TrackViewer.cs delete mode 100644 VG Music Studio.backup/UI/ValueTextBox.cs delete mode 100644 VG Music Studio.backup/Util/BetterExceptions.cs delete mode 100644 VG Music Studio.backup/Util/HSLColor.cs delete mode 100644 VG Music Studio.backup/Util/SampleUtils.cs delete mode 100644 VG Music Studio.backup/Util/TimeBarrier.cs delete mode 100644 VG Music Studio.backup/Util/Utils.cs delete mode 100644 VG Music Studio.backup/VG Music Studio.csproj delete mode 100644 VG Music Studio.backup/app.config delete mode 100644 VG Music Studio.backup/midi2agb.exe delete mode 100644 VG Music Studio.backup/upgrade.backup diff --git a/Legacy Repo (.NET Framework)/DLS2/.gitattributes b/Legacy Repo (.NET Framework)/DLS2/.gitattributes deleted file mode 100644 index 1ff0c423..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/.gitattributes +++ /dev/null @@ -1,63 +0,0 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain diff --git a/Legacy Repo (.NET Framework)/DLS2/.gitignore b/Legacy Repo (.NET Framework)/DLS2/.gitignore deleted file mode 100644 index 80921d31..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/.gitignore +++ /dev/null @@ -1,262 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -Build/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -#*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2.sln b/Legacy Repo (.NET Framework)/DLS2/DLS2.sln deleted file mode 100644 index 57d9d798..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30503.244 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DLS2", "DLS2\DLS2.csproj", "{AA962588-E769-4587-8211-AD1D901EAF2C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AA962588-E769-4587-8211-AD1D901EAF2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AA962588-E769-4587-8211-AD1D901EAF2C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA962588-E769-4587-8211-AD1D901EAF2C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AA962588-E769-4587-8211-AD1D901EAF2C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E5515F80-0D53-4618-9ED5-10A59FB4A7A0} - EndGlobalSection -EndGlobal diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Chunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Chunk.cs deleted file mode 100644 index 6b4b775d..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Chunk.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.Collections.Generic; -using System.IO; - -namespace Kermalis.DLS2 -{ - public abstract class DLSChunk - { - /// Length 4 - public string ChunkName { get; } - /// Size in bytes - protected internal uint Size { get; protected set; } - - protected DLSChunk(string name) - { - ChunkName = name; - } - protected DLSChunk(string name, EndianBinaryReader reader) - { - ChunkName = name; - Size = reader.ReadUInt32(); - } - - protected long GetEndOffset(EndianBinaryReader reader) - { - return reader.BaseStream.Position + Size; - } - protected void EatRemainingBytes(EndianBinaryReader reader, long endOffset) - { - if (reader.BaseStream.Position > endOffset) - { - throw new InvalidDataException(); - } - reader.BaseStream.Position = endOffset; - } - - internal abstract void UpdateSize(); - - internal virtual void Write(EndianBinaryWriter writer) - { - UpdateSize(); - writer.Write(ChunkName, 4); - writer.Write(Size); - } - - internal static List GetAllChunks(EndianBinaryReader reader, long endOffset) - { - var chunks = new List(); - while (reader.BaseStream.Position < endOffset) - { - chunks.Add(SwitchNextChunk(reader)); - } - if (reader.BaseStream.Position > endOffset) - { - throw new InvalidDataException(); - } - return chunks; - } - private static DLSChunk SwitchNextChunk(EndianBinaryReader reader) - { - string str = reader.ReadString(4, false); - switch (str) - { - case "art1": return new Level1ArticulatorChunk(reader); - case "art2": return new Level2ArticulatorChunk(reader); - case "colh": return new CollectionHeaderChunk(reader); - case "data": return new DataChunk(reader); - case "dlid": return new DLSIDChunk(reader); - case "fmt ": return new FormatChunk(reader); - case "insh": return new InstrumentHeaderChunk(reader); - case "LIST": return new ListChunk(reader); - case "ptbl": return new PoolTableChunk(reader); - case "rgnh": return new RegionHeaderChunk(reader); - case "wlnk": return new WaveLinkChunk(reader); - case "wsmp": return new WaveSampleChunk(reader); - // InfoSubChunks - case "IARL": - case "IART": - case "ICMS": - case "ICMD": - case "ICOP": - case "ICRD": - case "IENG": - case "IGNR": - case "IKEY": - case "IMED": - case "INAM": - case "IPRD": - case "ISBJ": - case "ISFT": - case "ISRC": - case "ISRF": - case "ITCH": return new InfoSubChunk(str, reader); - default: return new UnsupportedChunk(str, reader); - } - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/CollectionHeaderChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/CollectionHeaderChunk.cs deleted file mode 100644 index 0f5256e1..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/CollectionHeaderChunk.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.DLS2 -{ - // Collection Header Chunk - Page 40 of spec - public sealed class CollectionHeaderChunk : DLSChunk - { - public uint NumInstruments { get; internal set; } - - internal CollectionHeaderChunk() : base("colh") { } - public CollectionHeaderChunk(EndianBinaryReader reader) : base("colh", reader) - { - long endOffset = GetEndOffset(reader); - NumInstruments = reader.ReadUInt32(); - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 4; // NumInstruments - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(NumInstruments); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ConditionalChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ConditionalChunk.cs deleted file mode 100644 index c96c20ed..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ConditionalChunk.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Kermalis.DLS2 -{ - // ALL TODO: - - /*public enum DLSConditional : ushort - { - And = 1, - Or = 2, - Xor = 3, - Add = 4, - Subtract = 5, - Multiply = 6, - Divide = 7, - LogicalAnd = 8, - LogicalOr = 9, - Lt = 10, - Le = 11, - Gt = 12, - Ge = 13, - Eq = 14, - Not = 15, - Const = 16, - Query = 17, - QuerySupported = 18 - } - - // Conditional Chunk - Page 42 of spec - internal sealed class ConditionalChunk : DLSChunk - { - public ConditionalChunk(EndianBinaryReader reader) : base("cdl ", reader) - { - DLSConditional cond = reader.ReadEnum(); - - } - }*/ -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DLSIDChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DLSIDChunk.cs deleted file mode 100644 index b9b8aab5..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DLSIDChunk.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; - -namespace Kermalis.DLS2 -{ - // DLSID Chunk - Page 40 of spec - public sealed class DLSIDChunk : DLSChunk - { - private DLSID _dlsid; - public DLSID DLSID - { - get => _dlsid; - set - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - _dlsid = value; - } - } - - public DLSIDChunk(DLSID id) : base("dlid") - { - DLSID = id; - } - public DLSIDChunk(EndianBinaryReader reader) : base("dlid", reader) - { - long endOffset = GetEndOffset(reader); - DLSID = new DLSID(reader); - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 16; // DLSID - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - DLSID.Write(writer); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DataChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DataChunk.cs deleted file mode 100644 index f924db3e..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/DataChunk.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.DLS2 -{ - public sealed class DataChunk : RawDataChunk - { - public DataChunk(byte[] data) : base("data", data) { } - internal DataChunk(EndianBinaryReader reader) : base("data", reader) { } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/FormatChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/FormatChunk.cs deleted file mode 100644 index 7a7779e1..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/FormatChunk.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.IO; - -namespace Kermalis.DLS2 -{ - public abstract class FormatInfo - { - public ushort BitsPerSample { get; set; } - - internal abstract void Write(EndianBinaryWriter writer); - } - public sealed class PCMInfo : FormatInfo - { - internal PCMInfo() { } - internal PCMInfo(EndianBinaryReader reader) - { - BitsPerSample = reader.ReadUInt16(); - } - - internal override void Write(EndianBinaryWriter writer) - { - writer.Write(BitsPerSample); - } - } - // Untested! - public sealed class ExtensibleInfo : FormatInfo - { - public ushort ExtraInfo { get; set; } - public uint ChannelMask { get; set; } - public DLSID SubFormat { get; set; } - - internal ExtensibleInfo() - { - SubFormat = new DLSID(); - } - internal ExtensibleInfo(EndianBinaryReader reader) - { - BitsPerSample = reader.ReadUInt16(); - ushort byteSize = reader.ReadUInt16(); - if (byteSize != 22) - { - throw new InvalidDataException(); - } - ExtraInfo = reader.ReadUInt16(); - ChannelMask = reader.ReadUInt32(); - SubFormat = new DLSID(reader); - } - - internal override void Write(EndianBinaryWriter writer) - { - writer.Write(BitsPerSample); - writer.Write(22u); - writer.Write(ExtraInfo); - writer.Write(ChannelMask); - SubFormat.Write(writer); - } - } - - // Format Chunk - Page 57 of spec - public sealed class FormatChunk : DLSChunk - { - public WaveInfo WaveInfo { get; } - public FormatInfo FormatInfo { get; } - - public FormatChunk(WaveFormat format) : base("fmt ") - { - WaveInfo = new WaveInfo() { FormatTag = format }; - if (format == WaveFormat.Extensible) - { - FormatInfo = new ExtensibleInfo(); - } - else - { - FormatInfo = new PCMInfo(); - } - } - internal FormatChunk(EndianBinaryReader reader) : base("fmt ", reader) - { - long endOffset = GetEndOffset(reader); - WaveInfo = new WaveInfo(reader); - if (WaveInfo.FormatTag == WaveFormat.Extensible) - { - FormatInfo = new ExtensibleInfo(reader); - } - else - { - FormatInfo = new PCMInfo(reader); - } - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 14 // WaveFormat - + (WaveInfo.FormatTag == DLS2.WaveFormat.Extensible ? 26u : 2u); // FormatInfo - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - WaveInfo.Write(writer); - FormatInfo.Write(writer); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InfoSubChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InfoSubChunk.cs deleted file mode 100644 index 71a629a5..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InfoSubChunk.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Linq; - -namespace Kermalis.DLS2 -{ - public sealed class InfoSubChunk : DLSChunk - { - private string _text; - public string Text - { - get => _text; - set - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - if (value.Any(c => c > sbyte.MaxValue)) - { - throw new ArgumentException("Text must be ASCII"); - } - _text = value; - } - } - - public InfoSubChunk(string name, string text) : base(name) - { - Text = text; - } - internal InfoSubChunk(string name, EndianBinaryReader reader) : base(name, reader) - { - long endOffset = GetEndOffset(reader); - _text = reader.ReadStringNullTerminated(); - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = (uint)_text.Length + 1; // +1 for \0 - if (Size % 2 != 0) // Align by 2 bytes - { - Size++; - } - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(_text, (int)Size); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs deleted file mode 100644 index 077070c7..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/InstrumentHeaderChunk.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; - -namespace Kermalis.DLS2 -{ - // Instrument Header Chunk - Page 45 of spec - public sealed class InstrumentHeaderChunk : DLSChunk - { - public uint NumRegions { get; set; } - private MIDILocale _locale; - public MIDILocale Locale - { - get => _locale; - set - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - _locale = value; - } - } - - public InstrumentHeaderChunk() : base("insh") { } - internal InstrumentHeaderChunk(EndianBinaryReader reader) : base("insh", reader) - { - long endOffset = GetEndOffset(reader); - NumRegions = reader.ReadUInt32(); - _locale = new MIDILocale(reader); - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 4 // NumRegions - + 8; // Locale - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(NumRegions); - _locale.Write(writer); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level1ArticulatorChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level1ArticulatorChunk.cs deleted file mode 100644 index c6b4f260..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level1ArticulatorChunk.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; - -namespace Kermalis.DLS2 -{ - // Level 1 Articulator Chunk - Page 46 of spec - public sealed class Level1ArticulatorChunk : DLSChunk, IList, IReadOnlyList - { - private readonly List _connectionBlocks; - - public Level1ArticulatorConnectionBlock this[int index] - { - get => _connectionBlocks[index]; - set - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - _connectionBlocks[index] = value; - } - } - public int Count => _connectionBlocks.Count; - public bool IsReadOnly => false; - - public Level1ArticulatorChunk() : base("art1") - { - _connectionBlocks = new List(); - } - internal Level1ArticulatorChunk(EndianBinaryReader reader) : base("art1", reader) - { - long endOffset = GetEndOffset(reader); - uint byteSize = reader.ReadUInt32(); - if (byteSize != 8) - { - throw new InvalidDataException(); - } - uint numConnectionBlocks = reader.ReadUInt32(); - _connectionBlocks = new List((int)numConnectionBlocks); - for (uint i = 0; i < numConnectionBlocks; i++) - { - _connectionBlocks.Add(new Level1ArticulatorConnectionBlock(reader)); - } - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 4 // byteSize - + 4 // _numConnectionBlocks - + (uint)(12 * _connectionBlocks.Count); // _connectionBlocks - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(8u); - writer.Write((uint)_connectionBlocks.Count); - for (int i = 0; i < _connectionBlocks.Count; i++) - { - _connectionBlocks[i].Write(writer); - } - } - - public IEnumerator GetEnumerator() - { - return _connectionBlocks.GetEnumerator(); - } - IEnumerator IEnumerable.GetEnumerator() - { - return _connectionBlocks.GetEnumerator(); - } - - public void Add(Level1ArticulatorConnectionBlock item) - { - if (item is null) - { - throw new ArgumentNullException(nameof(item)); - } - _connectionBlocks.Add(item); - } - public void Clear() - { - _connectionBlocks.Clear(); - } - public void CopyTo(Level1ArticulatorConnectionBlock[] array, int arrayIndex) - { - _connectionBlocks.CopyTo(array, arrayIndex); - } - public bool Contains(Level1ArticulatorConnectionBlock item) - { - return _connectionBlocks.Contains(item); - } - public int IndexOf(Level1ArticulatorConnectionBlock item) - { - return _connectionBlocks.IndexOf(item); - } - public void Insert(int index, Level1ArticulatorConnectionBlock item) - { - if (item is null) - { - throw new ArgumentNullException(nameof(item)); - } - _connectionBlocks.Insert(index, item); - } - public bool Remove(Level1ArticulatorConnectionBlock item) - { - return _connectionBlocks.Remove(item); - } - public void RemoveAt(int index) - { - _connectionBlocks.RemoveAt(index); - } - - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level2ArticulatorChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level2ArticulatorChunk.cs deleted file mode 100644 index 71e8b33d..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/Level2ArticulatorChunk.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; - -namespace Kermalis.DLS2 -{ - // Level 2 Articulator Chunk - Page 49 of spec - public sealed class Level2ArticulatorChunk : DLSChunk, IList, IReadOnlyList - { - private readonly List _connectionBlocks; - - public Level2ArticulatorConnectionBlock this[int index] - { - get => _connectionBlocks[index]; - set - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - _connectionBlocks[index] = value; - } - } - public int Count => _connectionBlocks.Count; - public bool IsReadOnly => false; - - public Level2ArticulatorChunk() : base("art2") - { - _connectionBlocks = new List(); - } - internal Level2ArticulatorChunk(EndianBinaryReader reader) : base("art2", reader) - { - long endOffset = GetEndOffset(reader); - uint byteSize = reader.ReadUInt32(); - if (byteSize != 8) - { - throw new InvalidDataException(); - } - uint numConnectionBlocks = reader.ReadUInt32(); - _connectionBlocks = new List((int)numConnectionBlocks); - for (uint i = 0; i < numConnectionBlocks; i++) - { - _connectionBlocks.Add(new Level2ArticulatorConnectionBlock(reader)); - } - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 4 // byteSize - + 4 // _numConnectionBlocks - + (uint)(12 * _connectionBlocks.Count); // _connectionBlocks - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(8u); - writer.Write((uint)_connectionBlocks.Count); - for (int i = 0; i < _connectionBlocks.Count; i++) - { - _connectionBlocks[i].Write(writer); - } - } - - public IEnumerator GetEnumerator() - { - return _connectionBlocks.GetEnumerator(); - } - IEnumerator IEnumerable.GetEnumerator() - { - return _connectionBlocks.GetEnumerator(); - } - - public void Add(Level2ArticulatorConnectionBlock item) - { - if (item is null) - { - throw new ArgumentNullException(nameof(item)); - } - _connectionBlocks.Add(item); - } - public void Clear() - { - _connectionBlocks.Clear(); - } - public void CopyTo(Level2ArticulatorConnectionBlock[] array, int arrayIndex) - { - _connectionBlocks.CopyTo(array, arrayIndex); - } - public bool Contains(Level2ArticulatorConnectionBlock item) - { - return _connectionBlocks.Contains(item); - } - public int IndexOf(Level2ArticulatorConnectionBlock item) - { - return _connectionBlocks.IndexOf(item); - } - public void Insert(int index, Level2ArticulatorConnectionBlock item) - { - if (item is null) - { - throw new ArgumentNullException(nameof(item)); - } - _connectionBlocks.Insert(index, item); - } - public bool Remove(Level2ArticulatorConnectionBlock item) - { - return _connectionBlocks.Remove(item); - } - public void RemoveAt(int index) - { - _connectionBlocks.RemoveAt(index); - } - - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ListChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ListChunk.cs deleted file mode 100644 index 1c4107ee..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/ListChunk.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Kermalis.DLS2 -{ - // LIST Chunk - Page 40 of spec - public sealed class ListChunk : DLSChunk, IList, IReadOnlyList - { - /// Length 4 - public string Identifier { get; set; } - private readonly List _children; - - public int Count => _children.Count; - public bool IsReadOnly => false; - public DLSChunk this[int index] - { - get => _children[index]; - set - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - _children[index] = value; - } - } - - public ListChunk(string identifier) : base("LIST") - { - Identifier = identifier; - _children = new List(); - } - internal ListChunk(EndianBinaryReader reader) : base("LIST", reader) - { - long endOffset = GetEndOffset(reader); - Identifier = reader.ReadString(4, false); - _children = GetAllChunks(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 4; // Identifier - foreach (DLSChunk c in _children) - { - c.UpdateSize(); - Size += c.Size + 8; - } - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(Identifier, 4); - foreach (DLSChunk c in _children) - { - c.Write(writer); - } - } - - public void Add(DLSChunk chunk) - { - if (chunk is null) - { - throw new ArgumentNullException(nameof(chunk)); - } - _children.Add(chunk); - } - public void Clear() - { - _children.Clear(); - } - public bool Contains(DLSChunk chunk) - { - return _children.Contains(chunk); - } - public void CopyTo(DLSChunk[] array, int arrayIndex) - { - _children.CopyTo(array, arrayIndex); - } - public int IndexOf(DLSChunk chunk) - { - return _children.IndexOf(chunk); - } - public void Insert(int index, DLSChunk chunk) - { - if (chunk is null) - { - throw new ArgumentNullException(nameof(chunk)); - } - _children.Insert(index, chunk); - } - public bool Remove(DLSChunk chunk) - { - return _children.Remove(chunk); - } - public void RemoveAt(int index) - { - _children.RemoveAt(index); - } - - public IEnumerator GetEnumerator() - { - return _children.GetEnumerator(); - } - IEnumerator IEnumerable.GetEnumerator() - { - return _children.GetEnumerator(); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/PoolTableChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/PoolTableChunk.cs deleted file mode 100644 index 07d698cf..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/PoolTableChunk.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.Collections; -using System.Collections.Generic; -using System.IO; - -namespace Kermalis.DLS2 -{ - // Pool Table Chunk - Page 54 of spec - public sealed class PoolTableChunk : DLSChunk, IReadOnlyList - { - private uint _numCues; - private List _poolCues; - - public uint this[int index] => _poolCues[index]; - public int Count => (int)_numCues; - - internal PoolTableChunk() : base("ptbl") - { - _poolCues = new List(); - } - internal PoolTableChunk(EndianBinaryReader reader) : base("ptbl", reader) - { - long endOffset = GetEndOffset(reader); - uint byteSize = reader.ReadUInt32(); - if (byteSize != 8) - { - throw new InvalidDataException(); - } - _numCues = reader.ReadUInt32(); - _poolCues = new List((int)_numCues); - for (uint i = 0; i < _numCues; i++) - { - _poolCues.Add(reader.ReadUInt32()); - } - EatRemainingBytes(reader, endOffset); - } - - internal void UpdateCues(List newCues) - { - _numCues = (uint)newCues.Count; - _poolCues = newCues; - } - - internal override void UpdateSize() - { - Size = 4 // byteSize - + 4 // _numCues - + (4 * _numCues); // _poolCues - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(8u); - writer.Write(_numCues); - for (int i = 0; i < _numCues; i++) - { - writer.Write(_poolCues[i]); - } - } - - public IEnumerator GetEnumerator() - { - return _poolCues.GetEnumerator(); - } - IEnumerator IEnumerable.GetEnumerator() - { - return _poolCues.GetEnumerator(); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RawDataChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RawDataChunk.cs deleted file mode 100644 index 117d24fe..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RawDataChunk.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; - -namespace Kermalis.DLS2 -{ - public abstract class RawDataChunk : DLSChunk - { - private byte[] _data; - public byte[] Data - { - get => _data; - set - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - _data = value; - } - } - - protected RawDataChunk(string name, byte[] data) : base(name) - { - Data = data; - } - protected RawDataChunk(string name, EndianBinaryReader reader) : base(name, reader) - { - _data = reader.ReadBytes((int)Size); - } - - internal override void UpdateSize() - { - Size = (uint)_data.Length; - if (Size % 2 != 0) // Align by 2 bytes - { - Size++; - } - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(_data); - for (int i = _data.Length; i < Size; i++) - { - writer.Write((byte)0); - } - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RegionHeaderChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RegionHeaderChunk.cs deleted file mode 100644 index aaabd0b9..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/RegionHeaderChunk.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.DLS2 -{ - // Region Header Chunk - Page 45 of spec - public sealed class RegionHeaderChunk : DLSChunk - { - public Range KeyRange { get; set; } - public Range VelocityRange { get; set; } - public ushort Options { get; set; } - public ushort KeyGroup { get; set; } - public ushort Layer { get; set; } - - public RegionHeaderChunk() : base("rgnh") - { - KeyRange = new Range(0, 127); - VelocityRange = new Range(0, 127); - } - internal RegionHeaderChunk(EndianBinaryReader reader) : base("rgnh", reader) - { - long endOffset = GetEndOffset(reader); - KeyRange = new Range(reader); - VelocityRange = new Range(reader); - Options = reader.ReadUInt16(); - KeyGroup = reader.ReadUInt16(); - if (Size >= 14) // Size of 12 is also valid - { - Layer = reader.ReadUInt16(); - } - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 4 // KeyRange - + 4 // VelocityRange - + 2 // Options - + 2 // KeyGroup - + 2; // Layer - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - KeyRange.Write(writer); - VelocityRange.Write(writer); - writer.Write(Options); - writer.Write(KeyGroup); - writer.Write(Layer); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/UnsupportedChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/UnsupportedChunk.cs deleted file mode 100644 index acdccb1c..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/UnsupportedChunk.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.DLS2 -{ - public sealed class UnsupportedChunk : RawDataChunk - { - public UnsupportedChunk(string name, byte[] data) : base(name, data) { } - internal UnsupportedChunk(string name, EndianBinaryReader reader) : base(name, reader) { } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/VersionChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/VersionChunk.cs deleted file mode 100644 index 42302bf5..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/VersionChunk.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Kermalis.DLS2 -{ - // TODO: - - /*public sealed class VersionChunk : DLSChunk - { - - - internal VersionChunk(EndianBinaryReader reader) : base("vers", reader) - { - - } - }*/ -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveLinkChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveLinkChunk.cs deleted file mode 100644 index 37cb660f..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveLinkChunk.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.DLS2 -{ - public sealed class WaveLinkChunk : DLSChunk - { - public WaveLinkOptions Options { get; set; } - public ushort PhaseGroup { get; set; } - public WaveLinkChannels Channels { get; set; } - public uint TableIndex { get; set; } - - public WaveLinkChunk() : base("wlnk") - { - Channels = WaveLinkChannels.Left; - } - internal WaveLinkChunk(EndianBinaryReader reader) : base("wlnk", reader) - { - long endOffset = GetEndOffset(reader); - Options = reader.ReadEnum(); - PhaseGroup = reader.ReadUInt16(); - Channels = reader.ReadEnum(); - TableIndex = reader.ReadUInt32(); - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 2 // Options - + 2 // PhaseGroup - + 4 // Channel - + 4; // TableIndex - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(Options); - writer.Write(PhaseGroup); - writer.Write(Channels); - writer.Write(TableIndex); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveSampleChunk.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveSampleChunk.cs deleted file mode 100644 index d005392a..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Chunks/WaveSampleChunk.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.IO; - -namespace Kermalis.DLS2 -{ - public sealed class WaveSampleChunk : DLSChunk - { - public ushort UnityNote { get; set; } - public short FineTune { get; set; } - public int Gain { get; set; } - public WaveSampleOptions Options { get; set; } - - public WaveSampleLoop Loop { get; set; } // Combining "SampleLoops" and the loop list - - public WaveSampleChunk() : base("wsmp") - { - UnityNote = 60; - Loop = null; - } - internal WaveSampleChunk(EndianBinaryReader reader) : base("wsmp", reader) - { - long endOffset = GetEndOffset(reader); - uint byteSize = reader.ReadUInt32(); - if (byteSize != 20) - { - throw new InvalidDataException(); - } - UnityNote = reader.ReadUInt16(); - FineTune = reader.ReadInt16(); - Gain = reader.ReadInt32(); - Options = reader.ReadEnum(); - if (reader.ReadUInt32() == 1) - { - Loop = new WaveSampleLoop(reader); - } - EatRemainingBytes(reader, endOffset); - } - - internal override void UpdateSize() - { - Size = 4 // byteSize - + 2 // UnityNote - + 2 // FineTune - + 4 // Gain - + 4 // Options - + 4 // DoesLoop - + (Loop is null ? 0u : 16u); // Loop - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(20u); - writer.Write(UnityNote); - writer.Write(FineTune); - writer.Write(Gain); - writer.Write(Options); - if (Loop is null) - { - writer.Write(0u); - } - else - { - writer.Write(1u); - Loop.Write(writer); - } - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs deleted file mode 100644 index 6a763662..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS.cs +++ /dev/null @@ -1,235 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Kermalis.DLS2 -{ - public sealed class DLS : IList, IReadOnlyList - { - private readonly List _chunks; - - public int Count => _chunks.Count; - public bool IsReadOnly => false; - public DLSChunk this[int index] - { - get => _chunks[index]; - set - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - _chunks[index] = value; - } - } - - public CollectionHeaderChunk CollectionHeader => GetChunk(); - public ListChunk InstrumentList => GetListChunk("lins"); - public PoolTableChunk PoolTable => GetChunk(); - public ListChunk WavePool => GetListChunk("wvpl"); - - private T GetChunk() where T : DLSChunk - { - return (T)_chunks.Find(c => c is T); - } - private ListChunk GetListChunk(string str) - { - return (ListChunk)_chunks.Find(c => c is ListChunk lc && lc.Identifier == str); - } - -#if DEBUG - public static void Main() - { - //new DLS(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games\M\test.dls"); - //new DLS(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games\M\test2.dls"); - //new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\Arachno SoundFont - Version 1.0.dls"); - //new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\Musyng Kite.dls"); - new DLS(@"C:\Users\Kermalis\Music\Samples, Presets, Soundfonts, VSTs, etc\Soundfonts\RSE Corrected Soundfont Revision 17.dls"); - } -#endif - - /// For creating. - public DLS() - { - _chunks = new List() - { - new CollectionHeaderChunk(), - new ListChunk("lins"), - new PoolTableChunk(), - new ListChunk("wvpl"), - }; - } - public DLS(string path) - { - using (var reader = new EndianBinaryReader(File.Open(path, FileMode.Open))) - { - _chunks = Init(reader); - } - } - public DLS(Stream stream) - { - _chunks = Init(new EndianBinaryReader(stream)); - } - private List Init(EndianBinaryReader reader) - { - string str = reader.ReadString(4, false); - if (str != "RIFF") - { - throw new InvalidDataException("RIFF header was not found at the start of the file."); - } - uint size = reader.ReadUInt32(); - long endOffset = reader.BaseStream.Position + size; - str = reader.ReadString(4, false); - if (str != "DLS ") - { - throw new InvalidDataException("DLS header was not found at the expected offset."); - } - return DLSChunk.GetAllChunks(reader, endOffset); - } - - public void UpdateCollectionHeader() - { - CollectionHeader.NumInstruments = (uint)InstrumentList.Count; - } - /// Updates the pointers in the . Should be called after modifying . - public void UpdatePoolTable() - { - ListChunk wvpl = WavePool; - var newCues = new List(wvpl.Count); - uint cur = 0; - for (int i = 0; i < wvpl.Count; i++) - { - newCues.Add(cur); - DLSChunk c = wvpl[i]; - c.UpdateSize(); - cur += c.Size + 8; - } - PoolTable.UpdateCues(newCues); - } - public void Save(string path) - { - UpdateCollectionHeader(); - UpdatePoolTable(); - - using (var writer = new EndianBinaryWriter(File.Open(path, FileMode.Create))) - { - writer.Write("RIFF", 4); - writer.Write(UpdateSize()); - writer.Write("DLS ", 4); - foreach (DLSChunk c in _chunks) - { - c.Write(writer); - } - } - } - - public string GetHierarchy() - { - var str = new StringBuilder(); - int tabLevel = 0; - void ApplyTabLevel() - { - for (int t = 0; t < tabLevel; t++) - { - str.Append('\t'); - } - } - void Recursion(IReadOnlyList parent, string listName) - { - ApplyTabLevel(); - str.Append($"{listName} ({parent.Count})"); - tabLevel++; - foreach (DLSChunk c in parent) - { - str.AppendLine(); - if (c is ListChunk lc) - { - Recursion(lc, $"{lc.ChunkName} '{lc.Identifier}'"); - } - else - { - ApplyTabLevel(); - str.Append($"<{c.ChunkName}>"); - if (c is InfoSubChunk ic) - { - str.Append($" [\"{ic.Text}\"]"); - } - else if (c is RawDataChunk dc) - { - str.Append($" [{dc.Data.Length} bytes]"); - } - } - } -#pragma warning disable IDE0059 // Unnecessary assignment of a value - tabLevel--; -#pragma warning restore IDE0059 // Unnecessary assignment of a value - } - Recursion(this, "RIFF 'DLS '"); - return str.ToString(); - } - - private uint UpdateSize() - { - uint size = 4; - foreach (DLSChunk c in _chunks) - { - c.UpdateSize(); - size += c.Size + 8; - } - return size; - } - - public void Add(DLSChunk chunk) - { - if (chunk is null) - { - throw new ArgumentNullException(nameof(chunk)); - } - _chunks.Add(chunk); - } - public void Clear() - { - _chunks.Clear(); - } - public bool Contains(DLSChunk chunk) - { - return _chunks.Contains(chunk); - } - public void CopyTo(DLSChunk[] array, int arrayIndex) - { - _chunks.CopyTo(array, arrayIndex); - } - public int IndexOf(DLSChunk chunk) - { - return _chunks.IndexOf(chunk); - } - public void Insert(int index, DLSChunk chunk) - { - if (chunk is null) - { - throw new ArgumentNullException(nameof(chunk)); - } - _chunks.Insert(index, chunk); - } - public bool Remove(DLSChunk chunk) - { - return _chunks.Remove(chunk); - } - public void RemoveAt(int index) - { - _chunks.RemoveAt(index); - } - - public IEnumerator GetEnumerator() - { - return _chunks.GetEnumerator(); - } - IEnumerator IEnumerable.GetEnumerator() - { - return _chunks.GetEnumerator(); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS2.csproj b/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS2.csproj deleted file mode 100644 index 3f9657eb..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/DLS2.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - Kermalis - - DLS2 - DLS2 - DLS2 - Kermalis.DLS2 - 1.0.0.0 - ..\Build - - - - netcoreapp3.1 - Exe - - - - netstandard2.0 - Auto - none - false - - - - - - - diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level1ArticulatorEnums.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level1ArticulatorEnums.cs deleted file mode 100644 index 0cbe8222..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level1ArticulatorEnums.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Kermalis.DLS2 -{ - public enum Level1ArticulatorSource : ushort - { - None = 0x0, - LFO = 0x1, - KeyOnVelocity = 0x2, - KeyNumber = 0x3, - EG1 = 0x4, - EG2 = 0x5, - PitchWheel = 0x6, - Modulation_CC1 = 0x81, - ChannelVolume_CC7 = 0x87, - Pan_CC10 = 0x8A, - Expression_CC11 = 0x8B, - PitchBendRange_RPN0 = 0x100, - FineTune_RPN1 = 0x101, - CoarseTune_RPN2 = 0x102 - } - - public enum Level1ArticulatorDestination : ushort - { - None = 0x0, - Gain = 0x1, - Pitch = 0x3, - Pan = 0x4, - LFOFrequency = 0x104, - LFOStartDelay = 0x105, - EG1AttackTime = 0x206, - EG1DecayTime = 0x207, - EG1ReleaseTime = 0x209, - EG1SustainLevel = 0x20A, - EG2AttackTime = 0x30A, - EG2DecayTime = 0x30B, - EG2ReleaseTime = 0x30D, - EG2SustainLevel = 0x30E - } - - public enum Level1ArticulatorTransform : byte - { - None = 0x0, - Concave = 0x1 - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level2ArticulatorEnums.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level2ArticulatorEnums.cs deleted file mode 100644 index 4437952e..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/Level2ArticulatorEnums.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Kermalis.DLS2 -{ - public enum Level2ArticulatorSource : ushort - { - None = 0x0, - LFO = 0x1, - KeyOnVelocity = 0x2, - KeyNumber = 0x3, - EG1 = 0x4, - EG2 = 0x5, - PitchWheel = 0x6, - PolyPressure = 0x7, - ChannelPressure = 0x8, - Vibrato = 0x9, - Modulation_CC1 = 0x81, - ChannelVolume_CC7 = 0x87, - Pan_CC10 = 0x8A, - Expression_CC11 = 0x8B, - ChorusSend_CC91 = 0xDB, - Reverb_SendCC93 = 0xDD, - PitchBendRange_RPN0 = 0x100, - FineTune_RPN1 = 0x101, - CoarseTune_RPN2 = 0x102 - } - - public enum Level2ArticulatorDestination : ushort - { - None = 0x0, - Gain = 0x1, - Pitch = 0x3, - Pan = 0x4, - KeyNumber = 0x5, - Left = 0x10, - Right = 0x11, - Center = 0x12, - LFEChannel = 0x13, - LeftRear = 0x14, - RightRear = 0x15, - Chorus = 0x80, - Reverb = 0x81, - LFOFrequency = 0x104, - LFOStartDelay = 0x105, - VIBFrequency = 0x114, - VIBStartDelay = 0x115, - EG1AttackTime = 0x206, - EG1DecayTime = 0x207, - EG1ReleaseTime = 0x209, - EG1SustainLevel = 0x20A, - EG1DelayTime = 0x20B, - EG1HoldTime = 0x20C, - EG1ShutdownTime = 0x20D, - EG2AttackTime = 0x30A, - EG2DecayTime = 0x30B, - EG2ReleaseTime = 0x30D, - EG2SustainLevel = 0x30E, - EG2DelayTime = 0x30F, - EG2HoldTime = 0x310, - FilterCutoff = 0x500, - FilterResonance = 0x501 - } - - public enum Level2ArticulatorTransform : byte - { - None = 0x0, - Concave = 0x1, - Convex = 0x2, - Switch = 0x3 - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveFormat.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveFormat.cs deleted file mode 100644 index 4cab7976..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveFormat.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Kermalis.DLS2 -{ - public enum WaveFormat : ushort - { - Unknown = 0, - PCM = 1, - MSADPCM = 2, - Float = 3, - ALaw = 6, - MuLaw = 7, - DVIADPCM = 17, - IMAADPCM = 17, - Extensible = 0xFFFE - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkChannels.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkChannels.cs deleted file mode 100644 index 678bd25b..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkChannels.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace Kermalis.DLS2 -{ - [Flags] - public enum WaveLinkChannels : uint - { - None = 0, - Left = 1 << 0, - Right = 1 << 1, - Center = 1 << 2, - LowFrequencyEnergy = 1 << 3, - SurroundLeft = 1 << 4, - SurroundRight = 1 << 5, - LeftOfCenter = 1 << 6, - RightOfCenter = 1 << 7, - SurroundCenter = 1 << 8, - SideLeft = 1 << 9, - SideRight = 1 << 10, - Top = 1 << 11, - TopFrontLeft = 1 << 12, - TopFrontCenter = 1 << 13, - TopFrontRight = 1 << 14, - TopRearLeft = 1 << 15, - TopRearCenter = 1 << 16, - TopRearRight = 1 << 17 - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkOptions.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkOptions.cs deleted file mode 100644 index 7f47d8c1..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveLinkOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Kermalis.DLS2 -{ - [Flags] - public enum WaveLinkOptions : ushort - { - None = 0, - PhaseMaster = 1 << 0, - MultiChannel = 1 << 1 - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleLoop.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleLoop.cs deleted file mode 100644 index 68b63132..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleLoop.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Kermalis.DLS2 -{ - public enum LoopType : uint - { - Forward = 0, - Release = 1 - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleOptions.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleOptions.cs deleted file mode 100644 index ef87bab4..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Enums/WaveSampleOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Kermalis.DLS2 -{ - [Flags] - public enum WaveSampleOptions : uint - { - None = 0, - NoTruncation = 1 << 0, - NoCompression = 1 << 1 - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/ConnectionBlock.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/ConnectionBlock.cs deleted file mode 100644 index d8a9f75d..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/ConnectionBlock.cs +++ /dev/null @@ -1,163 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; - -namespace Kermalis.DLS2 -{ - public sealed class Level1ArticulatorConnectionBlock - { - public Level1ArticulatorSource Source { get; set; } - public Level1ArticulatorSource Control { get; set; } - public Level1ArticulatorDestination Destination { get; set; } - public Level1ArticulatorTransform Transform { get; set; } - public int Scale { get; set; } - - public Level1ArticulatorConnectionBlock() { } - internal Level1ArticulatorConnectionBlock(EndianBinaryReader reader) - { - Source = reader.ReadEnum(); - Control = reader.ReadEnum(); - Destination = reader.ReadEnum(); - Transform = reader.ReadEnum(); - Scale = reader.ReadInt32(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(Source); - writer.Write(Control); - writer.Write(Destination); - writer.Write(Transform); - writer.Write(Scale); - } - } - - public sealed class Level2ArticulatorConnectionBlock - { - public Level2ArticulatorSource Source { get; set; } - public Level2ArticulatorSource Control { get; set; } - public Level2ArticulatorDestination Destination { get; set; } - public ushort Transform_Raw { get; set; } - public int Scale { get; set; } - - public bool InvertSource - { - get => (Transform_Raw >> 15) != 0; - set - { - if (value) - { - Transform_Raw |= 1 << 15; - } - else - { - Transform_Raw &= unchecked((ushort)~(1 << 15)); - } - } - } - public bool BipolarSource - { - get => ((Transform_Raw >> 14) & 1) != 0; - set - { - if (value) - { - Transform_Raw |= 1 << 14; - } - else - { - Transform_Raw &= unchecked((ushort)~(1 << 14)); - } - } - } - public Level2ArticulatorTransform TransformSource - { - get => (Level2ArticulatorTransform)((Transform_Raw >> 10) & 0xF); - set - { - if (value > (Level2ArticulatorTransform)0xF) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - Transform_Raw &= unchecked((ushort)~(0xF << 10)); - Transform_Raw |= (ushort)((ushort)value << 10); - } - } - - public bool InvertDestination - { - get => ((Transform_Raw >> 9) & 1) != 0; - set - { - if (value) - { - Transform_Raw |= 1 << 9; - } - else - { - Transform_Raw &= unchecked((ushort)~(1 << 9)); - } - } - } - public bool BipolarDestination - { - get => ((Transform_Raw >> 8) & 1) != 0; - set - { - if (value) - { - Transform_Raw |= 1 << 8; - } - else - { - Transform_Raw &= unchecked((ushort)~(1 << 8)); - } - } - } - public Level2ArticulatorTransform TransformDestination - { - get => (Level2ArticulatorTransform)((Transform_Raw >> 4) & 0xF); - set - { - if (value > (Level2ArticulatorTransform)0xF) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - Transform_Raw &= unchecked((ushort)~(0xF << 4)); - Transform_Raw |= (ushort)((ushort)value << 4); - } - } - - public Level2ArticulatorTransform TransformOutput - { - get => (Level2ArticulatorTransform)(Transform_Raw & 0xF); - set - { - if (value > (Level2ArticulatorTransform)0xF) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - Transform_Raw &= unchecked((ushort)~0xF); - Transform_Raw |= (ushort)value; - } - } - - public Level2ArticulatorConnectionBlock() { } - internal Level2ArticulatorConnectionBlock(EndianBinaryReader reader) - { - Source = reader.ReadEnum(); - Control = reader.ReadEnum(); - Destination = reader.ReadEnum(); - Transform_Raw = reader.ReadUInt16(); - Scale = reader.ReadInt32(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(Source); - writer.Write(Control); - writer.Write(Destination); - writer.Write(Transform_Raw); - writer.Write(Scale); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/DLSID.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/DLSID.cs deleted file mode 100644 index d955ef70..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/DLSID.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -#if !DEBUG -using System.Collections.Generic; -#endif -using System.Linq; - -namespace Kermalis.DLS2 -{ - public sealed class DLSID - { - public uint Data1 { get; set; } - public ushort Data2 { get; set; } - public ushort Data3 { get; set; } - public byte[] Data4 { get; } - - public static DLSID Query_GMInHardware { get; } = new DLSID(0x178F2F24, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); - public static DLSID Query_GSInHardware { get; } = new DLSID(0x178F2F25, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); - public static DLSID Query_XGInHardware { get; } = new DLSID(0x178F2F26, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); - public static DLSID Query_SupportsDLS1 { get; } = new DLSID(0x178F2F27, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); - public static DLSID Query_SampleMemorySize { get; } = new DLSID(0x178F2F28, 0xC364, 0x11D1, new byte[] { 0xA7, 0x60, 0x00, 0x00, 0xF8, 0x75, 0xAC, 0x12 }); - public static DLSID Query_SamplePlaybackRate { get; } = new DLSID(0x2A91F713, 0xA4BF, 0x11D2, new byte[] { 0xBB, 0xDF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 }); - public static DLSID Query_ManufacturersID { get; } = new DLSID(0xB03E1181, 0x8095, 0x11D2, new byte[] { 0xA1, 0xEF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 }); - public static DLSID Query_ProductID { get; } = new DLSID(0xB03E1182, 0x8095, 0x11D2, new byte[] { 0xA1, 0xEF, 0x00, 0x60, 0x08, 0x33, 0xDB, 0xD8 }); - public static DLSID Query_SupportsDLS2 { get; } = new DLSID(0xF14599E5, 0x4689, 0x11D2, new byte[] { 0xAF, 0xA6, 0x00, 0xAA, 0x00, 0x24, 0xD8, 0xB6 }); - - public DLSID() - { - Data4 = new byte[8]; - } - internal DLSID(EndianBinaryReader reader) - { - Data1 = reader.ReadUInt32(); - Data2 = reader.ReadUInt16(); - Data3 = reader.ReadUInt16(); - Data4 = reader.ReadBytes(8); - } - public DLSID(uint data1, ushort data2, ushort data3, byte[] data4) - { - if (data4 is null) - { - throw new ArgumentNullException(nameof(data4)); - } - if (data4.Length != 8) - { - throw new ArgumentOutOfRangeException(nameof(data4.Length)); - } - Data1 = data1; - Data2 = data2; - Data3 = data3; - Data4 = data4; - } - public DLSID(byte[] data) - { - if (data is null) - { - throw new ArgumentNullException(nameof(data)); - } - if (data.Length != 16) - { - throw new ArgumentOutOfRangeException(nameof(data.Length)); - } - Data1 = (uint)EndianBitConverter.BytesToInt32(data, 0, Endianness.LittleEndian); - Data2 = (ushort)EndianBitConverter.BytesToInt16(data, 4, Endianness.LittleEndian); - Data3 = (ushort)EndianBitConverter.BytesToInt16(data, 6, Endianness.LittleEndian); - Data4 = new byte[8]; - for (int i = 0; i < 8; i++) - { - Data4[i] = data[8 + i]; - } - } - - public void Write(EndianBinaryWriter writer) - { - writer.Write(Data1); - writer.Write(Data2); - writer.Write(Data3); - writer.Write(Data4); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(obj, this)) - { - return true; - } - if (obj is DLSID id) - { - return id.Data1 == Data1 && id.Data2 == Data2 && id.Data3 == Data3 && id.Data4.SequenceEqual(Data4); - } - return false; - } - public override int GetHashCode() - { - // .NET Standard does not have this method -#if DEBUG - return HashCode.Combine(Data1, Data2, Data3, Data4); -#else - int hashCode = -0x8CAC62A; - hashCode = hashCode * -0x5AAAAAD7 + Data1.GetHashCode(); - hashCode = hashCode * -0x5AAAAAD7 + Data2.GetHashCode(); - hashCode = hashCode * -0x5AAAAAD7 + Data3.GetHashCode(); - hashCode = hashCode * -0x5AAAAAD7 + EqualityComparer.Default.GetHashCode(Data4); - return hashCode; -#endif - } - public override string ToString() - { - string str = Data1.ToString("X8") + '-' + Data2.ToString("X4") + '-' + Data3.ToString("X4") + '-'; - for (int i = 0; i < 2; i++) - { - str += Data4[i].ToString("X2"); - } - str += '-'; - for (int i = 2; i < 8; i++) - { - str += Data4[i].ToString("X2"); - } - return str; - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/MIDILocale.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/MIDILocale.cs deleted file mode 100644 index 49cd325b..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/MIDILocale.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; - -namespace Kermalis.DLS2 -{ - // MIDILOCALE - Page 45 of spec - public sealed class MIDILocale - { - public uint Bank_Raw { get; set; } - public uint Instrument_Raw { get; set; } - - public byte CC32 - { - get => (byte)(Bank_Raw & 0x7F); - set - { - if (value > 0x7F) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - Bank_Raw &= unchecked((uint)~0x7F); - Bank_Raw |= value; - } - } - public byte CC0 - { - get => (byte)((Bank_Raw >> 7) & 0x7F); - set - { - if (value > 0x7F) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - Bank_Raw &= unchecked((uint)~(0x7F << 7)); - Bank_Raw |= (uint)(value << 7); - } - } - public bool IsDrum - { - get => (Bank_Raw >> 31) != 0; - set - { - if (value) - { - Bank_Raw |= 1u << 31; - } - else - { - Bank_Raw &= ~(1 << 31); - } - } - } - public byte Instrument - { - get => (byte)(Instrument_Raw & 0x7F); - set - { - if (value > 0x7F) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - Instrument_Raw &= unchecked((uint)~0x7F); - Instrument_Raw |= value; - } - } - - public MIDILocale() { } - public MIDILocale(byte cc32, byte cc0, bool isDrum, byte instrument) - { - CC32 = cc32; - CC0 = cc0; - IsDrum = isDrum; - Instrument = instrument; - } - internal MIDILocale(EndianBinaryReader reader) - { - Bank_Raw = reader.ReadUInt32(); - Instrument_Raw = reader.ReadUInt32(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(Bank_Raw); - writer.Write(Instrument_Raw); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/Range.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/Range.cs deleted file mode 100644 index 7248bf8f..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/Range.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.DLS2 -{ - public sealed class Range - { - public ushort Low { get; set; } - public ushort High { get; set; } - - public Range() { } - public Range(ushort low, ushort high) - { - Low = low; - High = high; - } - internal Range(EndianBinaryReader reader) - { - Low = reader.ReadUInt16(); - High = reader.ReadUInt16(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(Low); - writer.Write(High); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveInfo.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveInfo.cs deleted file mode 100644 index 2f1cb25e..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveInfo.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.DLS2 -{ - public sealed class WaveInfo - { - public WaveFormat FormatTag { get; set; } - public ushort Channels { get; set; } - public uint SamplesPerSec { get; set; } - public uint AvgBytesPerSec { get; set; } - public ushort BlockAlign { get; set; } - - internal WaveInfo() { } - internal WaveInfo(EndianBinaryReader reader) - { - FormatTag = reader.ReadEnum(); - Channels = reader.ReadUInt16(); - SamplesPerSec = reader.ReadUInt32(); - AvgBytesPerSec = reader.ReadUInt32(); - BlockAlign = reader.ReadUInt16(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(FormatTag); - writer.Write(Channels); - writer.Write(SamplesPerSec); - writer.Write(AvgBytesPerSec); - writer.Write(BlockAlign); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveSampleLoop.cs b/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveSampleLoop.cs deleted file mode 100644 index 8e98a54d..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/DLS2/Structs/WaveSampleLoop.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.IO; - -namespace Kermalis.DLS2 -{ - public sealed class WaveSampleLoop - { - public LoopType LoopType { get; set; } - public uint LoopStart { get; set; } - public uint LoopLength { get; set; } - - public WaveSampleLoop() { } - internal WaveSampleLoop(EndianBinaryReader reader) - { - uint byteSize = reader.ReadUInt32(); - if (byteSize != 16) - { - throw new InvalidDataException(); - } - LoopType = reader.ReadEnum(); - LoopStart = reader.ReadUInt32(); - LoopLength = reader.ReadUInt32(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(16u); - writer.Write(LoopType); - writer.Write(LoopStart); - writer.Write(LoopLength); - } - } -} diff --git a/Legacy Repo (.NET Framework)/DLS2/LICENSE.md b/Legacy Repo (.NET Framework)/DLS2/LICENSE.md deleted file mode 100644 index c0a4d4aa..00000000 --- a/Legacy Repo (.NET Framework)/DLS2/LICENSE.md +++ /dev/null @@ -1,23 +0,0 @@ -The zlib/libpng License -======================= - -Copyright (c) 2016 gocha - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - - 3. This notice may not be removed or altered from any source - distribution. \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/SoundFont2/.gitattributes b/Legacy Repo (.NET Framework)/SoundFont2/.gitattributes deleted file mode 100644 index 1ff0c423..00000000 --- a/Legacy Repo (.NET Framework)/SoundFont2/.gitattributes +++ /dev/null @@ -1,63 +0,0 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain diff --git a/Legacy Repo (.NET Framework)/SoundFont2/.gitignore b/Legacy Repo (.NET Framework)/SoundFont2/.gitignore deleted file mode 100644 index 80921d31..00000000 --- a/Legacy Repo (.NET Framework)/SoundFont2/.gitignore +++ /dev/null @@ -1,262 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -Build/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -#*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/SoundFont2/LICENSE.md b/Legacy Repo (.NET Framework)/SoundFont2/LICENSE.md deleted file mode 100644 index c0a4d4aa..00000000 --- a/Legacy Repo (.NET Framework)/SoundFont2/LICENSE.md +++ /dev/null @@ -1,23 +0,0 @@ -The zlib/libpng License -======================= - -Copyright (c) 2016 gocha - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - - 3. This notice may not be removed or altered from any source - distribution. \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/SoundFont2/README.md b/Legacy Repo (.NET Framework)/SoundFont2/README.md deleted file mode 100644 index 73c5404e..00000000 --- a/Legacy Repo (.NET Framework)/SoundFont2/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Kermalis's C# SoundFont 2 library - -A C# library that can read and write SF2 sound bank files. - ----- -# To Do: -* Prevent loading the terminal data -* Find a way to "public seal" SF2Chunk and SF2ListChunk -* Getting info such as samples, modulators and generators -* Separate chunks into their own files -* Certain generators must be in a specified index (KeyRange, VelRange, SampleID and Instrument) according to spec \ No newline at end of file diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2.sln b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2.sln deleted file mode 100644 index b8d3f51d..00000000 --- a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27703.2035 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoundFont2", "SoundFont2\SoundFont2.csproj", "{3C554511-03EA-41F0-AA5D-BABB9C330A33}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3C554511-03EA-41F0-AA5D-BABB9C330A33}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3C554511-03EA-41F0-AA5D-BABB9C330A33}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {A6CED513-A215-4312-8C16-BAF58AA39F72} - EndGlobalSection -EndGlobal diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2.cs b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2.cs deleted file mode 100644 index 1811dc15..00000000 --- a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2.cs +++ /dev/null @@ -1,152 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.IO; - -namespace Kermalis.SoundFont2 -{ - public sealed class SF2 - { - private uint _size; - public InfoListChunk InfoChunk { get; } - public SdtaListChunk SoundChunk { get; } - public PdtaListChunk HydraChunk { get; } - - /// For creating - public SF2() - { - InfoChunk = new InfoListChunk(this); - SoundChunk = new SdtaListChunk(this); - HydraChunk = new PdtaListChunk(this); - } - - /// For reading - public SF2(string path) - { - using (var reader = new EndianBinaryReader(File.Open(path, FileMode.Open))) - { - string str = reader.ReadString(4, false); - if (str != "RIFF") - { - throw new InvalidDataException("RIFF header was not found at the start of the file."); - } - _size = reader.ReadUInt32(); - str = reader.ReadString(4, false); - if (str != "sfbk") - { - throw new InvalidDataException("sfbk header was not found at the expected offset."); - } - InfoChunk = new InfoListChunk(this, reader); - SoundChunk = new SdtaListChunk(this, reader); - HydraChunk = new PdtaListChunk(this, reader); - } - } - - public void Save(string path) - { - using (var writer = new EndianBinaryWriter(File.Open(path, FileMode.Create))) - { - AddTerminals(); - - writer.Write("RIFF", 4); - writer.Write(_size); - writer.Write("sfbk", 4); - - InfoChunk.Write(writer); - SoundChunk.Write(writer); - HydraChunk.Write(writer); - } - } - - - /// Returns sample index - public uint AddSample(short[] pcm16, string name, bool bLoop, uint loopPos, uint sampleRate, byte originalKey, sbyte pitchCorrection) - { - uint start = SoundChunk.SMPLSubChunk.AddSample(pcm16, bLoop, loopPos); - // If the sample is looped the standard requires us to add the 8 bytes from the start of the loop to the end - uint end, loopEnd, loopStart; - - uint len = (uint)pcm16.Length; - if (bLoop) - { - end = start + len + 8; - loopStart = start + loopPos; loopEnd = start + len; - } - else - { - end = start + len; - loopStart = 0; loopEnd = 0; - } - - return AddSampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection); - } - /// Returns instrument index - public uint AddInstrument(string name) - { - return HydraChunk.INSTSubChunk.AddInstrument(new SF2Instrument(name, (ushort)HydraChunk.IBAGSubChunk.Count)); - } - public void AddInstrumentBag() - { - HydraChunk.IBAGSubChunk.AddBag(new SF2Bag(this, false)); - } - public void AddInstrumentModulator() - { - HydraChunk.IMODSubChunk.AddModulator(new SF2ModulatorList()); - } - public void AddInstrumentGenerator() - { - HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList()); - } - public void AddInstrumentGenerator(SF2Generator generator, SF2GeneratorAmount amount) - { - HydraChunk.IGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); - } - public void AddPreset(string name, ushort preset, ushort bank) - { - HydraChunk.PHDRSubChunk.AddPreset(new SF2PresetHeader(name, preset, bank, (ushort)HydraChunk.PBAGSubChunk.Count)); - } - public void AddPresetBag() - { - HydraChunk.PBAGSubChunk.AddBag(new SF2Bag(this, true)); - } - public void AddPresetModulator() - { - HydraChunk.PMODSubChunk.AddModulator(new SF2ModulatorList()); - } - public void AddPresetGenerator() - { - HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList()); - } - public void AddPresetGenerator(SF2Generator generator, SF2GeneratorAmount amount) - { - HydraChunk.PGENSubChunk.AddGenerator(new SF2GeneratorList(generator, amount)); - } - - private uint AddSampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) - { - return HydraChunk.SHDRSubChunk.AddSample(new SF2SampleHeader(name, start, end, loopStart, loopEnd, sampleRate, originalKey, pitchCorrection)); - } - private void AddTerminals() - { - AddSampleHeader("EOS", 0, 0, 0, 0, 0, 0, 0); - AddInstrument("EOI"); - AddInstrumentBag(); - AddInstrumentGenerator(); - AddInstrumentModulator(); - AddPreset("EOP", 0xFF, 0xFF); - AddPresetBag(); - AddPresetGenerator(); - AddPresetModulator(); - } - - internal void UpdateSize() - { - if (InfoChunk == null || SoundChunk == null || HydraChunk == null) - { - return; - } - _size = 4 - + InfoChunk.UpdateSize() + 8 - + SoundChunk.UpdateSize() + 8 - + HydraChunk.UpdateSize() + 8; - } - } -} diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Chunks.cs b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Chunks.cs deleted file mode 100644 index 96013042..00000000 --- a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Chunks.cs +++ /dev/null @@ -1,1105 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Collections.Generic; - -namespace Kermalis.SoundFont2 -{ - public class SF2Chunk - { - protected readonly SF2 _sf2; - - /// Length 4 - public string ChunkName { get; } - /// Size in bytes - public uint Size { get; protected set; } - - protected SF2Chunk(SF2 inSf2, string name) - { - _sf2 = inSf2; - ChunkName = name; - } - protected SF2Chunk(SF2 inSf2, EndianBinaryReader reader) - { - _sf2 = inSf2; - ChunkName = reader.ReadString(4, false); - Size = reader.ReadUInt32(); - } - - internal virtual void Write(EndianBinaryWriter writer) - { - writer.Write(ChunkName, 4); - writer.Write(Size); - } - } - - public abstract class SF2ListChunk : SF2Chunk - { - ///Length 4 - public string ListChunkName { get; } - - protected SF2ListChunk(SF2 inSf2, string name) : base(inSf2, "LIST") - { - ListChunkName = name; - Size = 4; - } - protected SF2ListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - ListChunkName = reader.ReadString(4, false); - } - - internal abstract uint UpdateSize(); - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(ListChunkName, 4); - } - } - - public sealed class SF2PresetHeader - { - public const uint Size = 38; - - /// Length 20 - public string PresetName { get; set; } - public ushort Preset { get; set; } - public ushort Bank { get; set; } - public ushort PresetBagIndex { get; set; } - // Reserved for future implementations - private readonly uint _library; - private readonly uint _genre; - private readonly uint _morphology; - - internal SF2PresetHeader(string name, ushort preset, ushort bank, ushort index) - { - PresetName = name; - Preset = preset; - Bank = bank; - PresetBagIndex = index; - } - internal SF2PresetHeader(EndianBinaryReader reader) - { - PresetName = reader.ReadString(20, true); - Preset = reader.ReadUInt16(); - Bank = reader.ReadUInt16(); - PresetBagIndex = reader.ReadUInt16(); - _library = reader.ReadUInt32(); - _genre = reader.ReadUInt32(); - _morphology = reader.ReadUInt32(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(PresetName, 20); - writer.Write(Preset); - writer.Write(Bank); - writer.Write(PresetBagIndex); - writer.Write(_library); - writer.Write(_genre); - writer.Write(_morphology); - } - - public override string ToString() - { - return $"Preset Header - Bank = {Bank}" + - $",\nPreset = {Preset}" + - $",\nName = \"{PresetName}\""; - } - } - - /// Covers sfPresetBag and sfInstBag - public sealed class SF2Bag - { - public const uint Size = 4; - - /// Index in list of generators - public ushort GeneratorIndex { get; set; } - /// Index in list of modulators - public ushort ModulatorIndex { get; set; } - - internal SF2Bag(SF2 inSf2, bool isPresetBag) - { - if (isPresetBag) - { - GeneratorIndex = (ushort)inSf2.HydraChunk.PGENSubChunk.Count; - ModulatorIndex = (ushort)inSf2.HydraChunk.PMODSubChunk.Count; - } - else - { - GeneratorIndex = (ushort)inSf2.HydraChunk.IGENSubChunk.Count; - ModulatorIndex = (ushort)inSf2.HydraChunk.IMODSubChunk.Count; - } - } - internal SF2Bag(EndianBinaryReader reader) - { - GeneratorIndex = reader.ReadUInt16(); - ModulatorIndex = reader.ReadUInt16(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(GeneratorIndex); - writer.Write(ModulatorIndex); - } - - public override string ToString() - { - return $"Bag - Generator index = {GeneratorIndex}" + - $",\nModulator index = {ModulatorIndex}"; - } - } - - /// Covers sfModList and sfInstModList - public sealed class SF2ModulatorList - { - public const uint Size = 10; - - public SF2Modulator ModulatorSource { get; set; } - public SF2Generator ModulatorDestination { get; set; } - public short ModulatorAmount { get; set; } - public SF2Modulator ModulatorAmountSource { get; set; } - public SF2Transform ModulatorTransform { get; set; } - - internal SF2ModulatorList() { } - internal SF2ModulatorList(EndianBinaryReader reader) - { - ModulatorSource = reader.ReadEnum(); - ModulatorDestination = reader.ReadEnum(); - ModulatorAmount = reader.ReadInt16(); - ModulatorAmountSource = reader.ReadEnum(); - ModulatorTransform = reader.ReadEnum(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(ModulatorSource); - writer.Write(ModulatorDestination); - writer.Write(ModulatorAmount); - writer.Write(ModulatorAmountSource); - writer.Write(ModulatorTransform); - } - - public override string ToString() - { - return $"Modulator List - Modulator source = {ModulatorSource}" + - $",\nModulator destination = {ModulatorDestination}" + - $",\nModulator amount = {ModulatorAmount}" + - $",\nModulator amount source = {ModulatorAmountSource}" + - $",\nModulator transform = {ModulatorTransform}"; - } - } - - public sealed class SF2GeneratorList - { - public const uint Size = 4; - - public SF2Generator Generator { get; set; } - public SF2GeneratorAmount GeneratorAmount { get; set; } - - internal SF2GeneratorList() { } - internal SF2GeneratorList(SF2Generator generator, SF2GeneratorAmount amount) - { - Generator = generator; - GeneratorAmount = amount; - } - internal SF2GeneratorList(EndianBinaryReader reader) - { - Generator = reader.ReadEnum(); - GeneratorAmount = new SF2GeneratorAmount { Amount = reader.ReadInt16() }; - } - - public void Write(EndianBinaryWriter writer) - { - writer.Write(Generator); - writer.Write(GeneratorAmount.Amount); - } - - public override string ToString() - { - return $"Generator List - Generator = {Generator}" + - $",\nGenerator amount = \"{GeneratorAmount}\""; - } - } - - public sealed class SF2Instrument - { - public const uint Size = 22; - - /// Length 20 - public string InstrumentName { get; set; } - public ushort InstrumentBagIndex { get; set; } - - internal SF2Instrument(string name, ushort index) - { - InstrumentName = name; - InstrumentBagIndex = index; - } - internal SF2Instrument(EndianBinaryReader reader) - { - InstrumentName = reader.ReadString(20, true); - InstrumentBagIndex = reader.ReadUInt16(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(InstrumentName, 20); - writer.Write(InstrumentBagIndex); - } - - public override string ToString() - { - return $"Instrument - Name = \"{InstrumentName}\""; - } - } - - public sealed class SF2SampleHeader - { - public const uint Size = 46; - - /// Length 20 - public string SampleName { get; set; } - public uint Start { get; set; } - public uint End { get; set; } - public uint LoopStart { get; set; } - public uint LoopEnd { get; set; } - public uint SampleRate { get; set; } - public byte OriginalKey { get; set; } - public sbyte PitchCorrection { get; set; } - public ushort SampleLink { get; set; } - public SF2SampleLink SampleType { get; set; } - - internal SF2SampleHeader(string name, uint start, uint end, uint loopStart, uint loopEnd, uint sampleRate, byte originalKey, sbyte pitchCorrection) - { - SampleName = name; - Start = start; - End = end; - LoopStart = loopStart; - LoopEnd = loopEnd; - SampleRate = sampleRate; - OriginalKey = originalKey; - PitchCorrection = pitchCorrection; - SampleType = SF2SampleLink.MonoSample; - } - internal SF2SampleHeader(EndianBinaryReader reader) - { - SampleName = reader.ReadString(20, true); - Start = reader.ReadUInt32(); - End = reader.ReadUInt32(); - LoopStart = reader.ReadUInt32(); - LoopEnd = reader.ReadUInt32(); - SampleRate = reader.ReadUInt32(); - OriginalKey = reader.ReadByte(); - PitchCorrection = reader.ReadSByte(); - SampleLink = reader.ReadUInt16(); - SampleType = reader.ReadEnum(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(SampleName, 20); - writer.Write(Start); - writer.Write(End); - writer.Write(LoopStart); - writer.Write(LoopEnd); - writer.Write(SampleRate); - writer.Write(OriginalKey); - writer.Write(PitchCorrection); - writer.Write(SampleLink); - writer.Write(SampleType); - } - - public override string ToString() - { - return $"Sample - Name = \"{SampleName}\"" + - $",\nType = {SampleType}"; - } - } - - #region Sub-Chunks - - public sealed class VersionSubChunk : SF2Chunk - { - public SF2VersionTag Version { get; set; } - - internal VersionSubChunk(SF2 inSf2, string subChunkName) : base(inSf2, subChunkName) - { - Size = SF2VersionTag.Size; - inSf2.UpdateSize(); - } - internal VersionSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - Version = new SF2VersionTag(reader); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - Version.Write(writer); - } - - public override string ToString() - { - return $"Version Chunk - Revision = {Version}"; - } - } - - public sealed class HeaderSubChunk : SF2Chunk - { - public int MaxSize { get; } - private int _fieldTargetLength; - private string _field; - /// Length - public string Field - { - get => _field; - set - { - if (value.Length >= MaxSize) // Input too long; cut it down - { - _fieldTargetLength = MaxSize; - } - else if (value.Length % 2 == 0) // Even amount of characters - { - _fieldTargetLength = value.Length + 2; // Add two null-terminators to keep the byte count even - } - else // Odd amount of characters - { - _fieldTargetLength = value.Length + 1; // Add one null-terminator since that would make byte the count even - } - _field = value; - Size = (uint)_fieldTargetLength; - _sf2.UpdateSize(); - } - } - - internal HeaderSubChunk(SF2 inSf2, string subChunkName, int maxSize = 0x100) : base(inSf2, subChunkName) - { - MaxSize = maxSize; - } - internal HeaderSubChunk(SF2 inSf2, EndianBinaryReader reader, int maxSize = 0x100) : base(inSf2, reader) - { - MaxSize = maxSize; - _field = reader.ReadString((int)Size, true); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - writer.Write(_field, _fieldTargetLength); - } - - public override string ToString() - { - return $"Header Chunk - Name = \"{ChunkName}\"" + - $",\nField Max Size = {MaxSize}" + - $",\nField = \"{Field}\""; - } - } - - public sealed class SMPLSubChunk : SF2Chunk - { - private readonly List _samples = new List(); // Block of sample data - - internal SMPLSubChunk(SF2 inSf2) : base(inSf2, "smpl") { } - internal SMPLSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / sizeof(short); i++) - { - _samples.Add(reader.ReadInt16()); - } - } - - // Returns index of the start of the sample - internal uint AddSample(short[] pcm16, bool bLoop, uint loopPos) - { - uint start = (uint)_samples.Count; - - // Write wave - _samples.AddRange(pcm16); - - // If looping is enabled, write 8 samples from the loop point - if (bLoop) - { - // In case (loopPos + i) is greater than the sample length - uint max = (uint)pcm16.Length - loopPos; - for (uint i = 0; i < 8; i++) - { - _samples.Add(pcm16[loopPos + (i % max)]); - } - } - - // Write 46 empty samples - _samples.AddRange(new short[46]); - - Size = (uint)_samples.Count * sizeof(short); - _sf2.UpdateSize(); - return start; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - foreach (short s in _samples) - { - writer.Write(s); - } - } - - public override string ToString() - { - return $"Sample Data Chunk"; - } - } - - public sealed class PHDRSubChunk : SF2Chunk - { - private readonly List _presets = new List(); - public uint Count => (uint)_presets.Count; - - internal PHDRSubChunk(SF2 inSf2) : base(inSf2, "phdr") { } - internal PHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2PresetHeader.Size; i++) - { - _presets.Add(new SF2PresetHeader(reader)); - } - } - - internal void AddPreset(SF2PresetHeader preset) - { - _presets.Add(preset); - Size = Count * SF2PresetHeader.Size; - _sf2.UpdateSize(); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _presets[i].Write(writer); - } - } - - public override string ToString() - { - return $"Preset Header Chunk - Preset count = {Count}"; - } - } - - public sealed class INSTSubChunk : SF2Chunk - { - private readonly List _instruments = new List(); - public uint Count => (uint)_instruments.Count; - - internal INSTSubChunk(SF2 inSf2) : base(inSf2, "inst") { } - internal INSTSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2Instrument.Size; i++) - { - _instruments.Add(new SF2Instrument(reader)); - } - } - - internal uint AddInstrument(SF2Instrument instrument) - { - _instruments.Add(instrument); - Size = Count * SF2Instrument.Size; - _sf2.UpdateSize(); - return Count - 1; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _instruments[i].Write(writer); - } - } - - public override string ToString() - { - return $"Instrument Chunk - Instrument count = {Count}"; - } - } - - public sealed class BAGSubChunk : SF2Chunk - { - private readonly List _bags = new List(); - public uint Count => (uint)_bags.Count; - - internal BAGSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pbag" : "ibag") { } - internal BAGSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2Bag.Size; i++) - { - _bags.Add(new SF2Bag(reader)); - } - } - - internal void AddBag(SF2Bag bag) - { - _bags.Add(bag); - Size = Count * SF2Bag.Size; - _sf2.UpdateSize(); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _bags[i].Write(writer); - } - } - - public override string ToString() - { - return $"Bag Chunk - Name = \"{ChunkName}\"" + - $",\nBag count = {Count}"; - } - } - - public sealed class MODSubChunk : SF2Chunk - { - private readonly List _modulators = new List(); - public uint Count => (uint)_modulators.Count; - - internal MODSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pmod" : "imod") { } - internal MODSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2ModulatorList.Size; i++) - { - _modulators.Add(new SF2ModulatorList(reader)); - } - } - - internal void AddModulator(SF2ModulatorList modulator) - { - _modulators.Add(modulator); - Size = Count * SF2ModulatorList.Size; - _sf2.UpdateSize(); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _modulators[i].Write(writer); - } - } - - public override string ToString() - { - return $"Modulator Chunk - Name = \"{ChunkName}\"" + - $",\nModulator count = {Count}"; - } - } - - public sealed class GENSubChunk : SF2Chunk - { - private readonly List _generators = new List(); - public uint Count => (uint)_generators.Count; - - internal GENSubChunk(SF2 inSf2, bool preset) : base(inSf2, preset ? "pgen" : "igen") { } - internal GENSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2GeneratorList.Size; i++) - { - _generators.Add(new SF2GeneratorList(reader)); - } - } - - internal void AddGenerator(SF2GeneratorList generator) - { - _generators.Add(generator); - Size = Count * SF2GeneratorList.Size; - _sf2.UpdateSize(); - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _generators[i].Write(writer); - } - } - - public override string ToString() - { - return $"Generator Chunk - Name = \"{ChunkName}\"" + - $",\nGenerator count = {Count}"; - } - } - - public sealed class SHDRSubChunk : SF2Chunk - { - private readonly List _samples = new List(); - public uint Count => (uint)_samples.Count; - - internal SHDRSubChunk(SF2 inSf2) : base(inSf2, "shdr") { } - internal SHDRSubChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - for (int i = 0; i < Size / SF2SampleHeader.Size; i++) - { - _samples.Add(new SF2SampleHeader(reader)); - } - } - - internal uint AddSample(SF2SampleHeader sample) - { - _samples.Add(sample); - Size = Count * SF2SampleHeader.Size; - _sf2.UpdateSize(); - return Count - 1; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - for (int i = 0; i < Count; i++) - { - _samples[i].Write(writer); - } - } - - public override string ToString() - { - return $"Sample Header Chunk - Sample header count = {Count}"; - } - } - - #endregion - - #region Main Chunks - - public sealed class InfoListChunk : SF2ListChunk - { - private readonly List _subChunks = new List(); - private const string DefaultEngine = "EMU8000"; - public string Engine - { - get - { - if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "isng") { Field = DefaultEngine }); - return DefaultEngine; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "isng") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "isng") { Field = value }); - } - } - } - - private const string DefaultBank = "General MIDI"; - public string Bank - { - get - { - if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "INAM") { Field = DefaultBank }); - return DefaultBank; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "INAM") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "INAM") { Field = value }); - } - } - } - public string ROM - { - get - { - if (_subChunks.Find(s => s.ChunkName == "irom") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "irom") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "irom") { Field = value }); - } - } - } - public SF2VersionTag ROMVersion - { - get - { - if (_subChunks.Find(s => s.ChunkName == "iver") is VersionSubChunk chunk) - { - return chunk.Version; - } - else - { - return null; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "iver") is VersionSubChunk chunk) - { - chunk.Version = value; - } - else - { - _subChunks.Add(new VersionSubChunk(_sf2, "iver") { Version = value }); - } - } - } - public string Date - { - get - { - if (_subChunks.Find(s => s.ChunkName == "ICRD") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "ICRD") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "ICRD") { Field = value }); - } - } - } - public string Designer - { - get - { - if (_subChunks.Find(s => s.ChunkName == "IENG") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "IENG") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "IENG") { Field = value }); - } - } - } - public string Products - { - get - { - if (_subChunks.Find(s => s.ChunkName == "IPRD") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "IPRD") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "IPRD") { Field = value }); - } - } - } - public string Copyright - { - get - { - if (_subChunks.Find(s => s.ChunkName == "ICOP") is HeaderSubChunk icop) - { - return icop.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "ICOP") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "ICOP") { Field = value }); - } - } - } - - private const int CommentMaxSize = 0x10000; - public string Comment - { - get - { - if (_subChunks.Find(s => s.ChunkName == "ICMT") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "ICMT") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "ICMT", maxSize: CommentMaxSize) { Field = value }); - } - } - } - public string Tools - { - get - { - if (_subChunks.Find(s => s.ChunkName == "ISFT") is HeaderSubChunk chunk) - { - return chunk.Field; - } - else - { - return string.Empty; - } - } - set - { - if (_subChunks.Find(s => s.ChunkName == "ISFT") is HeaderSubChunk chunk) - { - chunk.Field = value; - } - else - { - _subChunks.Add(new HeaderSubChunk(_sf2, "ISFT") { Field = value }); - } - } - } - - internal InfoListChunk(SF2 inSf2) : base(inSf2, "INFO") - { - // Mandatory sub-chunks - _subChunks.Add(new VersionSubChunk(inSf2, "ifil") { Version = new SF2VersionTag(2, 1) }); - _subChunks.Add(new HeaderSubChunk(inSf2, "isng") { Field = DefaultEngine }); - _subChunks.Add(new HeaderSubChunk(inSf2, "INAM") { Field = DefaultBank }); - inSf2.UpdateSize(); - } - internal InfoListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - long startOffset = reader.BaseStream.Position; - while (reader.BaseStream.Position < startOffset + Size - 4) // The 4 represents the INFO that was already read - { - // Peek 4 chars for the chunk name - string name = reader.ReadString(4, false); - reader.BaseStream.Position -= 4; - switch (name) - { - case "ICMT": _subChunks.Add(new HeaderSubChunk(inSf2, reader, maxSize: CommentMaxSize)); break; - case "ifil": - case "iver": _subChunks.Add(new VersionSubChunk(inSf2, reader)); break; - case "isng": - case "INAM": - case "ICRD": - case "IENG": - case "IPRD": - case "ICOP": - case "ISFT": - case "irom": _subChunks.Add(new HeaderSubChunk(inSf2, reader)); break; - default: throw new NotSupportedException($"Unsupported chunk name at 0x{reader.BaseStream.Position:X}: \"{name}\""); - } - } - } - - internal override uint UpdateSize() - { - Size = 4; - foreach (SF2Chunk sub in _subChunks) - { - Size += sub.Size + 8; - } - - return Size; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - foreach (SF2Chunk sub in _subChunks) - { - sub.Write(writer); - } - } - - public override string ToString() - { - return $"Info List Chunk - Sub-chunk count = {_subChunks.Count}"; - } - } - - public sealed class SdtaListChunk : SF2ListChunk - { - public SMPLSubChunk SMPLSubChunk { get; } - - internal SdtaListChunk(SF2 inSf2) : base(inSf2, "sdta") - { - SMPLSubChunk = new SMPLSubChunk(inSf2); - inSf2.UpdateSize(); - } - internal SdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - SMPLSubChunk = new SMPLSubChunk(inSf2, reader); - } - - internal override uint UpdateSize() - { - return Size = 4 - + SMPLSubChunk.Size + 8; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - SMPLSubChunk.Write(writer); - } - - public override string ToString() - { - return $"Sample Data List Chunk"; - } - } - - public sealed class PdtaListChunk : SF2ListChunk - { - public PHDRSubChunk PHDRSubChunk { get; } - public BAGSubChunk PBAGSubChunk { get; } - public MODSubChunk PMODSubChunk { get; } - public GENSubChunk PGENSubChunk { get; } - public INSTSubChunk INSTSubChunk { get; } - public BAGSubChunk IBAGSubChunk { get; } - public MODSubChunk IMODSubChunk { get; } - public GENSubChunk IGENSubChunk { get; } - public SHDRSubChunk SHDRSubChunk { get; } - - internal PdtaListChunk(SF2 inSf2) : base(inSf2, "pdta") - { - PHDRSubChunk = new PHDRSubChunk(inSf2); - PBAGSubChunk = new BAGSubChunk(inSf2, true); - PMODSubChunk = new MODSubChunk(inSf2, true); - PGENSubChunk = new GENSubChunk(inSf2, true); - INSTSubChunk = new INSTSubChunk(inSf2); - IBAGSubChunk = new BAGSubChunk(inSf2, false); - IMODSubChunk = new MODSubChunk(inSf2, false); - IGENSubChunk = new GENSubChunk(inSf2, false); - SHDRSubChunk = new SHDRSubChunk(inSf2); - inSf2.UpdateSize(); - } - internal PdtaListChunk(SF2 inSf2, EndianBinaryReader reader) : base(inSf2, reader) - { - PHDRSubChunk = new PHDRSubChunk(inSf2, reader); - PBAGSubChunk = new BAGSubChunk(inSf2, reader); - PMODSubChunk = new MODSubChunk(inSf2, reader); - PGENSubChunk = new GENSubChunk(inSf2, reader); - INSTSubChunk = new INSTSubChunk(inSf2, reader); - IBAGSubChunk = new BAGSubChunk(inSf2, reader); - IMODSubChunk = new MODSubChunk(inSf2, reader); - IGENSubChunk = new GENSubChunk(inSf2, reader); - SHDRSubChunk = new SHDRSubChunk(inSf2, reader); - } - - internal override uint UpdateSize() - { - return Size = 4 - + PHDRSubChunk.Size + 8 - + PBAGSubChunk.Size + 8 - + PMODSubChunk.Size + 8 - + PGENSubChunk.Size + 8 - + INSTSubChunk.Size + 8 - + IBAGSubChunk.Size + 8 - + IMODSubChunk.Size + 8 - + IGENSubChunk.Size + 8 - + SHDRSubChunk.Size + 8; - } - - internal override void Write(EndianBinaryWriter writer) - { - base.Write(writer); - PHDRSubChunk.Write(writer); - PBAGSubChunk.Write(writer); - PMODSubChunk.Write(writer); - PGENSubChunk.Write(writer); - INSTSubChunk.Write(writer); - IBAGSubChunk.Write(writer); - IMODSubChunk.Write(writer); - IGENSubChunk.Write(writer); - SHDRSubChunk.Write(writer); - } - - public override string ToString() - { - return $"Hydra List Chunk"; - } - } - - #endregion -} diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Types.cs b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Types.cs deleted file mode 100644 index b297d802..00000000 --- a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SF2Types.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.Runtime.InteropServices; - -namespace Kermalis.SoundFont2 -{ - /// SF2 v2.1 spec page 16 - public sealed class SF2VersionTag - { - public const uint Size = 4; - - public ushort Major { get; } - public ushort Minor { get; } - - public SF2VersionTag(ushort major, ushort minor) - { - Major = major; - Minor = minor; - } - internal SF2VersionTag(EndianBinaryReader reader) - { - Major = reader.ReadUInt16(); - Minor = reader.ReadUInt16(); - } - - internal void Write(EndianBinaryWriter writer) - { - writer.Write(Major); - writer.Write(Minor); - } - - public override string ToString() - { - return $"v{Major}.{Minor}"; - } - } - - /// SF2 spec v2.1 page 19 - Two bytes that can handle either two 8-bit values or a single 16-bit value - [StructLayout(LayoutKind.Explicit)] - public struct SF2GeneratorAmount - { - [FieldOffset(0)] public byte LowByte; - [FieldOffset(1)] public byte HighByte; - [FieldOffset(0)] public short Amount; - [FieldOffset(0)] public ushort UAmount; - - public override string ToString() - { - return $"BLo = {LowByte}, BHi = {HighByte}, Sh = {Amount}, U = {UAmount}"; - } - } - - /// SF2 v2.1 spec page 20 - public enum SF2SampleLink : ushort - { - MonoSample = 1, - RightSample = 2, - LeftSample = 4, - LinkedSample = 8, - RomMonoSample = 0x8001, - RomRightSample = 0x8002, - RomLeftSample = 0x8004, - RomLinkedSample = 0x8008 - } - - /// SF2 v2.1 spec page 38 - public enum SF2Generator : ushort - { - StartAddrsOffset = 0, - EndAddrsOffset = 1, - StartloopAddrsOffset = 2, - EndloopAddrsOffset = 3, - StartAddrsCoarseOffset = 4, - ModLfoToPitch = 5, - VibLfoToPitch = 6, - ModEnvToPitch = 7, - InitialFilterFc = 8, - InitialFilterQ = 9, - ModLfoToFilterFc = 10, - ModEnvToFilterFc = 11, - EndAddrsCoarseOffset = 12, - ModLfoToVolume = 13, - ChorusEffectsSend = 15, - ReverbEffectsSend = 16, - Pan = 17, - DelayModLFO = 21, - FreqModLFO = 22, - DelayVibLFO = 23, - FreqVibLFO = 24, - DelayModEnv = 25, - AttackModEnv = 26, - HoldModEnv = 27, - DecayModEnv = 28, - SustainModEnv = 29, - ReleaseModEnv = 30, - KeynumToModEnvHold = 31, - KeynumToModEnvDecay = 32, - DelayVolEnv = 33, - AttackVolEnv = 34, - HoldVolEnv = 35, - DecayVolEnv = 36, - SustainVolEnv = 37, - ReleaseVolEnv = 38, - KeynumToVolEnvHold = 39, - KeynumToVolEnvDecay = 40, - Instrument = 41, - KeyRange = 43, - VelRange = 44, - StartloopAddrsCoarseOffset = 45, - Keynum = 46, - Velocity = 47, - InitialAttenuation = 48, - EndloopAddrsCoarseOffset = 50, - CoarseTune = 51, - FineTune = 52, - SampleID = 53, - SampleModes = 54, - ScaleTuning = 56, - ExclusiveClass = 57, - OverridingRootKey = 58, - EndOper = 60 - } - - /// SF2 v2.1 spec page 50 - public enum SF2Modulator : ushort - { - None = 0, - NoteOnVelocity = 1, - NoteOnKey = 2, - PolyPressure = 10, - ChnPressure = 13, - PitchWheel = 14, - PitchWheelSensivity = 16 - } - - /// SF2 v2.1 spec page 52 - public enum SF2Transform : ushort - { - Linear = 0, - Concave = 1, - Convex = 2 - } -} diff --git a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SoundFont2.csproj b/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SoundFont2.csproj deleted file mode 100644 index 3ed42002..00000000 --- a/Legacy Repo (.NET Framework)/SoundFont2/SoundFont2/SoundFont2.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - netstandard2.0 - Release - Kermalis - - SoundFont2 - SoundFont2 - SoundFont2 - Kermalis.SoundFont2 - 1.0.0.0 - - - - ..\Build - Auto - none - false - - - - - - - diff --git a/Legacy Repo (.NET Framework)/VGMusicStudio b/Legacy Repo (.NET Framework)/VGMusicStudio deleted file mode 160000 index 3a628c5c..00000000 --- a/Legacy Repo (.NET Framework)/VGMusicStudio +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3a628c5c4c62f0e159a3e1161705dbb482618cca diff --git a/MigrationBackup/655cc60b/VG Music Studio/NuGetUpgradeLog.html b/MigrationBackup/655cc60b/VG Music Studio/NuGetUpgradeLog.html deleted file mode 100644 index 7ca2a181..00000000 --- a/MigrationBackup/655cc60b/VG Music Studio/NuGetUpgradeLog.html +++ /dev/null @@ -1,223 +0,0 @@ - - - - - NuGetMigrationLog -

- NuGet Migration Report - VG Music Studio

\ No newline at end of file diff --git a/MigrationBackup/655cc60b/VG Music Studio/VG Music Studio.csproj b/MigrationBackup/655cc60b/VG Music Studio/VG Music Studio.csproj deleted file mode 100644 index 1b4c548a..00000000 --- a/MigrationBackup/655cc60b/VG Music Studio/VG Music Studio.csproj +++ /dev/null @@ -1,411 +0,0 @@ - - - - - Debug - AnyCPU - {97C8ACF8-66A3-4321-91D6-3E94EACA577F} - WinExe - Kermalis.VGMusicStudio - VG Music Studio - v4.8 - 512 - true - - false - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true - - - AnyCPU - true - full - false - ..\Build\ - DEBUG;TRACE - prompt - 4 - Off - false - - - AnyCPU - pdbonly - true - ..\Build\ - TRACE - prompt - 4 - false - On - - - Properties\Icon.ico - - - Kermalis.VGMusicStudio.Program - - - - Dependencies\DLS2.dll - - - ..\packages\EndianBinaryIO.1.1.2\lib\netstandard2.0\EndianBinaryIO.dll - - - ..\packages\Microsoft.Win32.Registry.5.0.0\lib\net461\Microsoft.Win32.Registry.dll - - - ..\packages\Microsoft.Win32.Registry.AccessControl.5.0.0\lib\net461\Microsoft.Win32.Registry.AccessControl.dll - - - ..\packages\Microsoft.Win32.SystemEvents.5.0.0\lib\net461\Microsoft.Win32.SystemEvents.dll - - - ..\packages\Microsoft.WindowsAPICodePack-Core.1.1.0.2\lib\Microsoft.WindowsAPICodePack.dll - - - ..\packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.Shell.dll - - - ..\packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.ShellExtensions.dll - - - ..\packages\NAudio.2.0.1\lib\netstandard2.0\NAudio.dll - - - ..\packages\NAudio.Asio.2.0.0\lib\netstandard2.0\NAudio.Asio.dll - - - ..\packages\NAudio.Core.2.0.0\lib\netstandard2.0\NAudio.Core.dll - - - ..\packages\NAudio.Midi.2.0.1\lib\netstandard2.0\NAudio.Midi.dll - - - ..\packages\NAudio.Wasapi.2.0.0\lib\netstandard2.0\NAudio.Wasapi.dll - - - ..\packages\NAudio.WinForms.2.0.1\lib\net472\NAudio.WinForms.dll - - - ..\packages\NAudio.WinMM.2.0.1\lib\netstandard2.0\NAudio.WinMM.dll - - - ..\packages\ObjectListView.Official.2.9.1\lib\net20\ObjectListView.dll - - - - - False - Dependencies\Sanford.Multimedia.Midi.dll - - - False - Dependencies\SoundFont2.dll - - - - ..\packages\System.CodeDom.5.0.0\lib\net461\System.CodeDom.dll - - - - - ..\packages\System.Configuration.ConfigurationManager.5.0.0\lib\net461\System.Configuration.ConfigurationManager.dll - - - - ..\packages\System.Data.Odbc.5.0.0\lib\net461\System.Data.Odbc.dll - - - ..\packages\System.Data.OleDb.5.0.0\lib\net461\System.Data.OleDb.dll - - - - ..\packages\System.Data.SqlClient.4.8.1\lib\net461\System.Data.SqlClient.dll - - - ..\packages\System.Diagnostics.EventLog.5.0.1\lib\net461\System.Diagnostics.EventLog.dll - - - ..\packages\System.Diagnostics.PerformanceCounter.5.0.1\lib\net461\System.Diagnostics.PerformanceCounter.dll - - - - - - ..\packages\System.Drawing.Common.5.0.0\lib\net461\System.Drawing.Common.dll - - - - ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll - True - True - - - ..\packages\System.IO.FileSystem.AccessControl.5.0.0\lib\net461\System.IO.FileSystem.AccessControl.dll - - - ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll - True - True - - - ..\packages\System.IO.Packaging.5.0.0\lib\net46\System.IO.Packaging.dll - - - ..\packages\System.IO.Pipes.AccessControl.5.0.0\lib\net461\System.IO.Pipes.AccessControl.dll - - - ..\packages\System.IO.Ports.5.0.0\lib\net461\System.IO.Ports.dll - - - - - ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll - True - True - - - - ..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll - - - - - ..\packages\System.Security.AccessControl.5.0.0\lib\net461\System.Security.AccessControl.dll - - - ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll - True - True - - - ..\packages\System.Security.Cryptography.Cng.5.0.0\lib\net47\System.Security.Cryptography.Cng.dll - - - ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll - True - True - - - ..\packages\System.Security.Cryptography.Pkcs.5.0.1\lib\net461\System.Security.Cryptography.Pkcs.dll - - - ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - True - True - - - ..\packages\System.Security.Cryptography.ProtectedData.5.0.0\lib\net461\System.Security.Cryptography.ProtectedData.dll - - - ..\packages\System.Security.Cryptography.Xml.5.0.0\lib\net461\System.Security.Cryptography.Xml.dll - - - ..\packages\System.Security.Permissions.5.0.0\lib\net461\System.Security.Permissions.dll - - - ..\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll - - - - ..\packages\System.ServiceModel.Duplex.4.8.0\lib\net461\System.ServiceModel.Duplex.dll - - - ..\packages\System.ServiceModel.Http.4.8.0\lib\net461\System.ServiceModel.Http.dll - - - ..\packages\System.ServiceModel.NetTcp.4.8.0\lib\net461\System.ServiceModel.NetTcp.dll - - - ..\packages\System.ServiceModel.Primitives.4.8.0\lib\net461\System.ServiceModel.Primitives.dll - - - ..\packages\System.ServiceModel.Security.4.8.0\lib\net461\System.ServiceModel.Security.dll - - - ..\packages\System.ServiceModel.Syndication.5.0.0\lib\net461\System.ServiceModel.Syndication.dll - - - - ..\packages\System.ServiceProcess.ServiceController.5.0.0\lib\net461\System.ServiceProcess.ServiceController.dll - - - - ..\packages\System.Text.Encoding.CodePages.5.0.0\lib\net461\System.Text.Encoding.CodePages.dll - - - ..\packages\System.Threading.AccessControl.5.0.0\lib\net461\System.Threading.AccessControl.dll - - - - - - - - - - - - - ..\packages\YamlDotNet.11.2.1\lib\net45\YamlDotNet.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Strings.resx - - - - - Component - - - - - - Component - - - - - - - - - - - - Always - - - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - True - - - Always - - - Always - - - Always - - - Always - - - - - ResXFileCodeGenerator - Strings.Designer.cs - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - - - - - False - Microsoft .NET Framework 4.7.1 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - \ No newline at end of file diff --git a/MigrationBackup/655cc60b/VG Music Studio/packages.config b/MigrationBackup/655cc60b/VG Music Studio/packages.config deleted file mode 100644 index 9786087a..00000000 --- a/MigrationBackup/655cc60b/VG Music Studio/packages.config +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/VG Music Studio.backup/AlphaDream.yaml b/VG Music Studio.backup/AlphaDream.yaml deleted file mode 100644 index c6709d4d..00000000 --- a/VG Music Studio.backup/AlphaDream.yaml +++ /dev/null @@ -1,201 +0,0 @@ -A88E_00: - Name: "Mario & Luigi - Superstar Saga (USA)" - AudioEngineVersion: "MLSS" - SongTableOffsets: 0x21CB70 - SongTableSizes: 407 - VoiceTableOffset: 0x21D1CC - SampleTableOffset: 0xA806B8 - SampleTableSize: 236 - Remap: "MLSS" - Playlists: - Music: - 1: "1" - 2: "2" - 3: "Mini Game" - 4: "Border Jump" - 5: "Star 'Stache Smash" - 6: "6" - 7: "7" - 8: "Stardust Fields" - 9: "Hoohoo Mountain" - 10: "Battle" - 11: "Victory" - 12: "Beanbean Fields" - 13: "Chucklehuck Woods" - 14: "Seabed" - 15: "Beanbean Castle" - 16: "Boss Battle" - 17: "Woohoo Hooniversity" - 18: "Teehee Valley" - 19: "Joke's End" - 20: "Little Fungitown" - 21: "Gwarhar Lagoon" - 22: "Underground" - 23: "Toad Town Square" - 24: "Bowser's Castle" - 25: "Warp Pipe" - 26: "Special Item" - 27: "Royal Welcome" - 28: "Prince Peasley's Theme" - 29: "Cackletta's Theme" - 30: "File Select" - 31: "Hoohoo Village" - 32: "Devastation" - 33: "Panic!" - 34: "Koopa Cruiser" - 35: "Danger!" - 36: "Popple Battle" - 37: "Cackletta Battle" - 38: "Bowletta Battle" - 39: "Ending" - 40: "Credits" - 41: "Title" - 42: "Chateau de Chucklehuck" - 43: "43" - 44: "Final Cackletta Battle" - 45: "45" - 46: "46" - 47: "47" - 48: "Professor E Gadd" - 49: "Ghostly Encounter" - 50: "Bean Time!" -A88J_00: - Name: "Mario & Luigi - Superstar Saga (Japan)" - SongTableOffsets: 0x205060 - SongTableSizes: 418 - VoiceTableOffset: 0x2056E8 - SampleTableOffset: 0x9721A8 - SampleTableSize: 239 - Copy: "A88E_00" -A88P_00: - Name: "Mario & Luigi - Superstar Saga (Europe)" - SongTableOffsets: 0x21DC50 - VoiceTableOffset: 0x21E2AC - SampleTableOffset: 0xA82088 - Copy: "A88E_00" -A84P_00: - Name: "Hamtaro - Rainbow Rescue (Europe)" - AudioEngineVersion: "Hamtaro" - SongTableOffsets: 0x694300 - SongTableSizes: 150 - VoiceTableOffset: 0x694558 - SampleTableOffset: 0xBCD510 - SampleTableSize: 149 - Playlists: - Music: - 1: "Title" - 2: "Menu" - 3: "Stickers" - 4: "Coloring" - 5: "The Clubhouse" - 6: "Ham-Ham Lawn" - 7: "Final Credits" - 8: "Bo" - 9: "Sunny Peak" - 10: "Flower Ranch" - 11: "Clover Elementary" - 12: "Ticky-Ticky Park" - 13: "Tip-Top Fair" - 14: "Taiko Drumming" - 15: "Sparkle Coast" - 16: "Aquarium" - 17: "Hamstarr Manor" - 18: "Puntaros" - 19: "Acorn Trail / Sea Spray Park" - 20: "Rainbow Theater" - 21: "Ham Game #1" - 22: "Ham Game #2" - 23: "Ham Game #3" - 24: "Ham Game #4" - 25: "Credits" - 26: "Ham-Ham Lawn (again)" - 27: "Ham-Ham March" - 28: "That's Not Good..." - 29: "Uh-Oh..." - 30: "Success!" - 31: "Oh No!" - 32: "Headband-Ham" - 33: "Bittersweet" - 34: "We did it!" - 35: "Pigeon Ride" - 36: "Boss" - 37: "Hamstarr" - 38: "Jingle" - 39: "Carrobo" -A84J_00: - Name: "Hamtaro - Rainbow Rescue (Japan)" - SongTableOffsets: 0x4DDE20 - VoiceTableOffset: 0x4DE078 - SampleTableOffset: 0x742FFC - Copy: "A84P_00" -B85A_00: - Name: "Hamtaro - Ham-Ham Games (Japan/USA)" - AudioEngineVersion: "Hamtaro" - SongTableOffsets: 0xDED20 - SongTableSizes: 212 - VoiceTableOffset: 0xDF10C - SampleTableOffset: 0x100000 - SampleTableSize: 368 - Playlists: - Music: - 1: "Title" - 2: "Menu" - 3: "The Clubhouse" - 4: "Cards / Crystal" - 5: "Introduction" - 6: "Credits" - 7: "Hamigo" - 8: "Event Preparation / Results" - 9: "Synchronized Swimming" - 10: "Other Events" - 11: "Marathon #1" - 12: "Marathon #2" - 13: "Marathon #3" - 14: "Marathon #4" - 15: "Marathon #5" - 16: "Results Ceremony" - 17: "Ham Shopping Network" - 18: "Ham Studio News" - 19: "Ham Fortune Telling" - 20: "Ham Studio Shows Introduction" - 21: "BGM #1" - 22: "BGM #2" - 23: "BGM #3" - 24: "BGM #4" - 25: "BGM #5" - 26: "BGM #6" - 27: "BGM #7" - 28: "BGM #8" - 29: "BGM #9" - 30: "BGM #10" - 31: "BGM #11" - 32: "Stadium Events" - 33: "BGM #12" - 34: "BGM #13" - 35: "BGM #14" - 36: "BGM #15" - 37: "BGM #16" - 38: "BGM #17" - 39: "BGM #18" - 40: "BGM #19" - 41: "BGM #20" - 42: "42" - 43: "BGM #21" - 44: "BGM #22" - 45: "BGM #23" - 46: "BGM #24" - 47: "BGM #25" - 48: "BGM #26" - 49: "BGM #27" - 50: "BGM #28" - 51: "BGM #29" - 52: "BGM #30" - 53: "Minigame A" - 54: "Minigame B" - 55: "Carrobo" -B85P_00: - Name: "Hamtaro - Ham-Ham Games (Europe)" - SongTableOffsets: 0xDED20 - VoiceTableOffset: 0xDF10C - SampleTableOffset: 0x100000 - Copy: "B85A_00" diff --git a/VG Music Studio.backup/Config.yaml b/VG Music Studio.backup/Config.yaml deleted file mode 100644 index d4bdf7b8..00000000 --- a/VG Music Studio.backup/Config.yaml +++ /dev/null @@ -1,137 +0,0 @@ -TaskbarProgress: True # True or False # Whether the taskbar will show the song progress -RefreshRate: 30 # RefreshRate >= 1 and RefreshRate <= 1000 # How many times a second the visual updates -CenterIndicators: False # True or False # Whether lines should be drawn for the center of panpot in the visual -PanpotIndicators: False # True or False # Whether lines should be drawn for the track's panpot in the visual -PlaylistMode: "Random" # "Random" or "Sequential" # The way the playlist will behave -PlaylistSongLoops: 0 # Loops >= 0 and Loops <= 9223372036854775807 # How many times a song should loop before fading out -PlaylistFadeOutMilliseconds: 10000 # Milliseconds >= 0 and Milliseconds <= 9223372036854775807 # How many milliseconds it should take to fade out of a song -MiddleCOctave: 4 # Octave >= --128 and Octave <= 127 # The octave that holds middle C. Used in the visual and track viewer -Colors: - 0: {H: 185, S: 240, L: 180} - 1: {H: 183, S: 240, L: 170} - 2: {H: 180, S: 240, L: 157} - 3: {H: 184, S: 240, L: 85} - 4: {H: 171, S: 240, L: 134} - 5: {H: 168, S: 240, L: 159} - 6: {H: 36, S: 240, L: 170} - 7: {H: 15, S: 240, L: 134} - 8: {H: 175, S: 240, L: 200} - 9: {H: 120, S: 240, L: 150} - 10: {H: 114, S: 240, L: 138} - 11: {H: 99, S: 240, L: 171} - 12: {H: 68, S: 240, L: 171} - 13: {H: 83, S: 240, L: 200} - 14: {H: 215, S: 240, L: 104} - 15: {H: 25, S: 240, L: 200} - 16: {H: 224, S: 240, L: 150} - 17: {H: 195, S: 240, L: 120} - 18: {H: 206, S: 240, L: 95} - 19: {H: 218, S: 240, L: 129} - 20: {H: 203, S: 240, L: 180} - 21: {H: 145, S: 240, L: 100} - 22: {H: 140, S: 240, L: 111} - 23: {H: 151, S: 240, L: 120} - 24: {H: 5, S: 240, L: 169} - 25: {H: 6, S: 240, L: 156} - 26: {H: 14, S: 240, L: 164} - 27: {H: 12, S: 240, L: 137} - 28: {H: 8, S: 240, L: 140} - 29: {H: 0, S: 240, L: 123} - 30: {H: 229, S: 240, L: 70} - 31: {H: 239, S: 240, L: 89} - 32: {H: 25, S: 180, L: 160} - 33: {H: 20, S: 180, L: 145} - 34: {H: 17, S: 180, L: 140} - 35: {H: 36, S: 240, L: 163} - 36: {H: 25, S: 180, L: 140} - 37: {H: 25, S: 210, L: 95} - 38: {H: 160, S: 0, L: 180} - 39: {H: 200, S: 240, L: 90} - 40: {H: 195, S: 240, L: 100} - 41: {H: 190, S: 240, L: 93} - 42: {H: 180, S: 240, L: 90} - 43: {H: 170, S: 240, L: 150} - 44: {H: 166, S: 240, L: 89} - 45: {H: 210, S: 240, L: 170} - 46: {H: 214, S: 240, L: 185} - 47: {H: 15, S: 135, L: 135} - 48: {H: 148, S: 240, L: 130} - 49: {H: 173, S: 240, L: 80} - 50: {H: 170, S: 240, L: 95} - 51: {H: 176, S: 240, L: 100} - 52: {H: 26, S: 240, L: 215} - 53: {H: 20, S: 240, L: 210} - 54: {H: 5, S: 240, L: 220} - 55: {H: 6, S: 240, L: 150} - 56: {H: 22, S: 240, L: 134} - 57: {H: 25, S: 240, L: 130} - 58: {H: 40, S: 240, L: 120} - 59: {H: 28, S: 240, L: 122} - 60: {H: 16, S: 240, L: 124} - 61: {H: 11, S: 240, L: 118} - 62: {H: 53, S: 240, L: 158} - 63: {H: 57, S: 240, L: 133} - 64: {H: 30, S: 240, L: 195} - 65: {H: 23, S: 240, L: 182} - 66: {H: 32, S: 240, L: 160} - 67: {H: 32, S: 240, L: 130} - 68: {H: 37, S: 240, L: 135} - 69: {H: 13, S: 240, L: 143} - 70: {H: 134, S: 240, L: 85} - 71: {H: 130, S: 240, L: 95} - 72: {H: 120, S: 240, L: 165} - 73: {H: 126, S: 240, L: 120} - 74: {H: 126, S: 240, L: 100} - 75: {H: 135, S: 240, L: 160} - 76: {H: 118, S: 240, L: 186} - 77: {H: 135, S: 240, L: 102} - 78: {H: 113, S: 240, L: 100} - 79: {H: 70, S: 240, L: 160} - 80: {H: 82, S: 240, L: 132} - 81: {H: 227, S: 240, L: 188} - 82: {H: 103, S: 240, L: 140} - 83: {H: 60, S: 240, L: 165} - 84: {H: 239, S: 240, L: 165} - 85: {H: 123, S: 240, L: 175} - 86: {H: 210, S: 240, L: 145} - 87: {H: 53, S: 240, L: 120} - 88: {H: 110, S: 240, L: 155} - 89: {H: 122, S: 240, L: 205} - 90: {H: 217, S: 240, L: 95} - 91: {H: 142, S: 240, L: 50} - 92: {H: 100, S: 240, L: 90} - 93: {H: 137, S: 240, L: 90} - 94: {H: 188, S: 240, L: 117} - 95: {H: 160, S: 240, L: 210} - 96: {H: 130, S: 240, L: 200} - 97: {H: 202, S: 240, L: 80} - 98: {H: 0, S: 240, L: 160} - 99: {H: 30, S: 240, L: 110} - 100: {H: 130, S: 240, L: 210} - 101: {H: 75, S: 240, L: 75} - 102: {H: 180, S: 240, L: 205} - 103: {H: 27, S: 200, L: 105} - 104: {H: 33, S: 200, L: 145} - 105: {H: 37, S: 220, L: 130} - 106: {H: 45, S: 240, L: 135} - 107: {H: 55, S: 240, L: 175} - 108: {H: 95, S: 240, L: 185} - 109: {H: 53, S: 240, L: 190} - 110: {H: 135, S: 240, L: 120} - 111: {H: 38, S: 240, L: 110} - 112: {H: 220, S: 240, L: 170} - 113: {H: 120, S: 80, L: 150} - 114: {H: 130, S: 120, L: 190} - 115: {H: 0, S: 80, L: 90} - 116: {H: 18, S: 125, L: 130} - 117: {H: 15, S: 70, L: 120} - 118: {H: 200, S: 80, L: 110} - 119: {H: 140, S: 60, L: 180} - 120: {H: 10, S: 240, L: 90} - 121: {H: 123, S: 156, L: 100} - 122: {H: 128, S: 240, L: 100} - 123: {H: 40, S: 240, L: 180} - 124: {H: 239, S: 200, L: 90} - 125: {H: 145, S: 10, L: 155} - 126: {H: 15, S: 80, L: 160} - 127: {H: 160, S: 80, L: 150} \ No newline at end of file diff --git a/VG Music Studio.backup/Core/ADPCMDecoder.cs b/VG Music Studio.backup/Core/ADPCMDecoder.cs deleted file mode 100644 index 9c8a4e94..00000000 --- a/VG Music Studio.backup/Core/ADPCMDecoder.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core -{ - internal class ADPCMDecoder - { - private static readonly short[] _indexTable = new short[8] - { - -1, -1, -1, -1, 2, 4, 6, 8 - }; - private static readonly short[] _stepTable = new short[89] - { - 00007, 00008, 00009, 00010, 00011, 00012, 00013, 00014, - 00016, 00017, 00019, 00021, 00023, 00025, 00028, 00031, - 00034, 00037, 00041, 00045, 00050, 00055, 00060, 00066, - 00073, 00080, 00088, 00097, 00107, 00118, 00130, 00143, - 00157, 00173, 00190, 00209, 00230, 00253, 00279, 00307, - 00337, 00371, 00408, 00449, 00494, 00544, 00598, 00658, - 00724, 00796, 00876, 00963, 01060, 01166, 01282, 01411, - 01552, 01707, 01878, 02066, 02272, 02499, 02749, 03024, - 03327, 03660, 04026, 04428, 04871, 05358, 05894, 06484, - 07132, 07845, 08630, 09493, 10442, 11487, 12635, 13899, - 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, - 32767 - }; - - private readonly byte[] _data; - public short LastSample; - public short StepIndex; - public int DataOffset; - public bool OnSecondNibble; - - public ADPCMDecoder(byte[] data) - { - LastSample = (short)(data[0] | (data[1] << 8)); - StepIndex = (short)((data[2] | (data[3] << 8)) & 0x7F); - DataOffset = 4; - _data = data; - } - - public static short[] ADPCMToPCM16(byte[] data) - { - var decoder = new ADPCMDecoder(data); - short[] buffer = new short[(data.Length - 4) * 2]; - for (int i = 0; i < buffer.Length; i++) - { - buffer[i] = decoder.GetSample(); - } - return buffer; - } - - public short GetSample() - { - int val = (_data[DataOffset] >> (OnSecondNibble ? 4 : 0)) & 0xF; - short step = _stepTable[StepIndex]; - int diff = - (step / 8) + - (step / 4 * (val & 1)) + - (step / 2 * ((val >> 1) & 1)) + - (step * ((val >> 2) & 1)); - - int a = (diff * ((((val >> 3) & 1) == 1) ? -1 : 1)) + LastSample; - if (a < short.MinValue) - { - a = short.MinValue; - } - else if (a > short.MaxValue) - { - a = short.MaxValue; - } - LastSample = (short)a; - - a = StepIndex + _indexTable[val & 7]; - if (a < 0) - { - a = 0; - } - else if (a > 88) - { - a = 88; - } - StepIndex = (short)a; - - if (OnSecondNibble) - { - DataOffset++; - } - OnSecondNibble = !OnSecondNibble; - return LastSample; - } - } -} diff --git a/VG Music Studio.backup/Core/Assembler.cs b/VG Music Studio.backup/Core/Assembler.cs deleted file mode 100644 index 8da670d3..00000000 --- a/VG Music Studio.backup/Core/Assembler.cs +++ /dev/null @@ -1,351 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core -{ - internal sealed class Assembler - { - private class Pair - { - public bool Global; - public int Offset; - } - private class Pointer - { - public string Label; - public int BinaryOffset; - } - private const string _fileErrorFormat = "{0}{3}{3}Error reading file included in line {1}:{3}{2}"; - private const string _mathErrorFormat = "{0}{3}{3}Error parsing value in line {1} (Are you missing a definition?):{3}{2}"; - private const string _cmdErrorFormat = "{0}{3}{3}Unknown command in line {1}:{3}\"{2}\""; - - public int BaseOffset { get; private set; } - private readonly List _loaded = new List(); - private readonly Dictionary _defines; - - private readonly Dictionary _labels = new Dictionary(); - private readonly List _lPointers = new List(); - private readonly List _bytes = new List(); - - public string FileName { get; } - public Endianness Endianness { get; } - public int this[string Label] => _labels[FixLabel(Label)].Offset; - public byte[] Binary => _bytes.ToArray(); - public int BinaryLength => _bytes.Count; - - public Assembler(string fileName, int baseOffset, Endianness endianness, Dictionary initialDefines = null) - { - FileName = fileName; - Endianness = endianness; - _defines = initialDefines ?? new Dictionary(); - Debug.WriteLine(Read(fileName)); - SetBaseOffset(baseOffset); - } - - public void SetBaseOffset(int baseOffset) - { - foreach (Pointer p in _lPointers) - { - // Our example label is SEQ_STUFF at the binary offset 0x1000, curBaseOffset is 0x500, baseOffset is 0x1800 - // There is a pointer (p) to SEQ_STUFF at the binary offset 0x1DFC - int oldPointer = EndianBitConverter.BytesToInt32(Binary, p.BinaryOffset, Endianness); // If there was a pointer to "SEQ_STUFF+4", the pointer would be 0x1504, at binary offset 0x1DFC - int labelOffset = oldPointer - BaseOffset; // Then labelOffset is 0x1004 (SEQ_STUFF+4) - byte[] newPointerBytes = EndianBitConverter.Int32ToBytes(baseOffset + labelOffset, Endianness); // b will contain {0x04, 0x28, 0x00, 0x00} [0x2804] (SEQ_STUFF+4 + baseOffset) - for (int i = 0; i < 4; i++) - { - _bytes[p.BinaryOffset + i] = newPointerBytes[i]; // Copy the new pointer to binary offset 0x1DF4 - } - } - BaseOffset = baseOffset; - } - - public static string FixLabel(string label) - { - string ret = ""; - for (int i = 0; i < label.Length; i++) - { - char c = label[i]; - if ((c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9' && i > 0)) - { - ret += c; - } - else - { - ret += '_'; - } - } - return ret; - } - - // Returns a status - private string Read(string fileName) - { - if (_loaded.Contains(fileName)) - { - return $"{fileName} was already loaded"; - } - - string[] file = File.ReadAllLines(fileName); - _loaded.Add(fileName); - - for (int i = 0; i < file.Length; i++) - { - string line = file[i]; - if (string.IsNullOrWhiteSpace(line)) - { - continue; // Skip empty lines - } - - bool readingCMD = false; // If it's reading the command - string cmd = null; - var args = new List(); - string str = string.Empty; - foreach (char c in line) - { - if (c == '@') // Ignore comments from this point - { - break; - } - else if (c == '.' && cmd == null) - { - readingCMD = true; - } - else if (c == ':') // Labels - { - if (!_labels.ContainsKey(str)) - { - _labels.Add(str, new Pair()); - } - _labels[str].Offset = _bytes.Count; - str = string.Empty; - } - else if (char.IsWhiteSpace(c)) - { - if (readingCMD) // If reading the command, otherwise do nothing - { - cmd = str; - readingCMD = false; - str = string.Empty; - } - } - else if (c == ',') - { - args.Add(str); - str = string.Empty; - } - else - { - str += c; - } - } - if (cmd == null) - { - continue; // Commented line - } - - args.Add(str); // Add last string before the newline - - switch (cmd.ToLower()) - { - case "include": - { - try - { - Read(args[0].Replace("\"", string.Empty)); - } - catch - { - throw new IOException(string.Format(_fileErrorFormat, fileName, i, args[0], Environment.NewLine)); - } - break; - } - case "equ": - { - try - { - _defines.Add(args[0], ParseInt(args[1])); - } - catch - { - throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); - } - break; - } - case "global": - { - if (!_labels.ContainsKey(args[0])) - { - _labels.Add(args[0], new Pair()); - } - _labels[args[0]].Global = true; - break; - } - case "align": - { - int align = ParseInt(args[0]); - for (int a = BinaryLength % align; a < align; a++) - { - _bytes.Add(0); - } - break; - } - case "byte": - { - try - { - foreach (string a in args) - { - _bytes.Add((byte)ParseInt(a)); - } - } - catch - { - throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); - } - break; - } - case "hword": - { - try - { - foreach (string a in args) - { - _bytes.AddRange(EndianBitConverter.Int16ToBytes((short)ParseInt(a), Endianness)); - } - } - catch - { - throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); - } - break; - } - case "int": - case "word": - { - try - { - foreach (string a in args) - { - _bytes.AddRange(EndianBitConverter.Int32ToBytes(ParseInt(a), Endianness)); - } - } - catch - { - throw new ArithmeticException(string.Format(_mathErrorFormat, fileName, i, line, Environment.NewLine)); - } - break; - } - case "end": - { - goto end; - } - case "section": // Ignore - { - break; - } - default: throw new NotSupportedException(string.Format(_cmdErrorFormat, fileName, i, cmd, Environment.NewLine)); - } - } - end: - return $"{fileName} loaded with no issues"; - } - - private static readonly CultureInfo _enUS = new CultureInfo("en-US"); - private int ParseInt(string value) - { - // First try regular values like "40" and "0x20" - if (value.StartsWith("0x") && int.TryParse(value.Substring(2), NumberStyles.HexNumber, _enUS, out int hex)) - { - return hex; - } - if (int.TryParse(value, NumberStyles.Integer, _enUS, out int dec)) - { - return dec; - } - // Then check if it's defined - if (_defines.TryGetValue(value, out int def)) - { - return def; - } - if (_labels.TryGetValue(value, out Pair pair)) - { - _lPointers.Add(new Pointer { Label = value, BinaryOffset = _bytes.Count }); - return pair.Offset; - } - - // Then check if it's math - bool foundMath = false; - string str = string.Empty; - int ret = 0; - bool add = true, // Add first, so the initial value is set - sub = false, - mul = false, - div = false; - for (int i = 0; i < value.Length; i++) - { - char c = value[i]; - - if (char.IsWhiteSpace(c)) // White space does nothing here - { - continue; - } - if (c == '+' || c == '-' || c == '*' || c == '/') - { - if (add) - { - ret += ParseInt(str); - } - else if (sub) - { - ret -= ParseInt(str); - } - else if (mul) - { - ret *= ParseInt(str); - } - else if (div) - { - ret /= ParseInt(str); - } - add = c == '+'; - sub = c == '-'; - mul = c == '*'; - div = c == '/'; - str = string.Empty; - foundMath = true; - } - else - { - str += c; - } - } - if (foundMath) - { - if (add) // Handle last - { - ret += ParseInt(str); - } - else if (sub) - { - ret -= ParseInt(str); - } - else if (mul) - { - ret *= ParseInt(str); - } - else if (div) - { - ret /= ParseInt(str); - } - return ret; - } - throw new ArgumentOutOfRangeException(nameof(value)); - } - } -} diff --git a/VG Music Studio.backup/Core/Config.cs b/VG Music Studio.backup/Core/Config.cs deleted file mode 100644 index 81bfbf62..00000000 --- a/VG Music Studio.backup/Core/Config.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core -{ - internal abstract class Config : IDisposable - { - public class Song - { - public long Index; - public string Name; - - public Song(long index, string name) - { - Index = index; Name = name; - } - - public override bool Equals(object obj) - { - return obj is Song other && other.Index == Index; - } - public override int GetHashCode() - { - return Index.GetHashCode(); - } - public override string ToString() - { - return Name; - } - } - public class Playlist - { - public string Name; - public List Songs; - - public Playlist(string name, IEnumerable songs) - { - Name = name; Songs = songs.ToList(); - } - - public override string ToString() - { - int songCount = Songs.Count; - CultureInfo cul = System.Threading.Thread.CurrentThread.CurrentUICulture; - if (cul.TwoLetterISOLanguageName == "it") // Italian - { - // PlaylistName - (1 Canzone) - // PlaylistName - (2 Canzoni) - return $"{Name} - ({songCount} {(songCount == 1 ? "Canzone" : "Canzoni")})"; - } - else if (cul.TwoLetterISOLanguageName == "es") // Spanish - { - // PlaylistName - (1 Canción) - // PlaylistName - (2 Canciones) - return $"{Name} - ({songCount} {(songCount == 1 ? "Canción" : "Canciones")})"; - } - else // Fallback to en-US - { - // PlaylistName - (1 Song) - // PlaylistName - (2 Songs) - return $"{Name} - ({songCount} {(songCount == 1 ? "Song" : "Songs")})"; - } - } - } - - public List Playlists = new List(); - - public Song GetFirstSong(long index) - { - foreach (Playlist p in Playlists) - { - foreach (Song s in p.Songs) - { - if (s.Index == index) - { - return s; - } - } - } - return null; - } - - public abstract string GetGameName(); - public abstract string GetSongName(long index); - - public virtual void Dispose() { } - } -} diff --git a/VG Music Studio.backup/Core/Engine.cs b/VG Music Studio.backup/Core/Engine.cs deleted file mode 100644 index 57200db2..00000000 --- a/VG Music Studio.backup/Core/Engine.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; - -namespace Kermalis.VGMusicStudio.Core -{ - internal class Engine : IDisposable - { - public enum EngineType : byte - { - None, - GBA_AlphaDream, - GBA_MP2K, - NDS_DSE, - NDS_SDAT - } - - public static Engine Instance { get; private set; } - - public EngineType Type { get; } - public Config Config { get; private set; } - public Mixer Mixer { get; private set; } - public IPlayer Player { get; private set; } - - public Engine(EngineType type, object playerArg) - { - switch (type) - { - case EngineType.GBA_AlphaDream: - { - byte[] rom = (byte[])playerArg; - if (rom.Length > GBA.Utils.CartridgeCapacity) - { - throw new Exception($"The ROM is too large. Maximum size is 0x{GBA.Utils.CartridgeCapacity:X7} bytes."); - } - var config = new GBA.AlphaDream.Config(rom); - Config = config; - var mixer = new GBA.AlphaDream.Mixer(config); - Mixer = mixer; - Player = new GBA.AlphaDream.Player(mixer, config); - break; - } - case EngineType.GBA_MP2K: - { - byte[] rom = (byte[])playerArg; - if (rom.Length > GBA.Utils.CartridgeCapacity) - { - throw new Exception($"The ROM is too large. Maximum size is 0x{GBA.Utils.CartridgeCapacity:X7} bytes."); - } - var config = new GBA.MP2K.Config(rom); - Config = config; - var mixer = new GBA.MP2K.Mixer(config); - Mixer = mixer; - Player = new GBA.MP2K.Player(mixer, config); - break; - } - case EngineType.NDS_DSE: - { - string bgmPath = (string)playerArg; - var config = new NDS.DSE.Config(bgmPath); - Config = config; - var mixer = new NDS.DSE.Mixer(); - Mixer = mixer; - Player = new NDS.DSE.Player(mixer, config); - break; - } - case EngineType.NDS_SDAT: - { - var sdat = (NDS.SDAT.SDAT)playerArg; - var config = new NDS.SDAT.Config(sdat); - Config = config; - var mixer = new NDS.SDAT.Mixer(); - Mixer = mixer; - Player = new NDS.SDAT.Player(mixer, config); - break; - } - default: throw new ArgumentOutOfRangeException(nameof(type)); - } - Type = type; - Instance = this; - } - - public void Dispose() - { - Config.Dispose(); - Config = null; - Mixer.Dispose(); - Mixer = null; - Player.Dispose(); - Player = null; - Instance = null; - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Channel.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Channel.cs deleted file mode 100644 index 4eba3ac1..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/Channel.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal abstract class Channel - { - protected readonly Mixer _mixer; - public EnvelopeState State; - public byte Key; - public bool Stopped; - - protected ADSR _adsr; - - protected byte _velocity; - protected int _pos; - protected float _interPos; - protected float _frequency; - protected byte _leftVol; - protected byte _rightVol; - - protected Channel(Mixer mixer) - { - _mixer = mixer; - } - - public ChannelVolume GetVolume() - { - const float max = 0x10000; - return new ChannelVolume - { - LeftVol = _leftVol * _velocity / max, - RightVol = _rightVol * _velocity / max - }; - } - public void SetVolume(byte vol, sbyte pan) - { - _leftVol = (byte)((vol * (-pan + 0x80)) >> 8); - _rightVol = (byte)((vol * (pan + 0x80)) >> 8); - } - public abstract void SetPitch(int pitch); - - public abstract void Process(float[] buffer); - } - internal class PCMChannel : Channel - { - private SampleHeader _sampleHeader; - private int _sampleOffset; - private bool _bFixed; - - public PCMChannel(Mixer mixer) : base(mixer) { } - public void Init(byte key, ADSR adsr, int sampleOffset, bool bFixed) - { - _velocity = adsr.A; - State = EnvelopeState.Attack; - _pos = 0; _interPos = 0; - Key = key; - _adsr = adsr; - _sampleHeader = _mixer.Config.Reader.ReadObject(sampleOffset); - _sampleOffset = sampleOffset + 0x10; - _bFixed = bFixed; - Stopped = false; - } - - public override void SetPitch(int pitch) - { - if (_sampleHeader != null) - { - _frequency = (_sampleHeader.SampleRate >> 10) * (float)Math.Pow(2, ((Key - 60) / 12f) + (pitch / 768f)); - } - } - - private void StepEnvelope() - { - switch (State) - { - case EnvelopeState.Attack: - { - int nextVel = _velocity + _adsr.A; - if (nextVel >= 0xFF) - { - State = EnvelopeState.Decay; - _velocity = 0xFF; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Decay: - { - int nextVel = (_velocity * _adsr.D) >> 8; - if (nextVel <= _adsr.S) - { - State = EnvelopeState.Sustain; - _velocity = _adsr.S; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Release: - { - int next = (_velocity * _adsr.R) >> 8; - if (next < 0) - { - next = 0; - } - _velocity = (byte)next; - break; - } - } - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - - ChannelVolume vol = GetVolume(); - float interStep = (_bFixed ? _sampleHeader.SampleRate >> 10 : _frequency) * _mixer.SampleRateReciprocal; - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = (_mixer.Config.ROM[_pos + _sampleOffset] - 0x80) / (float)0x80; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos += posDelta; - if (_pos >= _sampleHeader.Length) - { - if (_sampleHeader.DoesLoop == 0x40000000) - { - _pos = _sampleHeader.LoopOffset; - } - else - { - Stopped = true; - break; - } - } - } while (--samplesPerBuffer > 0); - } - } - internal class SquareChannel : Channel - { - private float[] _pat; - - public SquareChannel(Mixer mixer) : base(mixer) { } - public void Init(byte key, ADSR env, byte vol, sbyte pan, int pitch) - { - _pat = MP2K.Utils.SquareD50; // TODO - Key = key; - _adsr = env; - SetVolume(vol, pan); - SetPitch(pitch); - State = EnvelopeState.Attack; - } - - public override void SetPitch(int pitch) - { - _frequency = 3520 * (float)Math.Pow(2, ((Key - 69) / 12f) + (pitch / 768f)); - } - - private void StepEnvelope() - { - switch (State) - { - case EnvelopeState.Attack: - { - int next = _velocity + _adsr.A; - if (next >= 0xF) - { - State = EnvelopeState.Decay; - _velocity = 0xF; - } - else - { - _velocity = (byte)next; - } - break; - } - case EnvelopeState.Decay: - { - int next = (_velocity * _adsr.D) >> 3; - if (next <= _adsr.S) - { - State = EnvelopeState.Sustain; - _velocity = _adsr.S; - } - else - { - _velocity = (byte)next; - } - break; - } - case EnvelopeState.Release: - { - int next = (_velocity * _adsr.R) >> 3; - if (next < 0) - { - next = 0; - } - _velocity = (byte)next; - break; - } - } - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - - ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _pat[_pos]; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos = (_pos + posDelta) & 0x7; - } while (--samplesPerBuffer > 0); - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Commands.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Commands.cs deleted file mode 100644 index 50991f73..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/Commands.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class FinishCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Finish"; - public string Arguments => string.Empty; - } - internal class FreeNoteHamtaroCommand : ICommand // TODO: When optimization comes, get rid of free note vs note and just have the label differ - { - public Color Color => Color.SkyBlue; - public string Label => "Free Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Volume} {Duration}"; - - public byte Key { get; set; } - public byte Volume { get; set; } - public byte Duration { get; set; } - } - internal class FreeNoteMLSSCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Free Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Duration}"; - - public byte Key { get; set; } - public byte Duration { get; set; } - } - internal class JumpCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Jump"; - public string Arguments => $"0x{Offset:X7}"; - - public int Offset { get; set; } - } - internal class NoteHamtaroCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Volume} {Duration}"; - - public byte Key { get; set; } - public byte Volume { get; set; } - public byte Duration { get; set; } - } - internal class NoteMLSSCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Duration}"; - - public byte Key { get; set; } - public byte Duration { get; set; } - } - internal class PanpotCommand : ICommand - { - public Color Color => Color.GreenYellow; - public string Label => "Panpot"; - public string Arguments => Panpot.ToString(); - - public sbyte Panpot { get; set; } - } - internal class PitchBendCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend"; - public string Arguments => Bend.ToString(); - - public sbyte Bend { get; set; } - } - internal class PitchBendRangeCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend Range"; - public string Arguments => Range.ToString(); - - public byte Range { get; set; } - } - internal class RestCommand : ICommand - { - public Color Color => Color.PaleVioletRed; - public string Label => "Rest"; - public string Arguments => Rest.ToString(); - - public byte Rest { get; set; } - } - internal class TrackTempoCommand : ICommand - { - public Color Color => Color.DeepSkyBlue; - public string Label => "Track Tempo"; - public string Arguments => Tempo.ToString(); - - public byte Tempo { get; set; } - } - internal class VoiceCommand : ICommand - { - public Color Color => Color.DarkSalmon; - public string Label => "Voice"; - public string Arguments => Voice.ToString(); - - public byte Voice { get; set; } - } - internal class VolumeCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Volume"; - public string Arguments => Volume.ToString(); - - public byte Volume { get; set; } - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Config.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Config.cs deleted file mode 100644 index c1689303..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/Config.cs +++ /dev/null @@ -1,220 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using YamlDotNet.RepresentationModel; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class Config : Core.Config - { - public readonly byte[] ROM; - public readonly EndianBinaryReader Reader; - public readonly string GameCode; - public readonly byte Version; - - public readonly string Name; - public readonly AudioEngineVersion AudioEngineVersion; - public readonly int[] SongTableOffsets; - public readonly long[] SongTableSizes; - public readonly int VoiceTableOffset; - public readonly int SampleTableOffset; - public readonly long SampleTableSize; - - public Config(byte[] rom) - { - const string configFile = "AlphaDream.yaml"; - using (StreamReader fileStream = File.OpenText(Util.Utils.CombineWithBaseDirectory(configFile))) - { - string gcv = string.Empty; - try - { - ROM = rom; - Reader = new EndianBinaryReader(new MemoryStream(rom)); - GameCode = Reader.ReadString(4, false, 0xAC); - Version = Reader.ReadByte(0xBC); - gcv = $"{GameCode}_{Version:X2}"; - var yaml = new YamlStream(); - yaml.Load(fileStream); - - var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; - YamlMappingNode game; - try - { - game = (YamlMappingNode)mapping.Children.GetValue(gcv); - } - catch (BetterKeyNotFoundException) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); - } - - YamlNode nameNode = null, - audioEngineVersionNode = null, - songTableOffsetsNode = null, - voiceTableOffsetNode = null, - sampleTableOffsetNode = null, - songTableSizesNode = null, - sampleTableSizeNode = null; - void Load(YamlMappingNode gameToLoad) - { - if (gameToLoad.Children.TryGetValue("Copy", out YamlNode node)) - { - YamlMappingNode copyGame; - try - { - copyGame = (YamlMappingNode)mapping.Children.GetValue(node); - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); - } - Load(copyGame); - } - if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) - { - nameNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(AudioEngineVersion), out node)) - { - audioEngineVersionNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) - { - songTableOffsetsNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) - { - songTableSizesNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(VoiceTableOffset), out node)) - { - voiceTableOffsetNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SampleTableOffset), out node)) - { - sampleTableOffsetNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SampleTableSize), out node)) - { - sampleTableSizeNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) - { - var playlists = (YamlMappingNode)node; - foreach (KeyValuePair kvp in playlists) - { - string name = kvp.Key.ToString(); - var songs = new List(); - foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) - { - long songIndex = Util.Utils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, long.MaxValue); - if (songs.Any(s => s.Index == songIndex)) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); - } - songs.Add(new Song(songIndex, song.Value.ToString())); - } - Playlists.Add(new Playlist(name, songs)); - } - } - } - - Load(game); - - if (nameNode == null) - { - throw new BetterKeyNotFoundException(nameof(Name), null); - } - Name = nameNode.ToString(); - if (audioEngineVersionNode == null) - { - throw new BetterKeyNotFoundException(nameof(AudioEngineVersion), null); - } - AudioEngineVersion = Util.Utils.ParseEnum(nameof(AudioEngineVersion), audioEngineVersionNode.ToString()); - if (songTableOffsetsNode == null) - { - throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); - } - string[] songTables = songTableOffsetsNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); - int numSongTables = songTables.Length; - if (numSongTables == 0) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); - } - if (songTableSizesNode == null) - { - throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); - } - string[] songTableSizes = songTableSizesNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); - if (songTableSizes.Length != numSongTables) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); - } - SongTableOffsets = new int[numSongTables]; - SongTableSizes = new long[numSongTables]; - int maxOffset = rom.Length - 1; - for (int i = 0; i < numSongTables; i++) - { - SongTableOffsets[i] = (int)Util.Utils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); - SongTableSizes[i] = Util.Utils.ParseValue(nameof(SongTableSizes), songTableSizes[i], 1, maxOffset); - } - if (voiceTableOffsetNode == null) - { - throw new BetterKeyNotFoundException(nameof(VoiceTableOffset), null); - } - VoiceTableOffset = (int)Util.Utils.ParseValue(nameof(VoiceTableOffset), voiceTableOffsetNode.ToString(), 0, maxOffset); - if (sampleTableOffsetNode == null) - { - throw new BetterKeyNotFoundException(nameof(SampleTableOffset), null); - } - SampleTableOffset = (int)Util.Utils.ParseValue(nameof(SampleTableOffset), sampleTableOffsetNode.ToString(), 0, maxOffset); - if (sampleTableSizeNode == null) - { - throw new BetterKeyNotFoundException(nameof(SampleTableSize), null); - } - SampleTableSize = Util.Utils.ParseValue(nameof(SampleTableSize), sampleTableSizeNode.ToString(), 0, maxOffset); - - // The complete playlist - if (!Playlists.Any(p => p.Name == "Music")) - { - Playlists.Insert(0, new Playlist(Strings.PlaylistMusic, Playlists.SelectMany(p => p.Songs).Distinct().OrderBy(s => s.Index))); - } - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); - } - catch (InvalidValueException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + ex.Message)); - } - catch (YamlDotNet.Core.YamlException ex) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + ex.Message)); - } - } - } - - public override string GetGameName() - { - return Name; - } - public override string GetSongName(long index) - { - Song s = GetFirstSong(index); - if (s != null) - { - return s.Name; - } - return index.ToString(); - } - - public override void Dispose() - { - Reader.Dispose(); - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Enums.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Enums.cs deleted file mode 100644 index 7c1a9e14..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/Enums.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal enum AudioEngineVersion : byte - { - Hamtaro, - MLSS - } - - internal enum EnvelopeState : byte - { - Attack, - Decay, - Sustain, - Release - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Mixer.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Mixer.cs deleted file mode 100644 index de980045..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/Mixer.cs +++ /dev/null @@ -1,137 +0,0 @@ -using NAudio.Wave; -using System; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class Mixer : Core.Mixer - { - public readonly float SampleRateReciprocal; - private readonly float _samplesReciprocal; - public readonly int SamplesPerBuffer; - private bool _isFading; - private long _fadeMicroFramesLeft; - private float _fadePos; - private float _fadeStepPerMicroframe; - - public readonly Config Config; - private readonly WaveBuffer _audio; - private readonly float[][] _trackBuffers = new float[Player.NumTracks][]; - private readonly BufferedWaveProvider _buffer; - - public Mixer(Config config) - { - Config = config; - const int sampleRate = 13379; // TODO: Actual value unknown - SamplesPerBuffer = 224; // TODO - SampleRateReciprocal = 1f / sampleRate; - _samplesReciprocal = 1f / SamplesPerBuffer; - - int amt = SamplesPerBuffer * 2; - _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; - for (int i = 0; i < Player.NumTracks; i++) - { - _trackBuffers[i] = new float[amt]; - } - _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, 2)) // TODO - { - DiscardOnBufferOverflow = true, - BufferLength = SamplesPerBuffer * 64 - }; - Init(_buffer); - } - public override void Dispose() - { - base.Dispose(); - CloseWaveWriter(); - } - - public void BeginFadeIn() - { - _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * Utils.AGB_FPS); - _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; - _isFading = true; - } - public void BeginFadeOut() - { - _fadePos = 1f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * Utils.AGB_FPS); - _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; - _isFading = true; - } - public bool IsFading() - { - return _isFading; - } - public bool IsFadeDone() - { - return _isFading && _fadeMicroFramesLeft == 0; - } - public void ResetFade() - { - _isFading = false; - _fadeMicroFramesLeft = 0; - } - - private WaveFileWriter _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter?.Dispose(); - } - public void Process(Track[] tracks, bool output, bool recording) - { - _audio.Clear(); - float masterStep; - float masterLevel; - if (_isFading && _fadeMicroFramesLeft == 0) - { - masterStep = 0; - masterLevel = 0; - } - else - { - float fromMaster = 1f; - float toMaster = 1f; - if (_fadeMicroFramesLeft > 0) - { - const float scale = 10f / 6f; - fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadePos += _fadeStepPerMicroframe; - toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadeMicroFramesLeft--; - } - masterStep = (toMaster - fromMaster) * _samplesReciprocal; - masterLevel = fromMaster; - } - for (int i = 0; i < Player.NumTracks; i++) - { - Track track = tracks[i]; - if (track.Enabled && track.NoteDuration != 0 && !track.Channel.Stopped && !Mutes[i]) - { - float level = masterLevel; - float[] buf = _trackBuffers[i]; - Array.Clear(buf, 0, buf.Length); - track.Channel.Process(buf); - for (int j = 0; j < SamplesPerBuffer; j++) - { - _audio.FloatBuffer[j * 2] += buf[j * 2] * level; - _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; - level += masterStep; - } - } - } - if (output) - { - _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); - } - if (recording) - { - _waveWriter.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); - } - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Player.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Player.cs deleted file mode 100644 index d075b8cc..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/Player.cs +++ /dev/null @@ -1,696 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class Player : IPlayer - { - public const int NumTracks = 12; // 8 PCM, 4 PSG - private readonly Track[] _tracks = new Track[NumTracks]; - private readonly Mixer _mixer; - private readonly Config _config; - private readonly TimeBarrier _time; - private Thread _thread; - private byte _tempo; - private int _tempoStack; - private long _elapsedLoops; - - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; - - public PlayerState State { get; private set; } - public event SongEndedEvent SongEnded; - - public Player(Mixer mixer, Config config) - { - for (byte i = 0; i < NumTracks; i++) - { - _tracks[i] = new Track(i, mixer); - } - _mixer = mixer; - _config = config; - - _time = new TimeBarrier(Utils.AGB_FPS); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "AlphaDream Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) - { - _thread.Join(); - } - } - - private void InitEmulation() - { - _tempo = 120; // Player tempo is set to 75 on init, but I did not separate player and track tempo yet - _tempoStack = 0; - _elapsedLoops = 0; - ElapsedTicks = 0; - _mixer.ResetFade(); - for (int i = 0; i < NumTracks; i++) - { - _tracks[i].Init(); - } - } - private void SetTicks() - { - MaxTicks = 0; - bool u = false; - for (int trackIndex = 0; trackIndex < NumTracks; trackIndex++) - { - if (Events[trackIndex] == null) - { - continue; - } - - Events[trackIndex] = Events[trackIndex].OrderBy(e => e.Offset).ToList(); - List evs = Events[trackIndex]; - Track track = _tracks[trackIndex]; - track.Init(); - ElapsedTicks = 0; - while (true) - { - SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); - if (e.Ticks.Count > 0) - { - break; - } - - e.Ticks.Add(ElapsedTicks); - ExecuteNext(track, ref u); - if (track.Stopped) - { - break; - } - - ElapsedTicks += track.Rest; - track.Rest = 0; - } - if (ElapsedTicks > MaxTicks) - { - _longestTrack = trackIndex; - MaxTicks = ElapsedTicks; - } - track.NoteDuration = 0; - } - } - public void LoadSong(long index) - { - int songOffset = _config.Reader.ReadInt32(_config.SongTableOffsets[0] + (index * 4)); - if (songOffset == 0) - { - Events = null; - } - else - { - Events = new List[NumTracks]; - songOffset -= Utils.CartridgeOffset; - ushort trackBits = _config.Reader.ReadUInt16(songOffset); - for (int i = 0, usedTracks = 0; i < NumTracks; i++) - { - Track track = _tracks[i]; - if ((trackBits & (1 << i)) == 0) - { - track.Enabled = false; - track.StartOffset = 0; - } - else - { - track.Enabled = true; - Events[i] = new List(); - bool EventExists(long offset) - { - return Events[i].Any(e => e.Offset == offset); - } - - AddEvents(track.StartOffset = songOffset + _config.Reader.ReadInt16(songOffset + 2 + (2 * usedTracks++))); - void AddEvents(int startOffset) - { - _config.Reader.BaseStream.Position = startOffset; - bool cont = true; - while (cont) - { - long offset = _config.Reader.BaseStream.Position; - void AddEvent(ICommand command) - { - Events[i].Add(new SongEvent(offset, command)); - } - byte cmd = _config.Reader.ReadByte(); - switch (cmd) - { - case 0x00: - { - byte keyArg = _config.Reader.ReadByte(); - switch (_config.AudioEngineVersion) - { - case AudioEngineVersion.Hamtaro: - { - byte volume = _config.Reader.ReadByte(); - byte duration = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new FreeNoteHamtaroCommand { Key = (byte)(keyArg - 0x80), Volume = volume, Duration = duration }); - } - break; - } - case AudioEngineVersion.MLSS: - { - byte duration = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new FreeNoteMLSSCommand { Key = (byte)(keyArg - 0x80), Duration = duration }); - } - break; - } - } - break; - } - case 0xF0: - { - byte voice = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = voice }); - } - break; - } - case 0xF1: - { - byte volume = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VolumeCommand { Volume = volume }); - } - break; - } - case 0xF2: - { - byte panArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = (sbyte)(panArg - 0x80) }); - } - break; - } - case 0xF4: - { - byte range = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendRangeCommand { Range = range }); - } - break; - } - case 0xF5: - { - sbyte bend = _config.Reader.ReadSByte(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = bend }); - } - break; - } - case 0xF6: - { - byte rest = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = rest }); - } - break; - } - case 0xF8: - { - short jumpOffset = _config.Reader.ReadInt16(); - if (!EventExists(offset)) - { - int off = (int)(_config.Reader.BaseStream.Position + jumpOffset); - AddEvent(new JumpCommand { Offset = off }); - if (!EventExists(off)) - { - AddEvents(off); - } - } - cont = false; - break; - } - case 0xF9: - { - byte tempoArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new TrackTempoCommand { Tempo = tempoArg }); - } - break; - } - case 0xFF: - { - if (!EventExists(offset)) - { - AddEvent(new FinishCommand()); - } - cont = false; - break; - } - default: - { - if (cmd <= 0xDF) - { - byte key = _config.Reader.ReadByte(); - switch (_config.AudioEngineVersion) - { - case AudioEngineVersion.Hamtaro: - { - byte volume = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new NoteHamtaroCommand { Key = key, Volume = volume, Duration = cmd }); - } - break; - } - case AudioEngineVersion.MLSS: - { - if (!EventExists(offset)) - { - AddEvent(new NoteMLSSCommand { Key = key, Duration = cmd }); - } - break; - } - } - } - else - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, i, offset, cmd)); - } - break; - } - } - } - } - } - } - SetTicks(); - } - } - public void SetCurrentPosition(long ticks) - { - if (Events == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - bool u = false; - while (true) - { - if (ElapsedTicks == ticks) - { - goto finish; - } - - while (_tempoStack >= 75) - { - _tempoStack -= 75; - for (int trackIndex = 0; trackIndex < NumTracks; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (track.Enabled && !track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref u); - } - } - } - ElapsedTicks++; - if (ElapsedTicks == ticks) - { - goto finish; - } - } - _tempoStack += _tempo; - } - finish: - for (int i = 0; i < NumTracks; i++) - { - _tracks[i].NoteDuration = 0; - } - Pause(); - } - } - public void Play() - { - if (Events == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); - } - } - public void Pause() - { - if (State == PlayerState.Playing) - { - State = PlayerState.Paused; - WaitThread(); - } - else if (State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.Playing; - CreateThread(); - } - } - public void Stop() - { - if (State == PlayerState.Playing || State == PlayerState.Paused) - { - State = PlayerState.Stopped; - WaitThread(); - } - } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() - { - if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.ShutDown; - WaitThread(); - } - } - public void GetSongState(UI.SongInfoControl.SongInfo info) - { - info.Tempo = _tempo; - for (int i = 0; i < NumTracks; i++) - { - Track track = _tracks[i]; - if (track.Enabled) - { - UI.SongInfoControl.SongInfo.Track tin = info.Tracks[i]; - tin.Position = track.DataOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.Type = track.Type; - tin.Volume = track.Volume; - tin.PitchBend = track.GetPitch(); - tin.Panpot = track.Panpot; - if (track.NoteDuration != 0 && !track.Channel.Stopped) - { - tin.Keys[0] = track.Channel.Key; - ChannelVolume vol = track.Channel.GetVolume(); - tin.LeftVolume = vol.LeftVol; - tin.RightVolume = vol.RightVol; - } - else - { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; - } - } - } - } - - private VoiceEntry GetVoiceEntry(byte voice, byte key) - { - int vto = _config.VoiceTableOffset; - short voiceOffset = _config.Reader.ReadInt16(vto + (voice * 2)); - short nextVoiceOffset = _config.Reader.ReadInt16(vto + ((voice + 1) * 2)); - if (voiceOffset == nextVoiceOffset) - { - return null; - } - else - { - long pos = vto + voiceOffset; // Prevent object creation in the last iteration - VoiceEntry e = _config.Reader.ReadObject(pos); - while (e.MinKey > key || e.MaxKey < key) - { - pos += 8; - if (pos == nextVoiceOffset) - { - return null; - } - e = _config.Reader.ReadObject(); - } - return e; - } - } - private void PlayNote(Track track, byte key, byte duration) - { - VoiceEntry entry = GetVoiceEntry(track.Voice, key); - if (entry != null) - { - track.NoteDuration = duration; - if (track.Index >= 8) - { - // TODO: "Sample" byte in VoiceEntry - ((SquareChannel)track.Channel).Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, track.Volume, track.Panpot, track.GetPitch()); - } - else - { - int sto = _config.SampleTableOffset; - int sampleOffset = _config.Reader.ReadInt32(sto + (entry.Sample * 4)); // Some entries are 0. If you play them, are they silent, or does it not care if they are 0? - ((PCMChannel)track.Channel).Init(key, new ADSR { A = 0xFF, D = 0x00, S = 0xFF, R = 0x00 }, sto + sampleOffset, entry.IsFixedFrequency == 0x80); - track.Channel.SetVolume(track.Volume, track.Panpot); - track.Channel.SetPitch(track.GetPitch()); - } - } - } - private void ExecuteNext(Track track, ref bool update) - { - byte cmd = _config.ROM[track.DataOffset++]; - switch (cmd) - { - case 0x00: // Free Note - { - byte key = (byte)(_config.ROM[track.DataOffset++] - 0x80); - if (_config.AudioEngineVersion == AudioEngineVersion.Hamtaro) - { - track.Volume = _config.ROM[track.DataOffset++]; - update = true; - } - byte duration = _config.ROM[track.DataOffset++]; - track.Rest += duration; - if (track.PrevCommand == 0 && track.Channel.Key == key) - { - track.NoteDuration += duration; - } - else - { - PlayNote(track, key, duration); - } - break; - } - case 0xF0: // Voice - { - track.Voice = _config.ROM[track.DataOffset++]; - break; - } - case 0xF1: // Volume - { - track.Volume = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xF2: // Panpot - { - track.Panpot = (sbyte)(_config.ROM[track.DataOffset++] - 0x80); - update = true; - break; - } - case 0xF4: // Pitch Bend Range - { - track.PitchBendRange = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xF5: // Pitch Bend - { - track.PitchBend = (sbyte)_config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xF6: // Rest - { - track.Rest = _config.ROM[track.DataOffset++]; - break; - } - case 0xF8: // Jump - { - short ofs = (short)(_config.ROM[track.DataOffset++] | (_config.ROM[track.DataOffset++] << 8)); // Cast to short is necessary - track.DataOffset += ofs; - break; - } - case 0xF9: // Track Tempo - { - _tempo = _config.ROM[track.DataOffset++]; - break; - } - case 0xFF: // Finish - { - track.Stopped = true; - break; - } - default: - { - if (cmd <= 0xDF) // Note - { - byte key = _config.ROM[track.DataOffset++]; - if (_config.AudioEngineVersion == AudioEngineVersion.Hamtaro) - { - track.Volume = _config.ROM[track.DataOffset++]; - update = true; - } - track.Rest += cmd; - if (track.PrevCommand == 0 && track.Channel.Key == key) - { - track.NoteDuration += cmd; - } - else - { - PlayNote(track, key, cmd); - } - } - break; - } - } - - track.PrevCommand = cmd; - } - - private void Tick() - { - _time.Start(); - while (true) - { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - goto stop; - } - - void MixerProcess() - { - _mixer.Process(_tracks, playing, recording); - } - - while (_tempoStack >= 75) - { - _tempoStack -= 75; - bool allDone = true; - for (int trackIndex = 0; trackIndex < NumTracks; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (track.Enabled) - { - byte prevDuration = track.NoteDuration; - track.Tick(); - bool update = false; - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref update); - } - if (trackIndex == _longestTrack) - { - if (ElapsedTicks == MaxTicks) - { - if (!track.Stopped) - { - List evs = Events[trackIndex]; - for (int i = 0; i < evs.Count; i++) - { - SongEvent ev = evs[i]; - if (ev.Offset == track.DataOffset) - { - ElapsedTicks = ev.Ticks[0] - track.Rest; - break; - } - } - _elapsedLoops++; - if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) - { - _mixer.BeginFadeOut(); - } - } - } - else - { - ElapsedTicks++; - } - } - if (prevDuration == 1 && track.NoteDuration == 0) // Note was not renewed - { - track.Channel.State = EnvelopeState.Release; - } - if (!track.Stopped) - { - allDone = false; - } - if (track.NoteDuration != 0) - { - allDone = false; - if (update) - { - track.Channel.SetVolume(track.Volume, track.Panpot); - track.Channel.SetPitch(track.GetPitch()); - } - } - } - } - if (_mixer.IsFadeDone()) - { - allDone = true; - } - if (allDone) - { - MixerProcess(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - } - } - _tempoStack += _tempo; - MixerProcess(); - if (playing) - { - _time.Wait(); - } - } - stop: - _time.Stop(); - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs deleted file mode 100644 index b281c3d9..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_DLS.cs +++ /dev/null @@ -1,180 +0,0 @@ -using Kermalis.DLS2; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal sealed class SoundFontSaver_DLS - { - // Since every key will use the same articulation data, just store one instance - private static readonly Level2ArticulatorChunk _art2 = new Level2ArticulatorChunk() - { - new Level2ArticulatorConnectionBlock() { Destination = Level2ArticulatorDestination.LFOFrequency, Scale = 2786 }, - new Level2ArticulatorConnectionBlock() { Destination = Level2ArticulatorDestination.VIBFrequency, Scale = 2786 }, - new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.KeyNumber, Destination = Level2ArticulatorDestination.Pitch }, - new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.Modulation_CC1, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, - new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Vibrato, Control = Level2ArticulatorSource.ChannelPressure, Destination = Level2ArticulatorDestination.Pitch, BipolarSource = true, Scale = 0x320000 }, - new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Pan_CC10, Destination = Level2ArticulatorDestination.Pan, BipolarSource = true, Scale = 0xFE0000 }, - new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.ChorusSend_CC91, Destination = Level2ArticulatorDestination.Reverb, Scale = 0xC80000 }, - new Level2ArticulatorConnectionBlock() { Source = Level2ArticulatorSource.Reverb_SendCC93, Destination = Level2ArticulatorDestination.Chorus, Scale = 0xC80000 } - }; - - public static void Save(Config config, string path) - { - var dls = new DLS(); - AddInfo(config, dls); - Dictionary sampleDict = AddSamples(config, dls); - AddInstruments(config, dls, sampleDict); - dls.Save(path); - } - - private static void AddInfo(Config config, DLS dls) - { - var info = new ListChunk("INFO"); - dls.Add(info); - info.Add(new InfoSubChunk("INAM", config.Name)); - //info.Add(new InfoSubChunk("ICOP", config.Creator)); - info.Add(new InfoSubChunk("IENG", "Kermalis")); - info.Add(new InfoSubChunk("ISFT", Util.Utils.ProgramName)); - } - - private static Dictionary AddSamples(Config config, DLS dls) - { - ListChunk waves = dls.WavePool; - var sampleDict = new Dictionary((int)config.SampleTableSize); - for (int i = 0; i < config.SampleTableSize; i++) - { - int ofs = config.Reader.ReadInt32(config.SampleTableOffset + (i * 4)); - if (ofs == 0) - { - continue; // Skip null samples - } - - ofs += config.SampleTableOffset; - SampleHeader sh = config.Reader.ReadObject(ofs); - - // Create format chunk - var fmt = new FormatChunk(WaveFormat.PCM); - fmt.WaveInfo.Channels = 1; - fmt.WaveInfo.SamplesPerSec = (uint)(sh.SampleRate >> 10); - fmt.WaveInfo.AvgBytesPerSec = fmt.WaveInfo.SamplesPerSec; - fmt.WaveInfo.BlockAlign = 1; - fmt.FormatInfo.BitsPerSample = 8; - // Create wave sample chunk and add loop if there is one - var wsmp = new WaveSampleChunk() - { - UnityNote = 60, - Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression - }; - if (sh.DoesLoop == 0x40000000) - { - wsmp.Loop = new WaveSampleLoop - { - LoopStart = (uint)sh.LoopOffset, - LoopLength = (uint)(sh.Length - sh.LoopOffset), - LoopType = LoopType.Forward - }; - } - // Get PCM sample - byte[] pcm = new byte[sh.Length]; - System.Array.Copy(config.ROM, ofs + 0x10, pcm, 0, sh.Length); - - // Add - int dlsIndex = waves.Count; - waves.Add(new ListChunk("wave") - { - fmt, - wsmp, - new DataChunk(pcm), - new ListChunk("INFO") - { - new InfoSubChunk("INAM", $"Sample {i}") - } - }); - sampleDict.Add(i, (wsmp, dlsIndex)); - } - return sampleDict; - } - - private static void AddInstruments(Config config, DLS dls, Dictionary sampleDict) - { - ListChunk lins = dls.InstrumentList; - for (int v = 0; v < 256; v++) - { - short off = config.Reader.ReadInt16(config.VoiceTableOffset + (v * 2)); - short nextOff = config.Reader.ReadInt16(config.VoiceTableOffset + ((v + 1) * 2)); - int numEntries = (nextOff - off) / 8; // Each entry is 8 bytes - if (numEntries == 0) - { - continue; // Skip empty entries - } - - var ins = new ListChunk("ins "); - ins.Add(new InstrumentHeaderChunk - { - NumRegions = (uint)numEntries, - Locale = new MIDILocale(0, (byte)(v / 128), false, (byte)(v % 128)) - }); - var lrgn = new ListChunk("lrgn"); - ins.Add(lrgn); - ins.Add(new ListChunk("INFO") - { - new InfoSubChunk("INAM", $"Instrument {v}") - }); - lins.Add(ins); - for (int e = 0; e < numEntries; e++) - { - VoiceEntry entry = config.Reader.ReadObject(config.VoiceTableOffset + off + (e * 8)); - // Sample - if (entry.Sample >= config.SampleTableSize) - { - Debug.WriteLine(string.Format("Voice {0} uses an invalid sample id ({1})", v, entry.Sample)); - continue; - } - if (!sampleDict.TryGetValue(entry.Sample, out (WaveSampleChunk, int) value)) - { - Debug.WriteLine(string.Format("Voice {0} uses a null sample id ({1})", v, entry.Sample)); - continue; - } - void Add(ushort low, ushort high, ushort baseKey) - { - var rgnh = new RegionHeaderChunk(); - rgnh.KeyRange.Low = low; - rgnh.KeyRange.High = high; - lrgn.Add(new ListChunk("rgn2") - { - rgnh, - new WaveSampleChunk() - { - UnityNote = baseKey, - Options = WaveSampleOptions.NoTruncation | WaveSampleOptions.NoCompression, - Loop = value.Item1.Loop - }, - new WaveLinkChunk() - { - Channels = WaveLinkChannels.Left, - TableIndex = (uint)value.Item2 - }, - new ListChunk("lar2") - { - _art2 - } - }); - } - // Fixed frequency - Since DLS does not support it, we need to manually add every key with its own base note - if (entry.IsFixedFrequency == 0x80) - { - for (ushort i = entry.MinKey; i <= entry.MaxKey; i++) - { - Add(i, i, i); - } - } - else - { - Add(entry.MinKey, entry.MaxKey, 60); - } - } - } - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs deleted file mode 100644 index 6e0aa028..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/SoundFontSaver_SF2.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Kermalis.SoundFont2; -using Kermalis.VGMusicStudio.Util; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal sealed class SoundFontSaver_SF2 - { - public static void Save(Config config, string path) - { - var sf2 = new SF2(); - AddInfo(config, sf2); - Dictionary sampleDict = AddSamples(config, sf2); - AddInstruments(config, sf2, sampleDict); - sf2.Save(path); - } - - private static void AddInfo(Config config, SF2 sf2) - { - sf2.InfoChunk.Bank = config.Name; - //sf2.InfoChunk.Copyright = config.Creator; - sf2.InfoChunk.Tools = Util.Utils.ProgramName + " by Kermalis"; - } - - private static Dictionary AddSamples(Config config, SF2 sf2) - { - var sampleDict = new Dictionary((int)config.SampleTableSize); - for (int i = 0; i < config.SampleTableSize; i++) - { - int ofs = config.Reader.ReadInt32(config.SampleTableOffset + (i * 4)); - if (ofs == 0) - { - continue; - } - - ofs += config.SampleTableOffset; - SampleHeader sh = config.Reader.ReadObject(ofs); - - short[] pcm16 = SampleUtils.PCMU8ToPCM16(config.ROM, ofs + 0x10, sh.Length); - int sf2Index = (int)sf2.AddSample(pcm16, $"Sample {i}", sh.DoesLoop == 0x40000000, (uint)sh.LoopOffset, (uint)(sh.SampleRate >> 10), 60, 0); - sampleDict.Add(i, (sh, sf2Index)); - } - return sampleDict; - } - private static void AddInstruments(Config config, SF2 sf2, Dictionary sampleDict) - { - for (int v = 0; v < 256; v++) - { - short off = config.Reader.ReadInt16(config.VoiceTableOffset + (v * 2)); - short nextOff = config.Reader.ReadInt16(config.VoiceTableOffset + ((v + 1) * 2)); - int numEntries = (nextOff - off) / 8; // Each entry is 8 bytes - if (numEntries == 0) - { - continue; - } - - string name = "Instrument " + v; - sf2.AddPreset(name, (ushort)v, 0); - sf2.AddPresetBag(); - sf2.AddPresetGenerator(SF2Generator.Instrument, new SF2GeneratorAmount { Amount = (short)sf2.AddInstrument(name) }); - for (int e = 0; e < numEntries; e++) - { - VoiceEntry entry = config.Reader.ReadObject(config.VoiceTableOffset + off + (e * 8)); - sf2.AddInstrumentBag(); - // Key range - if (!(entry.MinKey == 0 && entry.MaxKey == 0x7F)) - { - sf2.AddInstrumentGenerator(SF2Generator.KeyRange, new SF2GeneratorAmount { LowByte = entry.MinKey, HighByte = entry.MaxKey }); - } - // Fixed frequency - if (entry.IsFixedFrequency == 0x80) - { - sf2.AddInstrumentGenerator(SF2Generator.ScaleTuning, new SF2GeneratorAmount { Amount = 0 }); - } - // Sample - if (entry.Sample < config.SampleTableSize) - { - if (!sampleDict.TryGetValue(entry.Sample, out (SampleHeader, int) value)) - { - Debug.WriteLine(string.Format("Voice {0} uses a null sample id ({1})", v, entry.Sample)); - } - else - { - sf2.AddInstrumentGenerator(SF2Generator.SampleModes, new SF2GeneratorAmount { Amount = (short)(value.Item1.DoesLoop == 0x40000000 ? 1 : 0) }); - sf2.AddInstrumentGenerator(SF2Generator.SampleID, new SF2GeneratorAmount { UAmount = (ushort)value.Item2 }); - } - } - else - { - Debug.WriteLine(string.Format("Voice {0} uses an invalid sample id ({1})", v, entry.Sample)); - } - } - } - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Structs.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Structs.cs deleted file mode 100644 index ac3b2bb3..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/Structs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class SampleHeader - { - /// 0x40000000 if True - public int DoesLoop { get; set; } - /// Right shift 10 for value - public int SampleRate { get; set; } - public int LoopOffset { get; set; } - public int Length { get; set; } - } - internal class VoiceEntry - { - public byte MinKey { get; set; } - public byte MaxKey { get; set; } - public byte Sample { get; set; } - /// 0x80 if True - public byte IsFixedFrequency { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown { get; set; } - } - - internal struct ChannelVolume - { - public float LeftVol, RightVol; - } - internal class ADSR // TODO - { - public byte A, D, S, R; - } -} diff --git a/VG Music Studio.backup/Core/GBA/AlphaDream/Track.cs b/VG Music Studio.backup/Core/GBA/AlphaDream/Track.cs deleted file mode 100644 index 296a2840..00000000 --- a/VG Music Studio.backup/Core/GBA/AlphaDream/Track.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.GBA.AlphaDream -{ - internal class Track - { - public readonly byte Index; - public readonly string Type; - public readonly Channel Channel; - - public byte Voice; - public byte PitchBendRange; - public byte Volume; - public byte Rest; - public byte NoteDuration; - public sbyte PitchBend; - public sbyte Panpot; - public bool Enabled; - public bool Stopped; - public int StartOffset; - public int DataOffset; - public byte PrevCommand; - - public int GetPitch() - { - return PitchBend * (PitchBendRange / 2); - } - - public Track(byte i, Mixer mixer) - { - Index = i; - if (i >= 8) - { - Type = Utils.PSGTypes[i & 3]; - Channel = new SquareChannel(mixer); // TODO: PSG Channels 3 and 4 - } - else - { - Type = "PCM8"; - Channel = new PCMChannel(mixer); - } - } - // 0x819B040 - public void Init() - { - Voice = 0; - Rest = 1; // Unsure why Rest starts at 1 - PitchBendRange = 2; - NoteDuration = 0; - PitchBend = 0; - Panpot = 0; // Start centered; ROM sets this to 0x7F since it's unsigned there - DataOffset = StartOffset; - Stopped = false; - Volume = 200; - PrevCommand = 0xFF; - //Tempo = 120; - //TempoStack = 0; - } - public void Tick() - { - if (Rest != 0) - { - Rest--; - } - if (NoteDuration > 0) - { - NoteDuration--; - } - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Channel.cs b/VG Music Studio.backup/Core/GBA/MP2K/Channel.cs deleted file mode 100644 index faa606d1..00000000 --- a/VG Music Studio.backup/Core/GBA/MP2K/Channel.cs +++ /dev/null @@ -1,777 +0,0 @@ -using System; -using System.Collections; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal abstract class Channel - { - public EnvelopeState State = EnvelopeState.Dead; - public Track Owner; - protected readonly Mixer _mixer; - - public Note Note; // Must be a struct & field - protected ADSR _adsr; - protected int _instPan; - - protected byte _velocity; - protected int _pos; - protected float _interPos; - protected float _frequency; - - protected Channel(Mixer mixer) - { - _mixer = mixer; - } - - public abstract ChannelVolume GetVolume(); - public abstract void SetVolume(byte vol, sbyte pan); - public abstract void SetPitch(int pitch); - public virtual void Release() - { - if (State < EnvelopeState.Releasing) - { - State = EnvelopeState.Releasing; - } - } - - public abstract void Process(float[] buffer); - - // Returns whether the note is active or not - public virtual bool TickNote() - { - if (State < EnvelopeState.Releasing) - { - if (Note.Duration > 0) - { - Note.Duration--; - if (Note.Duration == 0) - { - State = EnvelopeState.Releasing; - return false; - } - return true; - } - else - { - return true; - } - } - else - { - return false; - } - } - public void Stop() - { - State = EnvelopeState.Dead; - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = null; - } - } - internal class PCM8Channel : Channel - { - private SampleHeader _sampleHeader; - private int _sampleOffset; - private GoldenSunPSG _gsPSG; - private bool _bFixed; - private bool _bGoldenSun; - private bool _bCompressed; - private byte _leftVol; - private byte _rightVol; - private sbyte[] _decompressedSample; - - public PCM8Channel(Mixer mixer) : base(mixer) { } - public void Init(Track owner, Note note, ADSR adsr, int sampleOffset, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed) - { - State = EnvelopeState.Initializing; - _pos = 0; _interPos = 0; - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = owner; - Owner.Channels.Add(this); - Note = note; - _adsr = adsr; - _instPan = instPan; - _sampleHeader = _mixer.Config.Reader.ReadObject(sampleOffset); - _sampleOffset = sampleOffset + 0x10; - _bFixed = bFixed; - _bCompressed = bCompressed; - _decompressedSample = bCompressed ? Utils.Decompress(_sampleOffset, _sampleHeader.Length) : null; - _bGoldenSun = _mixer.Config.HasGoldenSunSynths && _sampleHeader.DoesLoop == 0x40000000 && _sampleHeader.LoopOffset == 0 && _sampleHeader.Length == 0; - if (_bGoldenSun) - { - _gsPSG = _mixer.Config.Reader.ReadObject(_sampleOffset); - } - SetVolume(vol, pan); - SetPitch(pitch); - } - - public override ChannelVolume GetVolume() - { - const float max = 0x10000; - return new ChannelVolume - { - LeftVol = _leftVol * _velocity / max * _mixer.PCM8MasterVolume, - RightVol = _rightVol * _velocity / max * _mixer.PCM8MasterVolume - }; - } - public override void SetVolume(byte vol, sbyte pan) - { - int combinedPan = pan + _instPan; - if (combinedPan > 63) - { - combinedPan = 63; - } - else if (combinedPan < -64) - { - combinedPan = -64; - } - const int fix = 0x2000; - if (State < EnvelopeState.Releasing) - { - int a = Note.Velocity * vol; - _leftVol = (byte)(a * (-combinedPan + 0x40) / fix); - _rightVol = (byte)(a * (combinedPan + 0x40) / fix); - } - } - public override void SetPitch(int pitch) - { - _frequency = (_sampleHeader.SampleRate >> 10) * (float)Math.Pow(2, ((Note.Key - 60) / 12f) + (pitch / 768f)); - } - - private void StepEnvelope() - { - switch (State) - { - case EnvelopeState.Initializing: - { - _velocity = _adsr.A; - State = EnvelopeState.Rising; - break; - } - case EnvelopeState.Rising: - { - int nextVel = _velocity + _adsr.A; - if (nextVel >= 0xFF) - { - State = EnvelopeState.Decaying; - _velocity = 0xFF; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Decaying: - { - int nextVel = (_velocity * _adsr.D) >> 8; - if (nextVel <= _adsr.S) - { - State = EnvelopeState.Playing; - _velocity = _adsr.S; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Playing: - { - break; - } - case EnvelopeState.Releasing: - { - int nextVel = (_velocity * _adsr.R) >> 8; - if (nextVel <= 0) - { - State = EnvelopeState.Dying; - _velocity = 0; - } - else - { - _velocity = (byte)nextVel; - } - break; - } - case EnvelopeState.Dying: - { - Stop(); - break; - } - } - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - if (State == EnvelopeState.Dead) - { - return; - } - - ChannelVolume vol = GetVolume(); - float interStep = _bFixed && !_bGoldenSun ? _mixer.SampleRate * _mixer.SampleRateReciprocal : _frequency * _mixer.SampleRateReciprocal; - if (_bGoldenSun) // Most Golden Sun processing is thanks to ipatix - { - interStep /= 0x40; - switch (_gsPSG.Type) - { - case GoldenSunPSGType.Square: - { - _pos += _gsPSG.CycleSpeed << 24; - int iThreshold = (_gsPSG.MinimumCycle << 24) + _pos; - iThreshold = (iThreshold < 0 ? ~iThreshold : iThreshold) >> 8; - iThreshold = (iThreshold * _gsPSG.CycleAmplitude) + (_gsPSG.InitialCycle << 24); - float threshold = iThreshold / (float)0x100000000; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _interPos < threshold ? 0.5f : -0.5f; - samp += 0.5f - threshold; - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - if (_interPos >= 1) - { - _interPos--; - } - } while (--samplesPerBuffer > 0); - break; - } - case GoldenSunPSGType.Saw: - { - const int fix = 0x70; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - _interPos += interStep; - if (_interPos >= 1) - { - _interPos--; - } - int var1 = (int)(_interPos * 0x100) - fix; - int var2 = (int)(_interPos * 0x10000) << 17; - int var3 = var1 - (var2 >> 27); - _pos = var3 + (_pos >> 1); - - float samp = _pos / (float)0x100; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - } while (--samplesPerBuffer > 0); - break; - } - case GoldenSunPSGType.Triangle: - { - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - _interPos += interStep; - if (_interPos >= 1) - { - _interPos--; - } - float samp = _interPos < 0.5f ? (_interPos * 4) - 1 : 3 - (_interPos * 4); - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - } while (--samplesPerBuffer > 0); - break; - } - } - } - else if (_bCompressed) - { - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _decompressedSample[_pos] / (float)0x80; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos += posDelta; - if (_pos >= _decompressedSample.Length) - { - Stop(); - break; - } - } while (--samplesPerBuffer > 0); - } - else - { - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = (sbyte)_mixer.Config.ROM[_pos + _sampleOffset] / (float)0x80; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos += posDelta; - if (_pos >= _sampleHeader.Length) - { - if (_sampleHeader.DoesLoop == 0x40000000) - { - _pos = _sampleHeader.LoopOffset; - } - else - { - Stop(); - break; - } - } - } while (--samplesPerBuffer > 0); - } - } - } - internal abstract class PSGChannel : Channel - { - protected enum GBPan : byte - { - Left, - Center, - Right - } - - private byte _processStep; - private EnvelopeState _nextState; - private byte _peakVelocity; - private byte _sustainVelocity; - protected GBPan _panpot = GBPan.Center; - - public PSGChannel(Mixer mixer) : base(mixer) { } - protected void Init(Track owner, Note note, ADSR env, int instPan) - { - State = EnvelopeState.Initializing; - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = owner; - Owner.Channels.Add(this); - Note = note; - _adsr.A = (byte)(env.A & 0x7); - _adsr.D = (byte)(env.D & 0x7); - _adsr.S = (byte)(env.S & 0xF); - _adsr.R = (byte)(env.R & 0x7); - _instPan = instPan; - } - - public override void Release() - { - if (State < EnvelopeState.Releasing) - { - if (_adsr.R == 0) - { - _velocity = 0; - Stop(); - } - else if (_velocity == 0) - { - Stop(); - } - else - { - _nextState = EnvelopeState.Releasing; - } - } - } - public override bool TickNote() - { - if (State < EnvelopeState.Releasing) - { - if (Note.Duration > 0) - { - Note.Duration--; - if (Note.Duration == 0) - { - if (_velocity == 0) - { - Stop(); - } - else - { - State = EnvelopeState.Releasing; - } - return false; - } - return true; - } - else - { - return true; - } - } - else - { - return false; - } - } - - public override ChannelVolume GetVolume() - { - const float max = 0x20; - return new ChannelVolume - { - LeftVol = _panpot == GBPan.Right ? 0 : _velocity / max, - RightVol = _panpot == GBPan.Left ? 0 : _velocity / max - }; - } - public override void SetVolume(byte vol, sbyte pan) - { - int combinedPan = pan + _instPan; - if (combinedPan > 63) - { - combinedPan = 63; - } - else if (combinedPan < -64) - { - combinedPan = -64; - } - if (State < EnvelopeState.Releasing) - { - _panpot = combinedPan < -21 ? GBPan.Left : combinedPan > 20 ? GBPan.Right : GBPan.Center; - _peakVelocity = (byte)((Note.Velocity * vol) >> 10); - _sustainVelocity = (byte)(((_peakVelocity * _adsr.S) + 0xF) >> 4); // TODO - if (State == EnvelopeState.Playing) - { - _velocity = _sustainVelocity; - } - } - } - - protected void StepEnvelope() - { - void dec() - { - _processStep = 0; - if (_velocity - 1 <= _sustainVelocity) - { - _velocity = _sustainVelocity; - _nextState = EnvelopeState.Playing; - } - else if (_velocity != 0) - { - _velocity--; - } - } - void sus() - { - _processStep = 0; - } - void rel() - { - if (_adsr.R == 0) - { - _velocity = 0; - Stop(); - } - else - { - _processStep = 0; - if (_velocity - 1 <= 0) - { - _nextState = EnvelopeState.Dying; - _velocity = 0; - } - else - { - _velocity--; - } - } - } - - switch (State) - { - case EnvelopeState.Initializing: - { - _nextState = EnvelopeState.Rising; - _processStep = 0; - if ((_adsr.A | _adsr.D) == 0 || (_sustainVelocity == 0 && _peakVelocity == 0)) - { - State = EnvelopeState.Playing; - _velocity = _sustainVelocity; - return; - } - else if (_adsr.A == 0 && _adsr.S < 0xF) - { - State = EnvelopeState.Decaying; - int next = _peakVelocity - 1; - if (next < 0) - { - next = 0; - } - _velocity = (byte)next; - if (_velocity < _sustainVelocity) - { - _velocity = _sustainVelocity; - } - return; - } - else if (_adsr.A == 0) - { - State = EnvelopeState.Playing; - _velocity = _sustainVelocity; - return; - } - else - { - State = EnvelopeState.Rising; - _velocity = 1; - return; - } - } - case EnvelopeState.Rising: - { - if (++_processStep >= _adsr.A) - { - if (_nextState == EnvelopeState.Decaying) - { - State = EnvelopeState.Decaying; - dec(); return; - } - if (_nextState == EnvelopeState.Playing) - { - State = EnvelopeState.Playing; - sus(); return; - } - if (_nextState == EnvelopeState.Releasing) - { - State = EnvelopeState.Releasing; - rel(); return; - } - _processStep = 0; - if (++_velocity >= _peakVelocity) - { - if (_adsr.D == 0) - { - _nextState = EnvelopeState.Playing; - } - else if (_peakVelocity == _sustainVelocity) - { - _nextState = EnvelopeState.Playing; - _velocity = _peakVelocity; - } - else - { - _velocity = _peakVelocity; - _nextState = EnvelopeState.Decaying; - } - } - } - break; - } - case EnvelopeState.Decaying: - { - if (++_processStep >= _adsr.D) - { - if (_nextState == EnvelopeState.Playing) - { - State = EnvelopeState.Playing; - sus(); return; - } - if (_nextState == EnvelopeState.Releasing) - { - State = EnvelopeState.Releasing; - rel(); return; - } - dec(); - } - break; - } - case EnvelopeState.Playing: - { - if (++_processStep >= 1) - { - if (_nextState == EnvelopeState.Releasing) - { - State = EnvelopeState.Releasing; - rel(); return; - } - sus(); - } - break; - } - case EnvelopeState.Releasing: - { - if (++_processStep >= _adsr.R) - { - if (_nextState == EnvelopeState.Dying) - { - Stop(); - return; - } - rel(); - } - break; - } - } - } - } - internal class SquareChannel : PSGChannel - { - private float[] _pat; - - public SquareChannel(Mixer mixer) : base(mixer) { } - public void Init(Track owner, Note note, ADSR env, int instPan, SquarePattern pattern) - { - Init(owner, note, env, instPan); - switch (pattern) - { - default: _pat = Utils.SquareD12; break; - case SquarePattern.D25: _pat = Utils.SquareD25; break; - case SquarePattern.D50: _pat = Utils.SquareD50; break; - case SquarePattern.D75: _pat = Utils.SquareD75; break; - } - } - - public override void SetPitch(int pitch) - { - _frequency = 3520 * (float)Math.Pow(2, ((Note.Key - 69) / 12f) + (pitch / 768f)); - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - if (State == EnvelopeState.Dead) - { - return; - } - - ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _pat[_pos]; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos = (_pos + posDelta) & 0x7; - } while (--samplesPerBuffer > 0); - } - } - internal class PCM4Channel : PSGChannel - { - private float[] _sample; - - public PCM4Channel(Mixer mixer) : base(mixer) { } - public void Init(Track owner, Note note, ADSR env, int instPan, int sampleOffset) - { - Init(owner, note, env, instPan); - _sample = Utils.PCM4ToFloat(sampleOffset); - } - - public override void SetPitch(int pitch) - { - _frequency = 7040 * (float)Math.Pow(2, ((Note.Key - 69) / 12f) + (pitch / 768f)); - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - if (State == EnvelopeState.Dead) - { - return; - } - - ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _sample[_pos]; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos = (_pos + posDelta) & 0x1F; - } while (--samplesPerBuffer > 0); - } - } - internal class NoiseChannel : PSGChannel - { - private BitArray _pat; - - public NoiseChannel(Mixer mixer) : base(mixer) { } - public void Init(Track owner, Note note, ADSR env, int instPan, NoisePattern pattern) - { - Init(owner, note, env, instPan); - _pat = pattern == NoisePattern.Fine ? Utils.NoiseFine : Utils.NoiseRough; - } - - public override void SetPitch(int pitch) - { - int key = Note.Key + (int)Math.Round(pitch / 64f); - if (key <= 20) - { - key = 0; - } - else - { - key -= 21; - if (key > 59) - { - key = 59; - } - } - byte v = Utils.NoiseFrequencyTable[key]; - // The following emulates 0x0400007C - SOUND4CNT_H - int r = v & 7; // Bits 0-2 - int s = v >> 4; // Bits 4-7 - _frequency = 524288f / (r == 0 ? 0.5f : r) / (float)Math.Pow(2, s + 1); - } - - public override void Process(float[] buffer) - { - StepEnvelope(); - if (State == EnvelopeState.Dead) - { - return; - } - - ChannelVolume vol = GetVolume(); - float interStep = _frequency * _mixer.SampleRateReciprocal; - - int bufPos = 0; int samplesPerBuffer = _mixer.SamplesPerBuffer; - do - { - float samp = _pat[_pos & (_pat.Length - 1)] ? 0.5f : -0.5f; - - buffer[bufPos++] += samp * vol.LeftVol; - buffer[bufPos++] += samp * vol.RightVol; - - _interPos += interStep; - int posDelta = (int)_interPos; - _interPos -= posDelta; - _pos = (_pos + posDelta) & (_pat.Length - 1); - } while (--samplesPerBuffer > 0); - } - } -} \ No newline at end of file diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Commands.cs b/VG Music Studio.backup/Core/GBA/MP2K/Commands.cs deleted file mode 100644 index 776d013f..00000000 --- a/VG Music Studio.backup/Core/GBA/MP2K/Commands.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class CallCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Call"; - public string Arguments => $"0x{Offset:X7}"; - - public int Offset { get; set; } - } - internal class EndOfTieCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "End Of Tie"; - public string Arguments => Key == -1 ? "All Ties" : Util.Utils.GetNoteName(Key); - - public int Key { get; set; } - } - internal class FinishCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Finish"; - public string Arguments => Prev ? "Resume previous track" : "End track"; - - public bool Prev { get; set; } - } - internal class JumpCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Jump"; - public string Arguments => $"0x{Offset:X7}"; - - public int Offset { get; set; } - } - internal class LFODelayCommand : ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Delay"; - public string Arguments => Delay.ToString(); - - public byte Delay { get; set; } - } - internal class LFODepthCommand : ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Depth"; - public string Arguments => Depth.ToString(); - - public byte Depth { get; set; } - } - internal class LFOSpeedCommand : ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Speed"; - public string Arguments => Speed.ToString(); - - public byte Speed { get; set; } - } - internal class LFOTypeCommand : ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Type"; - public string Arguments => Type.ToString(); - - public LFOType Type { get; set; } - } - internal class LibraryCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Library Call"; - public string Arguments => $"{Command}, {Argument}"; - - public byte Command { get; set; } - public byte Argument { get; set; } - } - internal class MemoryAccessCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Memory Access"; - public string Arguments => $"{Operator}, {Address}, {Data}"; - - public byte Operator { get; set; } - public byte Address { get; set; } - public byte Data { get; set; } - } - internal class NoteCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)} {Velocity} {Duration}"; - - public byte Key { get; set; } - public byte Velocity { get; set; } - public int Duration { get; set; } - } - internal class PanpotCommand : ICommand - { - public Color Color => Color.GreenYellow; - public string Label => "Panpot"; - public string Arguments => Panpot.ToString(); - - public sbyte Panpot { get; set; } - } - internal class PitchBendCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend"; - public string Arguments => Bend.ToString(); - - public sbyte Bend { get; set; } - } - internal class PitchBendRangeCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend Range"; - public string Arguments => Range.ToString(); - - public byte Range { get; set; } - } - internal class PriorityCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Priority"; - public string Arguments => Priority.ToString(); - - public byte Priority { get; set; } - } - internal class RepeatCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Repeat"; - public string Arguments => $"{Times}, 0x{Offset:X7}"; - - public byte Times { get; set; } - public int Offset { get; set; } - } - internal class RestCommand : ICommand - { - public Color Color => Color.PaleVioletRed; - public string Label => "Rest"; - public string Arguments => Rest.ToString(); - - public byte Rest { get; set; } - } - internal class ReturnCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Return"; - public string Arguments => string.Empty; - } - internal class TempoCommand : ICommand - { - public Color Color => Color.DeepSkyBlue; - public string Label => "Tempo"; - public string Arguments => Tempo.ToString(); - - public ushort Tempo { get; set; } - } - internal class TransposeCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Transpose"; - public string Arguments => Transpose.ToString(); - - public sbyte Transpose { get; set; } - } - internal class TuneCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Fine Tune"; - public string Arguments => Tune.ToString(); - - public sbyte Tune { get; set; } - } - internal class VoiceCommand : ICommand - { - public Color Color => Color.DarkSalmon; - public string Label => "Voice"; - public string Arguments => Voice.ToString(); - - public byte Voice { get; set; } - } - internal class VolumeCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Volume"; - public string Arguments => Volume.ToString(); - - public byte Volume { get; set; } - } -} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Config.cs b/VG Music Studio.backup/Core/GBA/MP2K/Config.cs deleted file mode 100644 index 9a0ea239..00000000 --- a/VG Music Studio.backup/Core/GBA/MP2K/Config.cs +++ /dev/null @@ -1,242 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using YamlDotNet.RepresentationModel; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class Config : Core.Config - { - public readonly byte[] ROM; - public readonly EndianBinaryReader Reader; - public readonly string GameCode; - public readonly byte Version; - - public readonly string Name; - public readonly int[] SongTableOffsets; - public readonly long[] SongTableSizes; - public readonly int SampleRate; - public readonly ReverbType ReverbType; - public readonly byte Reverb; - public readonly byte Volume; - public readonly bool HasGoldenSunSynths; - public readonly bool HasPokemonCompression; - - public Config(byte[] rom) - { - const string configFile = "MP2K.yaml"; - using (StreamReader fileStream = File.OpenText(Util.Utils.CombineWithBaseDirectory(configFile))) - { - string gcv = string.Empty; - try - { - ROM = rom; - Reader = new EndianBinaryReader(new MemoryStream(rom)); - GameCode = Reader.ReadString(4, false, 0xAC); - Version = Reader.ReadByte(0xBC); - gcv = $"{GameCode}_{Version:X2}"; - var yaml = new YamlStream(); - yaml.Load(fileStream); - - var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; - YamlMappingNode game; - try - { - game = (YamlMappingNode)mapping.Children.GetValue(gcv); - } - catch (BetterKeyNotFoundException) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KMissingGameCode, gcv))); - } - - YamlNode nameNode = null, - songTableOffsetsNode = null, - songTableSizesNode = null, - sampleRateNode = null, - reverbTypeNode = null, - reverbNode = null, - volumeNode = null, - hasGoldenSunSynthsNode = null, - hasPokemonCompression = null; - void Load(YamlMappingNode gameToLoad) - { - if (gameToLoad.Children.TryGetValue("Copy", out YamlNode node)) - { - YamlMappingNode copyGame; - try - { - copyGame = (YamlMappingNode)mapping.Children.GetValue(node); - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KCopyInvalidGameCode, ex.Key))); - } - Load(copyGame); - } - if (gameToLoad.Children.TryGetValue(nameof(Name), out node)) - { - nameNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableOffsets), out node)) - { - songTableOffsetsNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SongTableSizes), out node)) - { - songTableSizesNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(SampleRate), out node)) - { - sampleRateNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(ReverbType), out node)) - { - reverbTypeNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Reverb), out node)) - { - reverbNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Volume), out node)) - { - volumeNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(HasGoldenSunSynths), out node)) - { - hasGoldenSunSynthsNode = node; - } - if (gameToLoad.Children.TryGetValue(nameof(HasPokemonCompression), out node)) - { - hasPokemonCompression = node; - } - if (gameToLoad.Children.TryGetValue(nameof(Playlists), out node)) - { - var playlists = (YamlMappingNode)node; - foreach (KeyValuePair kvp in playlists) - { - string name = kvp.Key.ToString(); - var songs = new List(); - foreach (KeyValuePair song in (YamlMappingNode)kvp.Value) - { - long songIndex = Util.Utils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Playlists)), song.Key.ToString(), 0, long.MaxValue); - if (songs.Any(s => s.Index == songIndex)) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongRepeated, name, songIndex))); - } - songs.Add(new Song(songIndex, song.Value.ToString())); - } - Playlists.Add(new Playlist(name, songs)); - } - } - } - - Load(game); - - if (nameNode == null) - { - throw new BetterKeyNotFoundException(nameof(Name), null); - } - Name = nameNode.ToString(); - if (songTableOffsetsNode == null) - { - throw new BetterKeyNotFoundException(nameof(SongTableOffsets), null); - } - string[] songTables = songTableOffsetsNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); - int numSongTables = songTables.Length; - if (numSongTables == 0) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyNoEntries, nameof(SongTableOffsets)))); - } - if (songTableSizesNode == null) - { - throw new BetterKeyNotFoundException(nameof(SongTableSizes), null); - } - string[] sizes = songTableSizesNode.ToString().SplitSpace(StringSplitOptions.RemoveEmptyEntries); - if (sizes.Length != numSongTables) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorAlphaDreamMP2KSongTableCounts, nameof(SongTableSizes), nameof(SongTableOffsets)))); - } - SongTableOffsets = new int[numSongTables]; - SongTableSizes = new long[numSongTables]; - int maxOffset = rom.Length - 1; - for (int i = 0; i < numSongTables; i++) - { - SongTableSizes[i] = Util.Utils.ParseValue(nameof(SongTableSizes), sizes[i], 1, maxOffset); - SongTableOffsets[i] = (int)Util.Utils.ParseValue(nameof(SongTableOffsets), songTables[i], 0, maxOffset); - } - if (sampleRateNode == null) - { - throw new BetterKeyNotFoundException(nameof(SampleRate), null); - } - SampleRate = (int)Util.Utils.ParseValue(nameof(SampleRate), sampleRateNode.ToString(), 0, Utils.FrequencyTable.Length - 1); - if (reverbTypeNode == null) - { - throw new BetterKeyNotFoundException(nameof(ReverbType), null); - } - ReverbType = Util.Utils.ParseEnum(nameof(ReverbType), reverbTypeNode.ToString()); - if (reverbNode == null) - { - throw new BetterKeyNotFoundException(nameof(Reverb), null); - } - Reverb = (byte)Util.Utils.ParseValue(nameof(Reverb), reverbNode.ToString(), byte.MinValue, byte.MaxValue); - if (volumeNode == null) - { - throw new BetterKeyNotFoundException(nameof(Volume), null); - } - Volume = (byte)Util.Utils.ParseValue(nameof(Volume), volumeNode.ToString(), 0, 15); - if (hasGoldenSunSynthsNode == null) - { - throw new BetterKeyNotFoundException(nameof(HasGoldenSunSynths), null); - } - HasGoldenSunSynths = Util.Utils.ParseBoolean(nameof(HasGoldenSunSynths), hasGoldenSunSynthsNode.ToString()); - if (hasPokemonCompression == null) - { - throw new BetterKeyNotFoundException(nameof(HasPokemonCompression), null); - } - HasPokemonCompression = Util.Utils.ParseBoolean(nameof(HasPokemonCompression), hasPokemonCompression.ToString()); - - // The complete playlist - if (!Playlists.Any(p => p.Name == "Music")) - { - Playlists.Insert(0, new Playlist(Strings.PlaylistMusic, Playlists.SelectMany(p => p.Songs).Distinct().OrderBy(s => s.Index))); - } - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); - } - catch (InvalidValueException ex) - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamMP2KParseGameCode, gcv, configFile, Environment.NewLine + ex.Message)); - } - catch (YamlDotNet.Core.YamlException ex) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + ex.Message)); - } - } - } - - public override string GetGameName() - { - return Name; - } - public override string GetSongName(long index) - { - Song s = GetFirstSong(index); - if (s != null) - { - return s.Name; - } - return index.ToString(); - } - - public override void Dispose() - { - Reader.Dispose(); - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Enums.cs b/VG Music Studio.backup/Core/GBA/MP2K/Enums.cs deleted file mode 100644 index f22ac7e9..00000000 --- a/VG Music Studio.backup/Core/GBA/MP2K/Enums.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal enum EnvelopeState : byte - { - Initializing, - Rising, - Decaying, - Playing, - Releasing, - Dying, - Dead - } - internal enum ReverbType : byte - { - None, - Normal, - Camelot1, - Camelot2, - MGAT - } - - internal enum GoldenSunPSGType : byte - { - Square, - Saw, - Triangle - } - internal enum LFOType : byte - { - Pitch, - Volume, - Panpot - } - internal enum SquarePattern : byte - { - D12, - D25, - D50, - D75 - } - internal enum NoisePattern : byte - { - Fine, - Rough - } - internal enum VoiceType : byte - { - PCM8, - Square1, - Square2, - PCM4, - Noise, - Invalid5, - Invalid6, - Invalid7 - } - [Flags] - internal enum VoiceFlags : byte - { - // These are flags that apply to the types - Fixed = 0x08, // PCM8 - OffWithNoise = 0x08, // Square1, Square2, PCM4, Noise - Reversed = 0x10, // PCM8 - Compressed = 0x20, // PCM8 (Only in Pokémon main series games) - - // These are flags that cancel out every other bit after them if set so they should only be checked with equality - KeySplit = 0x40, - Drum = 0x80 - } -} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs b/VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs deleted file mode 100644 index 738859f6..00000000 --- a/VG Music Studio.backup/Core/GBA/MP2K/Mixer.cs +++ /dev/null @@ -1,275 +0,0 @@ -using NAudio.Wave; -using System; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class Mixer : Core.Mixer - { - public readonly int SampleRate; - public readonly int SamplesPerBuffer; - public readonly float SampleRateReciprocal; - private readonly float _samplesReciprocal; - public readonly float PCM8MasterVolume; - private bool _isFading; - private long _fadeMicroFramesLeft; - private float _fadePos; - private float _fadeStepPerMicroframe; - - public readonly Config Config; - private readonly WaveBuffer _audio; - private readonly float[][] _trackBuffers; - private readonly PCM8Channel[] _pcm8Channels; - private readonly SquareChannel _sq1; - private readonly SquareChannel _sq2; - private readonly PCM4Channel _pcm4; - private readonly NoiseChannel _noise; - private readonly PSGChannel[] _psgChannels; - private readonly BufferedWaveProvider _buffer; - - public Mixer(Config config) - { - Config = config; - (SampleRate, SamplesPerBuffer) = Utils.FrequencyTable[config.SampleRate]; - SampleRateReciprocal = 1f / SampleRate; - _samplesReciprocal = 1f / SamplesPerBuffer; - PCM8MasterVolume = config.Volume / 15f; - - _pcm8Channels = new PCM8Channel[24]; - for (int i = 0; i < _pcm8Channels.Length; i++) - { - _pcm8Channels[i] = new PCM8Channel(this); - } - _psgChannels = new PSGChannel[] { _sq1 = new SquareChannel(this), _sq2 = new SquareChannel(this), _pcm4 = new PCM4Channel(this), _noise = new NoiseChannel(this) }; - - int amt = SamplesPerBuffer * 2; - _audio = new WaveBuffer(amt * sizeof(float)) { FloatBufferCount = amt }; - _trackBuffers = new float[0x10][]; - for (int i = 0; i < _trackBuffers.Length; i++) - { - _trackBuffers[i] = new float[amt]; - } - _buffer = new BufferedWaveProvider(WaveFormat.CreateIeeeFloatWaveFormat(SampleRate, 2)) - { - DiscardOnBufferOverflow = true, - BufferLength = SamplesPerBuffer * 64 - }; - Init(_buffer); - } - public override void Dispose() - { - base.Dispose(); - CloseWaveWriter(); - } - - public PCM8Channel AllocPCM8Channel(Track owner, ADSR env, Note note, byte vol, sbyte pan, int instPan, int pitch, bool bFixed, bool bCompressed, int sampleOffset) - { - PCM8Channel nChn = null; - IOrderedEnumerable byOwner = _pcm8Channels.OrderByDescending(c => c.Owner == null ? 0xFF : c.Owner.Index); - foreach (PCM8Channel i in byOwner) // Find free - { - if (i.State == EnvelopeState.Dead || i.Owner == null) - { - nChn = i; - break; - } - } - if (nChn == null) // Find releasing - { - foreach (PCM8Channel i in byOwner) - { - if (i.State == EnvelopeState.Releasing) - { - nChn = i; - break; - } - } - } - if (nChn == null) // Find prioritized - { - foreach (PCM8Channel i in byOwner) - { - if (owner.Priority > i.Owner.Priority) - { - nChn = i; - break; - } - } - } - if (nChn == null) // None available - { - PCM8Channel lowest = byOwner.First(); // Kill lowest track's instrument if the track is lower than this one - if (lowest.Owner.Index >= owner.Index) - { - nChn = lowest; - } - } - if (nChn != null) // Could still be null from the above if - { - nChn.Init(owner, note, env, sampleOffset, vol, pan, instPan, pitch, bFixed, bCompressed); - } - return nChn; - } - public PSGChannel AllocPSGChannel(Track owner, ADSR env, Note note, byte vol, sbyte pan, int instPan, int pitch, VoiceType type, object arg) - { - PSGChannel nChn; - switch (type) - { - case VoiceType.Square1: - { - nChn = _sq1; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) - { - return null; - } - _sq1.Init(owner, note, env, instPan, (SquarePattern)arg); - break; - } - case VoiceType.Square2: - { - nChn = _sq2; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) - { - return null; - } - _sq2.Init(owner, note, env, instPan, (SquarePattern)arg); - break; - } - case VoiceType.PCM4: - { - nChn = _pcm4; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) - { - return null; - } - _pcm4.Init(owner, note, env, instPan, (int)arg); - break; - } - case VoiceType.Noise: - { - nChn = _noise; - if (nChn.State < EnvelopeState.Releasing && nChn.Owner.Index < owner.Index) - { - return null; - } - _noise.Init(owner, note, env, instPan, (NoisePattern)arg); - break; - } - default: return null; - } - nChn.SetVolume(vol, pan); - nChn.SetPitch(pitch); - return nChn; - } - - public void BeginFadeIn() - { - _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBA.Utils.AGB_FPS); - _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; - _isFading = true; - } - public void BeginFadeOut() - { - _fadePos = 1f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * GBA.Utils.AGB_FPS); - _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; - _isFading = true; - } - public bool IsFading() - { - return _isFading; - } - public bool IsFadeDone() - { - return _isFading && _fadeMicroFramesLeft == 0; - } - public void ResetFade() - { - _isFading = false; - _fadeMicroFramesLeft = 0; - } - - private WaveFileWriter _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter?.Dispose(); - } - public void Process(bool output, bool recording) - { - for (int i = 0; i < _trackBuffers.Length; i++) - { - float[] buf = _trackBuffers[i]; - Array.Clear(buf, 0, buf.Length); - } - _audio.Clear(); - - for (int i = 0; i < _pcm8Channels.Length; i++) - { - PCM8Channel c = _pcm8Channels[i]; - if (c.Owner != null) - { - c.Process(_trackBuffers[c.Owner.Index]); - } - } - - for (int i = 0; i < _psgChannels.Length; i++) - { - PSGChannel c = _psgChannels[i]; - if (c.Owner != null) - { - c.Process(_trackBuffers[c.Owner.Index]); - } - } - - float masterStep; - float masterLevel; - if (_isFading && _fadeMicroFramesLeft == 0) - { - masterStep = 0; - masterLevel = 0; - } - else - { - float fromMaster = 1f; - float toMaster = 1f; - if (_fadeMicroFramesLeft > 0) - { - const float scale = 10f / 6f; - fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadePos += _fadeStepPerMicroframe; - toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadeMicroFramesLeft--; - } - masterStep = (toMaster - fromMaster) * _samplesReciprocal; - masterLevel = fromMaster; - } - for (int i = 0; i < _trackBuffers.Length; i++) - { - if (!Mutes[i]) - { - float level = masterLevel; - float[] buf = _trackBuffers[i]; - for (int j = 0; j < SamplesPerBuffer; j++) - { - _audio.FloatBuffer[j * 2] += buf[j * 2] * level; - _audio.FloatBuffer[(j * 2) + 1] += buf[(j * 2) + 1] * level; - level += masterStep; - } - } - } - if (output) - { - _buffer.AddSamples(_audio.ByteBuffer, 0, _audio.ByteBufferCount); - } - if (recording) - { - _waveWriter.Write(_audio.ByteBuffer, 0, _audio.ByteBufferCount); - } - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Player.cs b/VG Music Studio.backup/Core/GBA/MP2K/Player.cs deleted file mode 100644 index e00bd21a..00000000 --- a/VG Music Studio.backup/Core/GBA/MP2K/Player.cs +++ /dev/null @@ -1,1510 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using Sanford.Multimedia.Midi; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class Player : IPlayer - { - public class MIDISaveArgs - { - public bool SaveCommandsBeforeTranspose; - public bool ReverseVolume; - public List<(int AbsoluteTick, (byte Numerator, byte Denominator))> TimeSignatures; - } - - private readonly Mixer _mixer; - private readonly Config _config; - private readonly TimeBarrier _time; - private Thread _thread; - private int _voiceTableOffset = -1; - private Track[] _tracks; - private ushort _tempo; - private int _tempoStack; - private long _elapsedLoops; - - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; - - public PlayerState State { get; private set; } - public event SongEndedEvent SongEnded; - - public Player(Mixer mixer, Config config) - { - _mixer = mixer; - _config = config; - - _time = new TimeBarrier(GBA.Utils.AGB_FPS); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "MP2K Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread != null && (_thread.ThreadState == System.Threading.ThreadState.Running || _thread.ThreadState == System.Threading.ThreadState.WaitSleepJoin)) - { - _thread.Join(); - } - } - - private void InitEmulation() - { - _tempo = 150; - _tempoStack = 0; - _elapsedLoops = 0; - ElapsedTicks = 0; - _mixer.ResetFade(); - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - _tracks[trackIndex].Init(); - } - } - private void SetTicks() - { - MaxTicks = 0; - bool u = false; - for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) - { - Events[trackIndex] = Events[trackIndex].OrderBy(e => e.Offset).ToList(); - List evs = Events[trackIndex]; - Track track = _tracks[trackIndex]; - track.Init(); - ElapsedTicks = 0; - while (true) - { - SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); - if (track.CallStackDepth == 0 && e.Ticks.Count > 0) - { - break; - } - else - { - e.Ticks.Add(ElapsedTicks); - ExecuteNext(track, ref u); - if (track.Stopped) - { - break; - } - else - { - ElapsedTicks += track.Rest; - track.Rest = 0; - } - } - } - if (ElapsedTicks > MaxTicks) - { - _longestTrack = trackIndex; - MaxTicks = ElapsedTicks; - } - track.StopAllChannels(); - } - } - public void LoadSong(long index) - { - if (_tracks != null) - { - for (int i = 0; i < _tracks.Length; i++) - { - _tracks[i].StopAllChannels(); - } - _tracks = null; - } - Events = null; - SongEntry entry = _config.Reader.ReadObject(_config.SongTableOffsets[0] + (index * 8)); - SongHeader header = _config.Reader.ReadObject(entry.HeaderOffset - GBA.Utils.CartridgeOffset); - int oldVoiceTableOffset = _voiceTableOffset; - _voiceTableOffset = header.VoiceTableOffset - GBA.Utils.CartridgeOffset; - if (oldVoiceTableOffset != _voiceTableOffset) - { - _voiceTypeCache = new string[byte.MaxValue + 1]; - } - _tracks = new Track[header.NumTracks]; - Events = new List[header.NumTracks]; - for (byte trackIndex = 0; trackIndex < header.NumTracks; trackIndex++) - { - int trackStart = header.TrackOffsets[trackIndex] - GBA.Utils.CartridgeOffset; - _tracks[trackIndex] = new Track(trackIndex, trackStart); - Events[trackIndex] = new List(); - bool EventExists(long offset) - { - return Events[trackIndex].Any(e => e.Offset == offset); - } - - byte runCmd = 0, prevKey = 0, prevVelocity = 0x7F; - int callStackDepth = 0; - AddEvents(trackStart); - void AddEvents(long startOffset) - { - _config.Reader.BaseStream.Position = startOffset; - bool cont = true; - while (cont) - { - long offset = _config.Reader.BaseStream.Position; - void AddEvent(ICommand command) - { - Events[trackIndex].Add(new SongEvent(offset, command)); - } - void EmulateNote(byte key, byte velocity, byte addedDuration) - { - prevKey = key; - prevVelocity = velocity; - if (!EventExists(offset)) - { - AddEvent(new NoteCommand - { - Key = key, - Velocity = velocity, - Duration = runCmd == 0xCF ? -1 : (Utils.RestTable[runCmd - 0xCF] + addedDuration) - }); - } - } - - byte cmd = _config.Reader.ReadByte(); - if (cmd >= 0xBD) // Commands that work within running status - { - runCmd = cmd; - } - - #region TIE & Notes - - if (runCmd >= 0xCF && cmd <= 0x7F) // Within running status - { - byte velocity, addedDuration; - byte[] peek = _config.Reader.PeekBytes(2); - if (peek[0] > 0x7F) - { - velocity = prevVelocity; - addedDuration = 0; - } - else if (peek[1] > 3) - { - velocity = _config.Reader.ReadByte(); - addedDuration = 0; - } - else - { - velocity = _config.Reader.ReadByte(); - addedDuration = _config.Reader.ReadByte(); - } - EmulateNote(cmd, velocity, addedDuration); - } - else if (cmd >= 0xCF) - { - byte key, velocity, addedDuration; - byte[] peek = _config.Reader.PeekBytes(3); - if (peek[0] > 0x7F) - { - key = prevKey; - velocity = prevVelocity; - addedDuration = 0; - } - else if (peek[1] > 0x7F) - { - key = _config.Reader.ReadByte(); - velocity = prevVelocity; - addedDuration = 0; - } - // TIE (0xCF) cannot have an added duration so it needs to stop here - else if (cmd == 0xCF || peek[2] > 3) - { - key = _config.Reader.ReadByte(); - velocity = _config.Reader.ReadByte(); - addedDuration = 0; - } - else - { - key = _config.Reader.ReadByte(); - velocity = _config.Reader.ReadByte(); - addedDuration = _config.Reader.ReadByte(); - } - EmulateNote(key, velocity, addedDuration); - } - - #endregion - - #region Rests - - else if (cmd >= 0x80 && cmd <= 0xB0) - { - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = Utils.RestTable[cmd - 0x80] }); - } - } - - #endregion - - #region Commands - - else if (runCmd < 0xCF && cmd <= 0x7F) - { - switch (runCmd) - { - case 0xBD: - { - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = cmd }); - } - break; - } - case 0xBE: - { - if (!EventExists(offset)) - { - AddEvent(new VolumeCommand { Volume = cmd }); - } - break; - } - case 0xBF: - { - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xC0: - { - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xC1: - { - if (!EventExists(offset)) - { - AddEvent(new PitchBendRangeCommand { Range = cmd }); - } - break; - } - case 0xC2: - { - if (!EventExists(offset)) - { - AddEvent(new LFOSpeedCommand { Speed = cmd }); - } - break; - } - case 0xC3: - { - if (!EventExists(offset)) - { - AddEvent(new LFODelayCommand { Delay = cmd }); - } - break; - } - case 0xC4: - { - if (!EventExists(offset)) - { - AddEvent(new LFODepthCommand { Depth = cmd }); - } - break; - } - case 0xC5: - { - if (!EventExists(offset)) - { - AddEvent(new LFOTypeCommand { Type = (LFOType)cmd }); - } - break; - } - case 0xC8: - { - if (!EventExists(offset)) - { - AddEvent(new TuneCommand { Tune = (sbyte)(cmd - 0x40) }); - } - break; - } - case 0xCD: - { - byte arg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LibraryCommand { Command = cmd, Argument = arg }); - } - break; - } - case 0xCE: - { - prevKey = cmd; - if (!EventExists(offset)) - { - AddEvent(new EndOfTieCommand { Key = cmd }); - } - break; - } - default: throw new Exception(string.Format(Strings.ErrorMP2KInvalidRunningStatusCommand, trackIndex, offset, runCmd)); - } - } - else if (cmd > 0xB0 && cmd < 0xCF) - { - switch (cmd) - { - case 0xB1: - case 0xB6: - { - if (!EventExists(offset)) - { - AddEvent(new FinishCommand { Prev = cmd == 0xB6 }); - } - cont = false; - break; - } - case 0xB2: - { - int jumpOffset = _config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; - if (!EventExists(offset)) - { - AddEvent(new JumpCommand { Offset = jumpOffset }); - if (!EventExists(jumpOffset)) - { - AddEvents(jumpOffset); - } - } - cont = false; - break; - } - case 0xB3: - { - int callOffset = _config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; - if (!EventExists(offset)) - { - AddEvent(new CallCommand { Offset = callOffset }); - } - if (callStackDepth < 3) - { - long backup = _config.Reader.BaseStream.Position; - callStackDepth++; - AddEvents(callOffset); - _config.Reader.BaseStream.Position = backup; - } - else - { - throw new Exception(string.Format(Strings.ErrorMP2KSDATNestedCalls, trackIndex)); - } - break; - } - case 0xB4: - { - if (!EventExists(offset)) - { - AddEvent(new ReturnCommand()); - } - if (callStackDepth != 0) - { - cont = false; - callStackDepth--; - } - break; - } - /*case 0xB5: // TODO: Logic so this isn't an infinite loop - { - byte times = config.Reader.ReadByte(); - int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; - if (!EventExists(offset)) - { - AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); - } - break; - }*/ - case 0xB9: - { - byte op = _config.Reader.ReadByte(); - byte address = _config.Reader.ReadByte(); - byte data = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new MemoryAccessCommand { Operator = op, Address = address, Data = data }); - } - break; - } - case 0xBA: - { - byte priority = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PriorityCommand { Priority = priority }); - } - break; - } - case 0xBB: - { - byte tempoArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new TempoCommand { Tempo = (ushort)(tempoArg * 2) }); - } - break; - } - case 0xBC: - { - sbyte transpose = _config.Reader.ReadSByte(); - if (!EventExists(offset)) - { - AddEvent(new TransposeCommand { Transpose = transpose }); - } - break; - } - // Commands that work within running status: - case 0xBD: - { - byte voice = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = voice }); - } - break; - } - case 0xBE: - { - byte volume = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VolumeCommand { Volume = volume }); - } - break; - } - case 0xBF: - { - byte panArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); - } - break; - } - case 0xC0: - { - byte bendArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = (sbyte)(bendArg - 0x40) }); - } - break; - } - case 0xC1: - { - byte range = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendRangeCommand { Range = range }); - } - break; - } - case 0xC2: - { - byte speed = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LFOSpeedCommand { Speed = speed }); - } - break; - } - case 0xC3: - { - byte delay = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LFODelayCommand { Delay = delay }); - } - break; - } - case 0xC4: - { - byte depth = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LFODepthCommand { Depth = depth }); - } - break; - } - case 0xC5: - { - byte type = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LFOTypeCommand { Type = (LFOType)type }); - } - break; - } - case 0xC8: - { - byte tuneArg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new TuneCommand { Tune = (sbyte)(tuneArg - 0x40) }); - } - break; - } - case 0xCD: - { - byte command = _config.Reader.ReadByte(); - byte arg = _config.Reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new LibraryCommand { Command = command, Argument = arg }); - } - break; - } - case 0xCE: - { - int key = _config.Reader.PeekByte() <= 0x7F ? (prevKey = _config.Reader.ReadByte()) : -1; - if (!EventExists(offset)) - { - AddEvent(new EndOfTieCommand { Key = key }); - } - break; - } - default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, trackIndex, offset, cmd)); - } - } - - #endregion - } - } - } - SetTicks(); - } - public void SetCurrentPosition(long ticks) - { - if (Events == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - bool u = false; - while (true) - { - if (ElapsedTicks == ticks) - { - goto finish; - } - else - { - while (_tempoStack >= 150) - { - _tempoStack -= 150; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (!track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref u); - } - } - } - ElapsedTicks++; - if (ElapsedTicks == ticks) - { - goto finish; - } - } - _tempoStack += _tempo; - } - } - finish: - for (int i = 0; i < _tracks.Length; i++) - { - _tracks[i].StopAllChannels(); - } - Pause(); - } - } - // TODO: Don't use events, read from rom - public void SaveAsMIDI(string fileName, MIDISaveArgs args) - { - // TODO: FINE vs PREV - // TODO: https://github.com/Kermalis/VGMusicStudio/issues/36 - // TODO: Nested calls - // TODO: REPT - byte baseVolume = 0x7F; - if (args.ReverseVolume) - { - baseVolume = Events.SelectMany(e => e).Where(e => e.Command is VolumeCommand).Select(e => ((VolumeCommand)e.Command).Volume).Max(); - Debug.WriteLine($"Reversing volume back from {baseVolume}."); - } - - using (var midi = new Sequence(24) { Format = 1 }) - { - var metaTrack = new Sanford.Multimedia.Midi.Track(); - midi.Add(metaTrack); - var ts = new TimeSignatureBuilder(); - foreach ((int AbsoluteTick, (byte Numerator, byte Denominator)) e in args.TimeSignatures) - { - ts.Numerator = e.Item2.Numerator; - ts.Denominator = e.Item2.Denominator; - ts.ClocksPerMetronomeClick = 24; - ts.ThirtySecondNotesPerQuarterNote = 8; - ts.Build(); - metaTrack.Insert(e.AbsoluteTick, ts.Result); - } - - for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) - { - var track = new Sanford.Multimedia.Midi.Track(); - midi.Add(track); - - bool foundTranspose = false; - int endOfPattern = 0; - long startOfPatternTicks = 0, endOfPatternTicks = 0; - sbyte transpose = 0; - var playing = new List(); - for (int i = 0; i < Events[trackIndex].Count; i++) - { - SongEvent e = Events[trackIndex][i]; - int ticks = (int)(e.Ticks[0] + (endOfPatternTicks - startOfPatternTicks)); - - // Preliminary check for saving events before transpose - switch (e.Command) - { - case TransposeCommand keysh: foundTranspose = true; break; - default: // If we should not save before transpose then skip this event - { - if (!args.SaveCommandsBeforeTranspose && !foundTranspose) - { - continue; - } - break; - } - } - // Now do the event magic... - switch (e.Command) - { - case CallCommand patt: - { - int callCmd = Events[trackIndex].FindIndex(c => c.Offset == patt.Offset); - endOfPattern = i; - endOfPatternTicks = e.Ticks[0]; - i = callCmd - 1; // -1 for incoming ++ - startOfPatternTicks = Events[trackIndex][callCmd].Ticks[0]; - break; - } - case EndOfTieCommand eot: - { - NoteCommand nc = eot.Key == -1 ? playing.LastOrDefault() : playing.LastOrDefault(no => no.Key == eot.Key); - if (nc != null) - { - int key = nc.Key + transpose; - if (key < 0) - { - key = 0; - } - else if (key > 0x7F) - { - key = 0x7F; - } - track.Insert(ticks, new ChannelMessage(ChannelCommand.NoteOff, trackIndex, key)); - playing.Remove(nc); - } - break; - } - case FinishCommand _: - { - // If the track is not only the finish command, place the finish command at the correct tick - if (track.Count > 1) - { - track.EndOfTrackOffset = (int)(e.Ticks[0] - track.GetMidiEvent(track.Count - 2).AbsoluteTicks); - } - goto endOfTrack; - } - case JumpCommand goTo: - { - if (trackIndex == 0) - { - int jumpCmd = Events[trackIndex].FindIndex(c => c.Offset == goTo.Offset); - metaTrack.Insert((int)Events[trackIndex][jumpCmd].Ticks[0], new MetaMessage(MetaType.Marker, new byte[] { (byte)'[' })); - metaTrack.Insert(ticks, new MetaMessage(MetaType.Marker, new byte[] { (byte)']' })); - } - break; - } - case LFODelayCommand lfodl: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 26, lfodl.Delay)); - break; - } - case LFODepthCommand mod: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.ModulationWheel, mod.Depth)); - break; - } - case LFOSpeedCommand lfos: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 21, lfos.Speed)); - break; - } - case LFOTypeCommand modt: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 22, (byte)modt.Type)); - break; - } - case LibraryCommand xcmd: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 30, xcmd.Command)); - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 29, xcmd.Argument)); - break; - } - case MemoryAccessCommand memacc: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 13, memacc.Operator)); - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 14, memacc.Address)); - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 12, memacc.Data)); - break; - } - case NoteCommand note: - { - int key = note.Key + transpose; - if (key < 0) - { - key = 0; - } - else if (key > 0x7F) - { - key = 0x7F; - } - track.Insert(ticks, new ChannelMessage(ChannelCommand.NoteOn, trackIndex, key, note.Velocity)); - if (note.Duration != -1) - { - track.Insert(ticks + note.Duration, new ChannelMessage(ChannelCommand.NoteOff, trackIndex, key)); - } - else - { - playing.Add(note); - } - break; - } - case PanpotCommand pan: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.Pan, pan.Panpot + 0x40)); - break; - } - case PitchBendCommand bend: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.PitchWheel, trackIndex, 0, bend.Bend + 0x40)); - break; - } - case PitchBendRangeCommand bendr: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 20, bendr.Range)); - break; - } - case PriorityCommand prio: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.VolumeFine, prio.Priority)); - break; - } - case ReturnCommand _: - { - if (endOfPattern != 0) - { - i = endOfPattern; - endOfPattern = 0; - startOfPatternTicks = endOfPatternTicks = 0; - } - break; - } - case TempoCommand tempo: - { - var change = new TempoChangeBuilder { Tempo = 60000000 / tempo.Tempo }; - change.Build(); - metaTrack.Insert(ticks, change.Result); - break; - } - case TransposeCommand keysh: - { - transpose = keysh.Transpose; - break; - } - case TuneCommand tune: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, 24, tune.Tune)); - break; - } - case VoiceCommand voice: - { - track.Insert(ticks, new ChannelMessage(ChannelCommand.ProgramChange, trackIndex, voice.Voice)); - break; - } - case VolumeCommand vol: - { - double d = baseVolume / (double)0x7F; - int volume = (int)(vol.Volume / d); - // If there are rounding errors, fix them (happens if baseVolume is not 127 and baseVolume is not vol.Volume) - if (volume * baseVolume / 0x7F == vol.Volume - 1) - { - volume++; - } - track.Insert(ticks, new ChannelMessage(ChannelCommand.Controller, trackIndex, (int)ControllerType.Volume, volume)); - break; - } - } - } - endOfTrack:; - } - midi.Save(fileName); - } - } - public void Play() - { - if (Events == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); - } - } - public void Pause() - { - if (State == PlayerState.Playing) - { - State = PlayerState.Paused; - WaitThread(); - } - else if (State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.Playing; - CreateThread(); - } - } - public void Stop() - { - if (State == PlayerState.Playing || State == PlayerState.Paused) - { - State = PlayerState.Stopped; - WaitThread(); - } - } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() - { - if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.ShutDown; - WaitThread(); - } - } - private string[] _voiceTypeCache; - public void GetSongState(UI.SongInfoControl.SongInfo info) - { - info.Tempo = _tempo; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - UI.SongInfoControl.SongInfo.Track tin = info.Tracks[trackIndex]; - tin.Position = track.DataOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.LFO = track.LFODepth; - if (_voiceTypeCache[track.Voice] == null) - { - byte t = _config.ROM[_voiceTableOffset + (track.Voice * 0xC)]; - if (t == (byte)VoiceFlags.KeySplit) - { - _voiceTypeCache[track.Voice] = "Key Split"; - } - else if (t == (byte)VoiceFlags.Drum) - { - _voiceTypeCache[track.Voice] = "Drum"; - } - else - { - switch ((VoiceType)(t & 0x7)) - { - case VoiceType.PCM8: _voiceTypeCache[track.Voice] = "PCM8"; break; - case VoiceType.Square1: _voiceTypeCache[track.Voice] = "Square 1"; break; - case VoiceType.Square2: _voiceTypeCache[track.Voice] = "Square 2"; break; - case VoiceType.PCM4: _voiceTypeCache[track.Voice] = "PCM4"; break; - case VoiceType.Noise: _voiceTypeCache[track.Voice] = "Noise"; break; - case VoiceType.Invalid5: _voiceTypeCache[track.Voice] = "Invalid 5"; break; - case VoiceType.Invalid6: _voiceTypeCache[track.Voice] = "Invalid 6"; break; - case VoiceType.Invalid7: _voiceTypeCache[track.Voice] = "Invalid 7"; break; - } - } - } - tin.Type = _voiceTypeCache[track.Voice]; - tin.Volume = track.GetVolume(); - tin.PitchBend = track.GetPitch(); - tin.Panpot = track.GetPanpot(); - - Channel[] channels = track.Channels.ToArray(); - if (channels.Length == 0) - { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; - } - else - { - int numKeys = 0; - float left = 0f; - float right = 0f; - for (int j = 0; j < channels.Length; j++) - { - Channel c = channels[j]; - if (c.State < EnvelopeState.Releasing) - { - tin.Keys[numKeys++] = c.Note.OriginalKey; - } - ChannelVolume vol = c.GetVolume(); - if (vol.LeftVol > left) - { - left = vol.LeftVol; - } - if (vol.RightVol > right) - { - right = vol.RightVol; - } - } - tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array - tin.LeftVolume = left; - tin.RightVolume = right; - } - } - } - - // TODO: Don't use config.Reader (Or make ReadObjectCached(offset)) - private void PlayNote(Track track, byte key, byte velocity, byte addedDuration) - { - int k = key + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - key = (byte)k; - track.PrevKey = key; - track.PrevVelocity = velocity; - if (track.Ready) - { - bool fromDrum = false; - int offset = _voiceTableOffset + (track.Voice * 12); - while (true) - { - VoiceEntry v = _config.Reader.ReadObject(offset); - if (v.Type == (int)VoiceFlags.KeySplit) - { - fromDrum = false; // In case there is a multi within a drum - byte inst = _config.Reader.ReadByte(v.Int8 - GBA.Utils.CartridgeOffset + key); - offset = v.Int4 - GBA.Utils.CartridgeOffset + (inst * 12); - } - else if (v.Type == (int)VoiceFlags.Drum) - { - fromDrum = true; - offset = v.Int4 - GBA.Utils.CartridgeOffset + (key * 12); - } - else - { - var note = new Note - { - Duration = track.RunCmd == 0xCF ? -1 : (Utils.RestTable[track.RunCmd - 0xCF] + addedDuration), - Velocity = velocity, - OriginalKey = key, - Key = fromDrum ? v.RootKey : key - }; - var type = (VoiceType)(v.Type & 0x7); - int instPan = v.Pan; - instPan = (instPan & 0x80) != 0 ? instPan - 0xC0 : 0; - switch (type) - { - case VoiceType.PCM8: - { - bool bFixed = (v.Type & (int)VoiceFlags.Fixed) != 0; - bool bCompressed = _config.HasPokemonCompression && ((v.Type & (int)VoiceFlags.Compressed) != 0); - _mixer.AllocPCM8Channel(track, v.ADSR, note, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - bFixed, bCompressed, v.Int4 - GBA.Utils.CartridgeOffset); - return; - } - case VoiceType.Square1: - case VoiceType.Square2: - { - _mixer.AllocPSGChannel(track, v.ADSR, note, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - type, (SquarePattern)v.Int4); - return; - } - case VoiceType.PCM4: - { - _mixer.AllocPSGChannel(track, v.ADSR, note, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - type, v.Int4 - GBA.Utils.CartridgeOffset); - return; - } - case VoiceType.Noise: - { - _mixer.AllocPSGChannel(track, v.ADSR, note, - track.GetVolume(), track.GetPanpot(), instPan, track.GetPitch(), - type, (NoisePattern)v.Int4); - return; - } - } - } - } - } - } - private void ExecuteNext(Track track, ref bool update) - { - byte cmd = _config.ROM[track.DataOffset++]; - if (cmd >= 0xBD) // Commands that work within running status - { - track.RunCmd = cmd; - } - if (track.RunCmd >= 0xCF && cmd <= 0x7F) // Within running status - { - byte peek0 = _config.ROM[track.DataOffset]; - byte peek1 = _config.ROM[track.DataOffset + 1]; - byte velocity, addedDuration; - if (peek0 > 0x7F) - { - velocity = track.PrevVelocity; - addedDuration = 0; - } - else if (peek1 > 3) - { - track.DataOffset++; - velocity = peek0; - addedDuration = 0; - } - else - { - track.DataOffset += 2; - velocity = peek0; - addedDuration = peek1; - } - PlayNote(track, cmd, velocity, addedDuration); - } - else if (cmd >= 0xCF) - { - byte peek0 = _config.ROM[track.DataOffset]; - byte peek1 = _config.ROM[track.DataOffset + 1]; - byte peek2 = _config.ROM[track.DataOffset + 2]; - byte key, velocity, addedDuration; - if (peek0 > 0x7F) - { - key = track.PrevKey; - velocity = track.PrevVelocity; - addedDuration = 0; - } - else if (peek1 > 0x7F) - { - track.DataOffset++; - key = peek0; - velocity = track.PrevVelocity; - addedDuration = 0; - } - else if (cmd == 0xCF || peek2 > 3) - { - track.DataOffset += 2; - key = peek0; - velocity = peek1; - addedDuration = 0; - } - else - { - track.DataOffset += 3; - key = peek0; - velocity = peek1; - addedDuration = peek2; - } - PlayNote(track, key, velocity, addedDuration); - } - else if (cmd >= 0x80 && cmd <= 0xB0) - { - track.Rest = Utils.RestTable[cmd - 0x80]; - } - else if (track.RunCmd < 0xCF && cmd <= 0x7F) - { - switch (track.RunCmd) - { - case 0xBD: - { - track.Voice = cmd; - //track.Ready = true; // This is unnecessary because if we're in running status of a voice command, then Ready was already set - break; - } - case 0xBE: - { - track.Volume = cmd; - update = true; - break; - } - case 0xBF: - { - track.Panpot = (sbyte)(cmd - 0x40); - update = true; - break; - } - case 0xC0: - { - track.PitchBend = (sbyte)(cmd - 0x40); - update = true; - break; - } - case 0xC1: - { - track.PitchBendRange = cmd; - update = true; - break; - } - case 0xC2: - { - track.LFOSpeed = cmd; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } - case 0xC3: - { - track.LFODelay = cmd; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } - case 0xC4: - { - track.LFODepth = cmd; - update = true; - break; - } - case 0xC5: - { - track.LFOType = (LFOType)cmd; - update = true; - break; - } - case 0xC8: - { - track.Tune = (sbyte)(cmd - 0x40); - update = true; - break; - } - case 0xCD: - { - track.DataOffset++; - break; - } - case 0xCE: - { - track.PrevKey = cmd; - int k = cmd + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - track.ReleaseChannels(k); - break; - } - default: throw new Exception(string.Format(Strings.ErrorMP2KInvalidRunningStatusCommand, track.Index, track.DataOffset, track.RunCmd)); - } - } - else if (cmd > 0xB0 && cmd < 0xCF) - { - switch (cmd) - { - case 0xB1: - case 0xB6: - { - track.Stopped = true; - //track.ReleaseAllTieingChannels(); // Necessary? - break; - } - case 0xB2: - { - track.DataOffset = (_config.ROM[track.DataOffset++] | (_config.ROM[track.DataOffset++] << 8) | (_config.ROM[track.DataOffset++] << 16) | (_config.ROM[track.DataOffset++] << 24)) - GBA.Utils.CartridgeOffset; - break; - } - case 0xB3: - { - if (track.CallStackDepth < 3) - { - int callOffset = (_config.ROM[track.DataOffset++] | (_config.ROM[track.DataOffset++] << 8) | (_config.ROM[track.DataOffset++] << 16) | (_config.ROM[track.DataOffset++] << 24)) - GBA.Utils.CartridgeOffset; - track.CallStack[track.CallStackDepth] = track.DataOffset; - track.CallStackDepth++; - track.DataOffset = callOffset; - } - else - { - throw new Exception(string.Format(Strings.ErrorMP2KSDATNestedCalls, track.Index)); - } - break; - } - case 0xB4: - { - if (track.CallStackDepth != 0) - { - track.CallStackDepth--; - track.DataOffset = track.CallStack[track.CallStackDepth]; - } - break; - } - /*case 0xB5: // TODO: Logic so this isn't an infinite loop - { - byte times = config.Reader.ReadByte(); - int repeatOffset = config.Reader.ReadInt32() - GBA.Utils.CartridgeOffset; - if (!EventExists(offset)) - { - AddEvent(new RepeatCommand { Times = times, Offset = repeatOffset }); - } - break; - }*/ - case 0xB9: - { - track.DataOffset += 3; - break; - } - case 0xBA: - { - track.Priority = _config.ROM[track.DataOffset++]; - break; - } - case 0xBB: - { - _tempo = (ushort)(_config.ROM[track.DataOffset++] * 2); - break; - } - case 0xBC: - { - track.Transpose = (sbyte)_config.ROM[track.DataOffset++]; - break; - } - // Commands that work within running status: - case 0xBD: - { - track.Voice = _config.ROM[track.DataOffset++]; - track.Ready = true; - break; - } - case 0xBE: - { - track.Volume = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xBF: - { - track.Panpot = (sbyte)(_config.ROM[track.DataOffset++] - 0x40); - update = true; - break; - } - case 0xC0: - { - track.PitchBend = (sbyte)(_config.ROM[track.DataOffset++] - 0x40); - update = true; - break; - } - case 0xC1: - { - track.PitchBendRange = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xC2: - { - track.LFOSpeed = _config.ROM[track.DataOffset++]; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } - case 0xC3: - { - track.LFODelay = _config.ROM[track.DataOffset++]; - track.LFOPhase = 0; - track.LFODelayCount = 0; - update = true; - break; - } - case 0xC4: - { - track.LFODepth = _config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xC5: - { - track.LFOType = (LFOType)_config.ROM[track.DataOffset++]; - update = true; - break; - } - case 0xC8: - { - track.Tune = (sbyte)(_config.ROM[track.DataOffset++] - 0x40); - update = true; - break; - } - case 0xCD: - { - track.DataOffset += 2; - break; - } - case 0xCE: - { - byte peek = _config.ROM[track.DataOffset]; - if (peek > 0x7F) - { - track.ReleaseChannels(track.PrevKey); - } - else - { - track.DataOffset++; - track.PrevKey = peek; - int k = peek + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - track.ReleaseChannels(k); - } - break; - } - default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, track.Index, track.DataOffset, cmd)); - } - } - } - - private void Tick() - { - _time.Start(); - while (true) - { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - goto stop; - } - - void MixerProcess() - { - _mixer.Process(playing, recording); - } - - while (_tempoStack >= 150) - { - _tempoStack -= 150; - bool allDone = true; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - track.Tick(); - bool update = false; - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track, ref update); - } - if (trackIndex == _longestTrack) - { - if (ElapsedTicks == MaxTicks) - { - if (!track.Stopped) - { - List evs = Events[trackIndex]; - for (int i = 0; i < evs.Count; i++) - { - SongEvent ev = evs[i]; - if (ev.Offset == track.DataOffset) - { - ElapsedTicks = ev.Ticks[0] - track.Rest; - break; - } - } - _elapsedLoops++; - if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) - { - _mixer.BeginFadeOut(); - } - } - } - else - { - ElapsedTicks++; - } - } - if (!track.Stopped) - { - allDone = false; - } - if (track.Channels.Count > 0) - { - allDone = false; - if (update || track.LFODepth > 0) - { - track.UpdateChannels(); - } - } - } - if (_mixer.IsFadeDone()) - { - allDone = true; - } - if (allDone) - { - MixerProcess(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - } - } - _tempoStack += _tempo; - MixerProcess(); - if (playing) - { - _time.Wait(); - } - } - stop: - _time.Stop(); - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Structs.cs b/VG Music Studio.backup/Core/GBA/MP2K/Structs.cs deleted file mode 100644 index 2da7d2c4..00000000 --- a/VG Music Studio.backup/Core/GBA/MP2K/Structs.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class SongEntry - { - public int HeaderOffset { get; set; } - public short Player { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown { get; set; } - } - internal class SongHeader - { - public byte NumTracks { get; set; } - public byte NumBlocks { get; set; } - public byte Priority { get; set; } - public byte Reverb { get; set; } - public int VoiceTableOffset { get; set; } - [BinaryArrayVariableLength(nameof(NumTracks))] - public int[] TrackOffsets { get; set; } - } - internal class VoiceEntry - { - public byte Type { get; set; } // 0 - public byte RootKey { get; set; } // 1 - public byte Unknown { get; set; } // 2 - public byte Pan { get; set; } // 3 - /// SquarePattern for Square1/Square2, NoisePattern for Noise, Address for PCM8/PCM4/KeySplit/Drum - public int Int4 { get; set; } // 4 - /// ADSR for PCM8/Square1/Square2/PCM4/Noise, KeysAddress for KeySplit - public ADSR ADSR { get; set; } // 8 - [BinaryIgnore] - public int Int8 => (ADSR.R << 24) | (ADSR.S << 16) | (ADSR.D << 8) | (ADSR.A); - } - internal struct ADSR // Used as a struct in GBChannel - { - public byte A { get; set; } - public byte D { get; set; } - public byte S { get; set; } - public byte R { get; set; } - } - internal class GoldenSunPSG - { - /// Always 0x80 - public byte Unknown { get; set; } - public GoldenSunPSGType Type { get; set; } - public byte InitialCycle { get; set; } - public byte CycleSpeed { get; set; } - public byte CycleAmplitude { get; set; } - public byte MinimumCycle { get; set; } - } - internal class SampleHeader - { - /// 0x40000000 if True - public int DoesLoop { get; set; } - /// Right shift 10 for value - public int SampleRate { get; set; } - public int LoopOffset { get; set; } - public int Length { get; set; } - } - - internal struct ChannelVolume - { - public float LeftVol, RightVol; - } - internal struct Note - { - public byte Key, OriginalKey; - public byte Velocity; - /// -1 if forever - public int Duration; - } -} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Track.cs b/VG Music Studio.backup/Core/GBA/MP2K/Track.cs deleted file mode 100644 index 1288008a..00000000 --- a/VG Music Studio.backup/Core/GBA/MP2K/Track.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal class Track - { - public readonly byte Index; - private readonly int _startOffset; - public byte Voice; - public byte PitchBendRange; - public byte Priority; - public byte Volume; - public byte Rest; - public byte LFOPhase; - public byte LFODelayCount; - public byte LFOSpeed; - public byte LFODelay; - public byte LFODepth; - public LFOType LFOType; - public sbyte PitchBend; - public sbyte Tune; - public sbyte Panpot; - public sbyte Transpose; - public bool Ready; - public bool Stopped; - public int DataOffset; - public int[] CallStack = new int[3]; - public byte CallStackDepth; - public byte RunCmd; - public byte PrevKey; - public byte PrevVelocity; - - public readonly List Channels = new List(); - - public int GetPitch() - { - int lfo = LFOType == LFOType.Pitch ? (Utils.Tri(LFOPhase) * LFODepth) >> 8 : 0; - return (PitchBend * PitchBendRange) + Tune + lfo; - } - public byte GetVolume() - { - int lfo = LFOType == LFOType.Volume ? (Utils.Tri(LFOPhase) * LFODepth * 3 * Volume) >> 19 : 0; - int v = Volume + lfo; - if (v < 0) - { - v = 0; - } - else if (v > 0x7F) - { - v = 0x7F; - } - return (byte)v; - } - public sbyte GetPanpot() - { - int lfo = LFOType == LFOType.Panpot ? (Utils.Tri(LFOPhase) * LFODepth * 3) >> 12 : 0; - int p = Panpot + lfo; - if (p < -0x40) - { - p = -0x40; - } - else if (p > 0x3F) - { - p = 0x3F; - } - return (sbyte)p; - } - - public Track(byte i, int startOffset) - { - Index = i; - _startOffset = startOffset; - } - public void Init() - { - Voice = 0; - Priority = 0; - Rest = 0; - LFODelay = 0; - LFODelayCount = 0; - LFOPhase = 0; - LFODepth = 0; - CallStackDepth = 0; - PitchBend = 0; - Tune = 0; - Panpot = 0; - Transpose = 0; - DataOffset = _startOffset; - RunCmd = 0; - PrevKey = 0; - PrevVelocity = 0x7F; - PitchBendRange = 2; - LFOType = LFOType.Pitch; - Ready = false; - Stopped = false; - LFOSpeed = 22; - Volume = 100; - StopAllChannels(); - } - public void Tick() - { - if (Rest != 0) - { - Rest--; - } - if (LFODepth > 0) - { - LFOPhase += LFOSpeed; - } - else - { - LFOPhase = 0; - } - int active = 0; - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - if (chans[i].TickNote()) - { - active++; - } - } - if (active != 0) - { - if (LFODelayCount > 0) - { - LFODelayCount--; - LFOPhase = 0; - } - } - else - { - LFODelayCount = LFODelay; - } - if ((LFODelay == LFODelayCount && LFODelay != 0) || LFOSpeed == 0) - { - LFOPhase = 0; - } - } - - public void ReleaseChannels(int key) - { - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - Channel c = chans[i]; - if (c.Note.OriginalKey == key && c.Note.Duration == -1) - { - c.Release(); - } - } - } - public void StopAllChannels() - { - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - chans[i].Stop(); - } - } - public void UpdateChannels() - { - byte vol = GetVolume(); - sbyte pan = GetPanpot(); - int pitch = GetPitch(); - for (int i = 0; i < Channels.Count; i++) - { - Channel c = Channels[i]; - c.SetVolume(vol, pan); - c.SetPitch(pitch); - } - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/MP2K/Utils.cs b/VG Music Studio.backup/Core/GBA/MP2K/Utils.cs deleted file mode 100644 index fc10fbba..00000000 --- a/VG Music Studio.backup/Core/GBA/MP2K/Utils.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.GBA.MP2K -{ - internal static class Utils - { - public static readonly byte[] RestTable = new byte[49] - { - 00, 01, 02, 03, 04, 05, 06, 07, - 08, 09, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, - 24, 28, 30, 32, 36, 40, 42, 44, - 48, 52, 54, 56, 60, 64, 66, 68, - 72, 76, 78, 80, 84, 88, 90, 92, - 96 - }; - public static readonly (int sampleRate, int samplesPerBuffer)[] FrequencyTable = new (int, int)[12] - { - (05734, 096), // 59.72916666666667 - (07884, 132), // 59.72727272727273 - (10512, 176), // 59.72727272727273 - (13379, 224), // 59.72767857142857 - (15768, 264), // 59.72727272727273 - (18157, 304), // 59.72697368421053 - (21024, 352), // 59.72727272727273 - (26758, 448), // 59.72767857142857 - (31536, 528), // 59.72727272727273 - (36314, 608), // 59.72697368421053 - (40137, 672), // 59.72767857142857 - (42048, 704) // 59.72727272727273 - }; - - // Squares - public static readonly float[] SquareD12 = new float[8] { 0.875f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f, -0.125f }; - public static readonly float[] SquareD25 = new float[8] { 0.750f, 0.750f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f, -0.250f }; - public static readonly float[] SquareD50 = new float[8] { 0.500f, 0.500f, 0.500f, 0.500f, -0.500f, -0.500f, -0.500f, -0.500f }; - public static readonly float[] SquareD75 = new float[8] { 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, 0.250f, -0.750f, -0.750f }; - - // Noises - public static readonly BitArray NoiseFine; - public static readonly BitArray NoiseRough; - public static readonly byte[] NoiseFrequencyTable = new byte[60] - { - 0xD7, 0xD6, 0xD5, 0xD4, - 0xC7, 0xC6, 0xC5, 0xC4, - 0xB7, 0xB6, 0xB5, 0xB4, - 0xA7, 0xA6, 0xA5, 0xA4, - 0x97, 0x96, 0x95, 0x94, - 0x87, 0x86, 0x85, 0x84, - 0x77, 0x76, 0x75, 0x74, - 0x67, 0x66, 0x65, 0x64, - 0x57, 0x56, 0x55, 0x54, - 0x47, 0x46, 0x45, 0x44, - 0x37, 0x36, 0x35, 0x34, - 0x27, 0x26, 0x25, 0x24, - 0x17, 0x16, 0x15, 0x14, - 0x07, 0x06, 0x05, 0x04, - 0x03, 0x02, 0x01, 0x00 - }; - - // PCM4 - public static float[] PCM4ToFloat(int sampleOffset) - { - var config = (Config)Engine.Instance.Config; - float[] sample = new float[0x20]; - float sum = 0; - for (int i = 0; i < 0x10; i++) - { - byte b = config.ROM[sampleOffset + i]; - float first = (b >> 4) / 16f; - float second = (b & 0xF) / 16f; - sum += sample[i * 2] = first; - sum += sample[(i * 2) + 1] = second; - } - float dcCorrection = sum / 0x20; - for (int i = 0; i < 0x20; i++) - { - sample[i] -= dcCorrection; - } - return sample; - } - - // Pokémon Only - private static readonly sbyte[] _compressionLookup = new sbyte[16] - { - 0, 1, 4, 9, 16, 25, 36, 49, -64, -49, -36, -25, -16, -9, -4, -1 - }; - public static sbyte[] Decompress(int sampleOffset, int sampleLength) - { - var config = (Config)Engine.Instance.Config; - var samples = new List(); - sbyte compressionLevel = 0; - int compressionByte = 0, compressionIdx = 0; - - for (int i = 0; true; i++) - { - byte b = config.ROM[sampleOffset + i]; - if (compressionByte == 0) - { - compressionByte = 0x20; - compressionLevel = (sbyte)b; - samples.Add(compressionLevel); - if (++compressionIdx >= sampleLength) - { - break; - } - } - else - { - if (compressionByte < 0x20) - { - compressionLevel += _compressionLookup[b >> 4]; - samples.Add(compressionLevel); - if (++compressionIdx >= sampleLength) - { - break; - } - } - compressionByte--; - compressionLevel += _compressionLookup[b & 0xF]; - samples.Add(compressionLevel); - if (++compressionIdx >= sampleLength) - { - break; - } - } - } - - return samples.ToArray(); - } - - static Utils() - { - NoiseFine = new BitArray(0x8000); - int reg = 0x4000; - for (int i = 0; i < NoiseFine.Length; i++) - { - if ((reg & 1) == 1) - { - reg >>= 1; - reg ^= 0x6000; - NoiseFine[i] = true; - } - else - { - reg >>= 1; - NoiseFine[i] = false; - } - } - NoiseRough = new BitArray(0x80); - reg = 0x40; - for (int i = 0; i < NoiseRough.Length; i++) - { - if ((reg & 1) == 1) - { - reg >>= 1; - reg ^= 0x60; - NoiseRough[i] = true; - } - else - { - reg >>= 1; - NoiseRough[i] = false; - } - } - } - public static int Tri(int index) - { - index = (index - 64) & 0xFF; - return (index < 128) ? (index * 12) - 768 : 2304 - (index * 12); - } - } -} diff --git a/VG Music Studio.backup/Core/GBA/Utils.cs b/VG Music Studio.backup/Core/GBA/Utils.cs deleted file mode 100644 index d5858818..00000000 --- a/VG Music Studio.backup/Core/GBA/Utils.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.GBA -{ - internal static class Utils - { - public const double AGB_FPS = 59.7275; - public const int SystemClock = 16777216; // 16.777216 MHz (16*1024*1024 Hz) - - public const int CartridgeOffset = 0x08000000; - public const int CartridgeCapacity = 0x02000000; - - public static readonly string[] PSGTypes = new string[4] { "Square 1", "Square 2", "PCM4", "Noise" }; - } -} diff --git a/VG Music Studio.backup/Core/GlobalConfig.cs b/VG Music Studio.backup/Core/GlobalConfig.cs deleted file mode 100644 index 4b3b57f7..00000000 --- a/VG Music Studio.backup/Core/GlobalConfig.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.IO; -using YamlDotNet.RepresentationModel; - -namespace Kermalis.VGMusicStudio.Core -{ - internal enum PlaylistMode : byte - { - Random, - Sequential - } - - internal sealed class GlobalConfig - { - public static GlobalConfig Instance { get; private set; } - - public readonly bool TaskbarProgress; - public readonly ushort RefreshRate; - public readonly bool CenterIndicators; - public readonly bool PanpotIndicators; - public readonly PlaylistMode PlaylistMode; - public readonly long PlaylistSongLoops; - public readonly long PlaylistFadeOutMilliseconds; - public readonly sbyte MiddleCOctave; - public readonly HSLColor[] Colors; - - private GlobalConfig() - { - const string configFile = "Config.yaml"; - using (StreamReader fileStream = File.OpenText(Utils.CombineWithBaseDirectory(configFile))) - { - try - { - var yaml = new YamlStream(); - yaml.Load(fileStream); - - var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; - TaskbarProgress = mapping.GetValidBoolean(nameof(TaskbarProgress)); - RefreshRate = (ushort)mapping.GetValidValue(nameof(RefreshRate), 1, 1000); - CenterIndicators = mapping.GetValidBoolean(nameof(CenterIndicators)); - PanpotIndicators = mapping.GetValidBoolean(nameof(PanpotIndicators)); - PlaylistMode = mapping.GetValidEnum(nameof(PlaylistMode)); - PlaylistSongLoops = mapping.GetValidValue(nameof(PlaylistSongLoops), 0, long.MaxValue); - PlaylistFadeOutMilliseconds = mapping.GetValidValue(nameof(PlaylistFadeOutMilliseconds), 0, long.MaxValue); - MiddleCOctave = (sbyte)mapping.GetValidValue(nameof(MiddleCOctave), sbyte.MinValue, sbyte.MaxValue); - - var cmap = (YamlMappingNode)mapping.Children[nameof(Colors)]; - Colors = new HSLColor[256]; - foreach (KeyValuePair c in cmap) - { - int i = (int)Utils.ParseValue(string.Format(Strings.ConfigKeySubkey, nameof(Colors)), c.Key.ToString(), 0, 127); - if (Colors[i] != null) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigColorRepeated, i))); - } - double h = 0, s = 0, l = 0; - foreach (KeyValuePair v in ((YamlMappingNode)c.Value).Children) - { - string key = v.Key.ToString(); - string valueName = string.Format(Strings.ConfigKeySubkey, string.Format("{0} {1}", nameof(Colors), i)); - if (key == "H") - { - h = Utils.ParseValue(valueName, v.Value.ToString(), 0, 240); - } - else if (key == "S") - { - s = Utils.ParseValue(valueName, v.Value.ToString(), 0, 240); - } - else if (key == "L") - { - l = Utils.ParseValue(valueName, v.Value.ToString(), 0, 240); - } - else - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigColorInvalidKey, i))); - } - } - var co = new HSLColor(h, s, l); - Colors[i] = co; - Colors[i + 128] = co; - } - for (int i = 0; i < Colors.Length; i++) - { - if (Colors[i] == null) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigColorMissing, i))); - } - } - } - catch (BetterKeyNotFoundException ex) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + string.Format(Strings.ErrorConfigKeyMissing, ex.Key))); - } - catch (Exception ex) when (ex is InvalidValueException || ex is YamlDotNet.Core.YamlException) - { - throw new Exception(string.Format(Strings.ErrorParseConfig, configFile, Environment.NewLine + ex.Message)); - } - } - } - - public static void Init() - { - Instance = new GlobalConfig(); - } - } -} diff --git a/VG Music Studio.backup/Core/Mixer.cs b/VG Music Studio.backup/Core/Mixer.cs deleted file mode 100644 index e0a242f6..00000000 --- a/VG Music Studio.backup/Core/Mixer.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Kermalis.VGMusicStudio.UI; -using NAudio.CoreAudioApi; -using NAudio.CoreAudioApi.Interfaces; -using NAudio.Wave; -using System; - -namespace Kermalis.VGMusicStudio.Core -{ - internal abstract class Mixer : IAudioSessionEventsHandler, IDisposable - { - public readonly bool[] Mutes = new bool[SongInfoControl.SongInfo.MaxTracks]; - private IWavePlayer _out; - private AudioSessionControl _appVolume; - - protected void Init(IWaveProvider waveProvider) - { - _out = new WasapiOut(); - _out.Init(waveProvider); - using (var en = new MMDeviceEnumerator()) - { - SessionCollection sessions = en.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia).AudioSessionManager.Sessions; - int id = System.Diagnostics.Process.GetCurrentProcess().Id; - for (int i = 0; i < sessions.Count; i++) - { - AudioSessionControl session = sessions[i]; - if (session.GetProcessID == id) - { - _appVolume = session; - _appVolume.RegisterEventClient(this); - break; - } - } - } - _out.Play(); - } - - private bool _volChange = true; - public void OnVolumeChanged(float volume, bool isMuted) - { - if (_volChange) - { - MainForm.Instance.SetVolumeBarValue(volume); - } - _volChange = true; - } - public void OnDisplayNameChanged(string displayName) - { - throw new NotImplementedException(); - } - public void OnIconPathChanged(string iconPath) - { - throw new NotImplementedException(); - } - public void OnChannelVolumeChanged(uint channelCount, IntPtr newVolumes, uint channelIndex) - { - throw new NotImplementedException(); - } - public void OnGroupingParamChanged(ref Guid groupingId) - { - throw new NotImplementedException(); - } - // Fires on @out.Play() and @out.Stop() - public void OnStateChanged(AudioSessionState state) - { - if (state == AudioSessionState.AudioSessionStateActive) - { - OnVolumeChanged(_appVolume.SimpleAudioVolume.Volume, _appVolume.SimpleAudioVolume.Mute); - } - } - public void OnSessionDisconnected(AudioSessionDisconnectReason disconnectReason) - { - throw new NotImplementedException(); - } - public void SetVolume(float volume) - { - _volChange = false; - _appVolume.SimpleAudioVolume.Volume = volume; - } - - public virtual void Dispose() - { - _out.Stop(); - _out.Dispose(); - _appVolume.Dispose(); - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Channel.cs b/VG Music Studio.backup/Core/NDS/DSE/Channel.cs deleted file mode 100644 index 2ff239db..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/Channel.cs +++ /dev/null @@ -1,368 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Channel - { - public readonly byte Index; - - public Track Owner; - public EnvelopeState State; - public byte RootKey; - public byte Key; - public byte NoteVelocity; - public sbyte Panpot; // Not necessary - public ushort BaseTimer; - public ushort Timer; - public uint NoteLength; - public byte Volume; - - private int _pos; - private short _prevLeft; - private short _prevRight; - - private int _envelopeTimeLeft; - private int _volumeIncrement; - private int _velocity; // From 0-0x3FFFFFFF ((128 << 23) - 1) - private byte _targetVolume; - - private byte _attackVolume; - private byte _attack; - private byte _decay; - private byte _sustain; - private byte _hold; - private byte _decay2; - private byte _release; - - // PCM8, PCM16, ADPCM - private SWD.SampleBlock _sample; - // PCM8, PCM16 - private int _dataOffset; - // ADPCM - private ADPCMDecoder _adpcmDecoder; - private short _adpcmLoopLastSample; - private short _adpcmLoopStepIndex; - - public Channel(byte i) - { - Index = i; - } - - public bool StartPCM(SWD localswd, SWD masterswd, byte voice, int key, uint noteLength) - { - SWD.IProgramInfo programInfo = localswd.Programs.ProgramInfos[voice]; - if (programInfo != null) - { - for (int i = 0; i < programInfo.SplitEntries.Length; i++) - { - SWD.ISplitEntry split = programInfo.SplitEntries[i]; - if (key >= split.LowKey && key <= split.HighKey) - { - _sample = masterswd.Samples[split.SampleId]; - Key = (byte)key; - RootKey = split.SampleRootKey; - BaseTimer = (ushort)(NDS.Utils.ARM7_CLOCK / _sample.WavInfo.SampleRate); - if (_sample.WavInfo.SampleFormat == SampleFormat.ADPCM) - { - _adpcmDecoder = new ADPCMDecoder(_sample.Data); - } - //attackVolume = sample.WavInfo.AttackVolume == 0 ? split.AttackVolume : sample.WavInfo.AttackVolume; - //attack = sample.WavInfo.Attack == 0 ? split.Attack : sample.WavInfo.Attack; - //decay = sample.WavInfo.Decay == 0 ? split.Decay : sample.WavInfo.Decay; - //sustain = sample.WavInfo.Sustain == 0 ? split.Sustain : sample.WavInfo.Sustain; - //hold = sample.WavInfo.Hold == 0 ? split.Hold : sample.WavInfo.Hold; - //decay2 = sample.WavInfo.Decay2 == 0 ? split.Decay2 : sample.WavInfo.Decay2; - //release = sample.WavInfo.Release == 0 ? split.Release : sample.WavInfo.Release; - //attackVolume = split.AttackVolume == 0 ? sample.WavInfo.AttackVolume : split.AttackVolume; - //attack = split.Attack == 0 ? sample.WavInfo.Attack : split.Attack; - //decay = split.Decay == 0 ? sample.WavInfo.Decay : split.Decay; - //sustain = split.Sustain == 0 ? sample.WavInfo.Sustain : split.Sustain; - //hold = split.Hold == 0 ? sample.WavInfo.Hold : split.Hold; - //decay2 = split.Decay2 == 0 ? sample.WavInfo.Decay2 : split.Decay2; - //release = split.Release == 0 ? sample.WavInfo.Release : split.Release; - _attackVolume = split.AttackVolume == 0 ? _sample.WavInfo.AttackVolume == 0 ? (byte)0x7F : _sample.WavInfo.AttackVolume : split.AttackVolume; - _attack = split.Attack == 0 ? _sample.WavInfo.Attack == 0 ? (byte)0x7F : _sample.WavInfo.Attack : split.Attack; - _decay = split.Decay == 0 ? _sample.WavInfo.Decay == 0 ? (byte)0x7F : _sample.WavInfo.Decay : split.Decay; - _sustain = split.Sustain == 0 ? _sample.WavInfo.Sustain == 0 ? (byte)0x7F : _sample.WavInfo.Sustain : split.Sustain; - _hold = split.Hold == 0 ? _sample.WavInfo.Hold == 0 ? (byte)0x7F : _sample.WavInfo.Hold : split.Hold; - _decay2 = split.Decay2 == 0 ? _sample.WavInfo.Decay2 == 0 ? (byte)0x7F : _sample.WavInfo.Decay2 : split.Decay2; - _release = split.Release == 0 ? _sample.WavInfo.Release == 0 ? (byte)0x7F : _sample.WavInfo.Release : split.Release; - DetermineEnvelopeStartingPoint(); - _pos = 0; - _prevLeft = _prevRight = 0; - NoteLength = noteLength; - return true; - } - } - } - return false; - } - - public void Stop() - { - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = null; - Volume = 0; - } - - private bool CMDB1___sub_2074CA0() - { - bool b = true; - bool ge = _sample.WavInfo.EnvMult >= 0x7F; - bool ee = _sample.WavInfo.EnvMult == 0x7F; - if (_sample.WavInfo.EnvMult > 0x7F) - { - ge = _attackVolume >= 0x7F; - ee = _attackVolume == 0x7F; - } - if (!ee & ge - && _attack > 0x7F - && _decay > 0x7F - && _sustain > 0x7F - && _hold > 0x7F - && _decay2 > 0x7F - && _release > 0x7F) - { - b = false; - } - return b; - } - private void DetermineEnvelopeStartingPoint() - { - State = EnvelopeState.Two; // This isn't actually placed in this func - bool atLeastOneThingIsValid = CMDB1___sub_2074CA0(); // Neither is this - if (atLeastOneThingIsValid) - { - if (_attack != 0) - { - _velocity = _attackVolume << 23; - State = EnvelopeState.Hold; - UpdateEnvelopePlan(0x7F, _attack); - } - else - { - _velocity = 0x7F << 23; - if (_hold != 0) - { - UpdateEnvelopePlan(0x7F, _hold); - State = EnvelopeState.Decay; - } - else if (_decay != 0) - { - UpdateEnvelopePlan(_sustain, _decay); - State = EnvelopeState.Decay2; - } - else - { - UpdateEnvelopePlan(0, _release); - State = EnvelopeState.Six; - } - } - // Unk1E = 1 - } - else if (State != EnvelopeState.One) // What should it be? - { - State = EnvelopeState.Zero; - _velocity = 0x7F << 23; - } - } - public void SetEnvelopePhase7_2074ED8() - { - if (State != EnvelopeState.Zero) - { - UpdateEnvelopePlan(0, _release); - State = EnvelopeState.Seven; - } - } - public int StepEnvelope() - { - if (State > EnvelopeState.Two) - { - if (_envelopeTimeLeft != 0) - { - _envelopeTimeLeft--; - _velocity += _volumeIncrement; - if (_velocity < 0) - { - _velocity = 0; - } - else if (_velocity > 0x3FFFFFFF) - { - _velocity = 0x3FFFFFFF; - } - } - else - { - _velocity = _targetVolume << 23; - switch (State) - { - default: return _velocity >> 23; // case 8 - case EnvelopeState.Hold: - { - if (_hold == 0) - { - goto LABEL_6; - } - else - { - UpdateEnvelopePlan(0x7F, _hold); - State = EnvelopeState.Decay; - } - break; - } - case EnvelopeState.Decay: - LABEL_6: - { - if (_decay == 0) - { - _velocity = _sustain << 23; - goto LABEL_9; - } - else - { - UpdateEnvelopePlan(_sustain, _decay); - State = EnvelopeState.Decay2; - } - break; - } - case EnvelopeState.Decay2: - LABEL_9: - { - if (_decay2 == 0) - { - goto LABEL_11; - } - else - { - UpdateEnvelopePlan(0, _decay2); - State = EnvelopeState.Six; - } - break; - } - case EnvelopeState.Six: - LABEL_11: - { - UpdateEnvelopePlan(0, 0); - State = EnvelopeState.Two; - break; - } - case EnvelopeState.Seven: - { - State = EnvelopeState.Eight; - _velocity = 0; - _envelopeTimeLeft = 0; - break; - } - } - } - } - return _velocity >> 23; - } - private void UpdateEnvelopePlan(byte targetVolume, int envelopeParam) - { - if (envelopeParam == 0x7F) - { - _volumeIncrement = 0; - _envelopeTimeLeft = int.MaxValue; - } - else - { - _targetVolume = targetVolume; - _envelopeTimeLeft = _sample.WavInfo.EnvMult == 0 - ? Utils.Duration32[envelopeParam] * 1000 / 10000 - : Utils.Duration16[envelopeParam] * _sample.WavInfo.EnvMult * 1000 / 10000; - _volumeIncrement = _envelopeTimeLeft == 0 ? 0 : ((targetVolume << 23) - _velocity) / _envelopeTimeLeft; - } - } - - public void Process(out short left, out short right) - { - if (Timer != 0) - { - int numSamples = (_pos + 0x100) / Timer; - _pos = (_pos + 0x100) % Timer; - // prevLeft and prevRight are stored because numSamples can be 0. - for (int i = 0; i < numSamples; i++) - { - short samp; - switch (_sample.WavInfo.SampleFormat) - { - case SampleFormat.PCM8: - { - // If hit end - if (_dataOffset >= _sample.Data.Length) - { - if (_sample.WavInfo.Loop) - { - _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = (short)((sbyte)_sample.Data[_dataOffset++] << 8); - break; - } - case SampleFormat.PCM16: - { - // If hit end - if (_dataOffset >= _sample.Data.Length) - { - if (_sample.WavInfo.Loop) - { - _dataOffset = (int)(_sample.WavInfo.LoopStart * 4); - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = (short)(_sample.Data[_dataOffset++] | (_sample.Data[_dataOffset++] << 8)); - break; - } - case SampleFormat.ADPCM: - { - // If just looped - if (_adpcmDecoder.DataOffset == _sample.WavInfo.LoopStart * 4 && !_adpcmDecoder.OnSecondNibble) - { - _adpcmLoopLastSample = _adpcmDecoder.LastSample; - _adpcmLoopStepIndex = _adpcmDecoder.StepIndex; - } - // If hit end - if (_adpcmDecoder.DataOffset >= _sample.Data.Length && !_adpcmDecoder.OnSecondNibble) - { - if (_sample.WavInfo.Loop) - { - _adpcmDecoder.DataOffset = (int)(_sample.WavInfo.LoopStart * 4); - _adpcmDecoder.StepIndex = _adpcmLoopStepIndex; - _adpcmDecoder.LastSample = _adpcmLoopLastSample; - _adpcmDecoder.OnSecondNibble = false; - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = _adpcmDecoder.GetSample(); - break; - } - default: samp = 0; break; - } - samp = (short)(samp * Volume / 0x7F); - _prevLeft = (short)(samp * (-Panpot + 0x40) / 0x80); - _prevRight = (short)(samp * (Panpot + 0x40) / 0x80); - } - } - left = _prevLeft; - right = _prevRight; - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Commands.cs b/VG Music Studio.backup/Core/NDS/DSE/Commands.cs deleted file mode 100644 index a7d8eaa3..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/Commands.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Drawing; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class ExpressionCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Expression"; - public string Arguments => Expression.ToString(); - - public byte Expression { get; set; } - } - internal class FinishCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Finish"; - public string Arguments => string.Empty; - } - internal class InvalidCommand : ICommand - { - public Color Color => Color.MediumVioletRed; - public string Label => $"Invalid 0x{Command:X}"; - public string Arguments => string.Empty; - - public byte Command { get; set; } - } - internal class LoopStartCommand : ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Loop Start"; - public string Arguments => $"0x{Offset:X}"; - - public long Offset { get; set; } - } - internal class NoteCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetPianoKeyName(Key)} {OctaveChange} {Velocity} {Duration}"; - - public byte Key { get; set; } - public sbyte OctaveChange { get; set; } - public byte Velocity { get; set; } - public uint Duration { get; set; } - } - internal class OctaveAddCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Add To Octave"; - public string Arguments => OctaveChange.ToString(); - - public sbyte OctaveChange { get; set; } - } - internal class OctaveSetCommand : ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Set Octave"; - public string Arguments => Octave.ToString(); - - public byte Octave { get; set; } - } - internal class PanpotCommand : ICommand - { - public Color Color => Color.GreenYellow; - public string Label => "Panpot"; - public string Arguments => Panpot.ToString(); - - public sbyte Panpot { get; set; } - } - internal class PitchBendCommand : ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend"; - public string Arguments => $"{(sbyte)Bend}, {(sbyte)(Bend >> 8)}"; - - public ushort Bend { get; set; } - } - internal class RestCommand : ICommand - { - public Color Color => Color.PaleVioletRed; - public string Label => "Rest"; - public string Arguments => Rest.ToString(); - - public uint Rest { get; set; } - } - internal class SkipBytesCommand : ICommand - { - public Color Color => Color.MediumVioletRed; - public string Label => $"Skip 0x{Command:X}"; - public string Arguments => string.Join(", ", SkippedBytes.Select(b => $"0x{b:X}")); - - public byte Command { get; set; } - public byte[] SkippedBytes { get; set; } - } - internal class TempoCommand : ICommand - { - public Color Color => Color.DeepSkyBlue; - public string Label => $"Tempo {Command - 0xA3}"; // The two possible tempo commands are 0xA4 and 0xA5 - public string Arguments => Tempo.ToString(); - - public byte Command { get; set; } - public byte Tempo { get; set; } - } - internal class UnknownCommand : ICommand - { - public Color Color => Color.MediumVioletRed; - public string Label => $"Unknown 0x{Command:X}"; - public string Arguments => string.Join(", ", Args.Select(b => $"0x{b:X}")); - - public byte Command { get; set; } - public byte[] Args { get; set; } - } - internal class VoiceCommand : ICommand - { - public Color Color => Color.DarkSalmon; - public string Label => "Voice"; - public string Arguments => Voice.ToString(); - - public byte Voice { get; set; } - } - internal class VolumeCommand : ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Volume"; - public string Arguments => Volume.ToString(); - - public byte Volume { get; set; } - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Config.cs b/VG Music Studio.backup/Core/NDS/DSE/Config.cs deleted file mode 100644 index 25f856ca..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/Config.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Kermalis.VGMusicStudio.Properties; -using System; -using System.IO; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Config : Core.Config - { - public readonly string BGMPath; - public readonly string[] BGMFiles; - - public Config(string bgmPath) - { - BGMPath = bgmPath; - BGMFiles = Directory.GetFiles(bgmPath, "bgm*.smd", SearchOption.TopDirectoryOnly); - if (BGMFiles.Length == 0) - { - throw new Exception(Strings.ErrorDSENoSequences); - } - var songs = new Song[BGMFiles.Length]; - for (int i = 0; i < BGMFiles.Length; i++) - { - using (var reader = new EndianBinaryReader(File.OpenRead(BGMFiles[i]))) - { - SMD.Header header = reader.ReadObject(); - songs[i] = new Song(i, $"{Path.GetFileNameWithoutExtension(BGMFiles[i])} - {new string(header.Label.TakeWhile(c => c != '\0').ToArray())}"); - } - } - Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); - } - - public override string GetGameName() - { - return "DSE"; - } - public override string GetSongName(long index) - { - return index < 0 || index >= BGMFiles.Length - ? index.ToString() - : '\"' + BGMFiles[index] + '\"'; - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Enums.cs b/VG Music Studio.backup/Core/NDS/DSE/Enums.cs deleted file mode 100644 index 6cec60df..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/Enums.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal enum EnvelopeState : byte - { - Zero = 0, - One = 1, - Two = 2, - Hold = 3, - Decay = 4, - Decay2 = 5, - Six = 6, - Seven = 7, - Eight = 8 - } - - internal enum SampleFormat : ushort - { - PCM8 = 0x000, - PCM16 = 0x100, - ADPCM = 0x200 - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Mixer.cs b/VG Music Studio.backup/Core/NDS/DSE/Mixer.cs deleted file mode 100644 index 29c8de54..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/Mixer.cs +++ /dev/null @@ -1,220 +0,0 @@ -using NAudio.Wave; -using System; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Mixer : Core.Mixer - { - private const int _numChannels = 0x20; - private readonly float _samplesReciprocal; - private readonly int _samplesPerBuffer; - private bool _isFading; - private long _fadeMicroFramesLeft; - private float _fadePos; - private float _fadeStepPerMicroframe; - - private readonly Channel[] _channels; - private readonly BufferedWaveProvider _buffer; - - public Mixer() - { - // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. - // - gbatek - // I'm not using either of those because the samples per buffer leads to an overflow eventually - const int sampleRate = 65456; - _samplesPerBuffer = 341; // TODO - _samplesReciprocal = 1f / _samplesPerBuffer; - - _channels = new Channel[_numChannels]; - for (byte i = 0; i < _numChannels; i++) - { - _channels[i] = new Channel(i); - } - - _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) - { - DiscardOnBufferOverflow = true, - BufferLength = _samplesPerBuffer * 64 - }; - Init(_buffer); - } - public override void Dispose() - { - base.Dispose(); - CloseWaveWriter(); - } - - public Channel AllocateChannel() - { - int GetScore(Channel c) - { - // Free channels should be used before releasing channels - return c.Owner == null ? -2 : Utils.IsStateRemovable(c.State) ? -1 : 0; - } - Channel nChan = null; - for (int i = 0; i < _numChannels; i++) - { - Channel c = _channels[i]; - if (nChan != null) - { - int nScore = GetScore(nChan); - int cScore = GetScore(c); - if (cScore <= nScore && (cScore < nScore || c.Volume <= nChan.Volume)) - { - nChan = c; - } - } - else - { - nChan = c; - } - } - return nChan != null && 0 >= GetScore(nChan) ? nChan : null; - } - - public void ChannelTick() - { - for (int i = 0; i < _numChannels; i++) - { - Channel chan = _channels[i]; - if (chan.Owner != null) - { - chan.Volume = (byte)chan.StepEnvelope(); - if (chan.NoteLength == 0 && !Utils.IsStateRemovable(chan.State)) - { - chan.SetEnvelopePhase7_2074ED8(); - } - int vol = SDAT.Utils.SustainTable[chan.NoteVelocity] + SDAT.Utils.SustainTable[chan.Volume] + SDAT.Utils.SustainTable[chan.Owner.Volume] + SDAT.Utils.SustainTable[chan.Owner.Expression]; - //int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" - int pitch = (chan.Key - chan.RootKey) << 6; // "<< 6" is "* 0x40" - if (Utils.IsStateRemovable(chan.State) && vol <= -92544) - { - chan.Stop(); - } - else - { - chan.Volume = SDAT.Utils.GetChannelVolume(vol); - chan.Panpot = chan.Owner.Panpot; - chan.Timer = SDAT.Utils.GetChannelTimer(chan.BaseTimer, pitch); - } - } - } - } - - public void BeginFadeIn() - { - _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); - _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; - _isFading = true; - } - public void BeginFadeOut() - { - _fadePos = 1f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); - _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; - _isFading = true; - } - public bool IsFading() - { - return _isFading; - } - public bool IsFadeDone() - { - return _isFading && _fadeMicroFramesLeft == 0; - } - public void ResetFade() - { - _isFading = false; - _fadeMicroFramesLeft = 0; - } - - private WaveFileWriter _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter?.Dispose(); - } - public void Process(bool output, bool recording) - { - float masterStep; - float masterLevel; - if (_isFading && _fadeMicroFramesLeft == 0) - { - masterStep = 0; - masterLevel = 0; - } - else - { - float fromMaster = 1f; - float toMaster = 1f; - if (_fadeMicroFramesLeft > 0) - { - const float scale = 10f / 6f; - fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadePos += _fadeStepPerMicroframe; - toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadeMicroFramesLeft--; - } - masterStep = (toMaster - fromMaster) * _samplesReciprocal; - masterLevel = fromMaster; - } - byte[] b = new byte[4]; - for (int i = 0; i < _samplesPerBuffer; i++) - { - int left = 0, - right = 0; - for (int j = 0; j < _numChannels; j++) - { - Channel chan = _channels[j]; - if (chan.Owner != null) - { - bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null - chan.Process(out short channelLeft, out short channelRight); - if (!muted) - { - left += channelLeft; - right += channelRight; - } - } - } - float f = left * masterLevel; - if (f < short.MinValue) - { - f = short.MinValue; - } - else if (f > short.MaxValue) - { - f = short.MaxValue; - } - left = (int)f; - b[0] = (byte)left; - b[1] = (byte)(left >> 8); - f = right * masterLevel; - if (f < short.MinValue) - { - f = short.MinValue; - } - else if (f > short.MaxValue) - { - f = short.MaxValue; - } - right = (int)f; - b[2] = (byte)right; - b[3] = (byte)(right >> 8); - masterLevel += masterStep; - if (output) - { - _buffer.AddSamples(b, 0, 4); - } - if (recording) - { - _waveWriter.Write(b, 0, 4); - } - } - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Player.cs b/VG Music Studio.backup/Core/NDS/DSE/Player.cs deleted file mode 100644 index 4fa2b81e..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/Player.cs +++ /dev/null @@ -1,1040 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Player : IPlayer - { - private readonly Mixer _mixer; - private readonly Config _config; - private readonly TimeBarrier _time; - private Thread _thread; - private readonly SWD _masterSWD; - private SWD _localSWD; - private byte[] _smdFile; - private Track[] _tracks; - private byte _tempo; - private int _tempoStack; - private long _elapsedLoops; - - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; - - public PlayerState State { get; private set; } - public event SongEndedEvent SongEnded; - - public Player(Mixer mixer, Config config) - { - _mixer = mixer; - _config = config; - _masterSWD = new SWD(Path.Combine(config.BGMPath, "bgm.swd")); - - _time = new TimeBarrier(192); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "DSE Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) - { - _thread.Join(); - } - } - - private void InitEmulation() - { - _tempo = 120; - _tempoStack = 0; - _elapsedLoops = 0; - ElapsedTicks = 0; - _mixer.ResetFade(); - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - _tracks[trackIndex].Init(); - } - } - private void SetTicks() - { - MaxTicks = 0; - for (int trackIndex = 0; trackIndex < Events.Length; trackIndex++) - { - Events[trackIndex] = Events[trackIndex].OrderBy(e => e.Offset).ToList(); - List evs = Events[trackIndex]; - Track track = _tracks[trackIndex]; - track.Init(); - ElapsedTicks = 0; - while (true) - { - SongEvent e = evs.Single(ev => ev.Offset == track.CurOffset); - if (e.Ticks.Count > 0) - { - break; - } - else - { - e.Ticks.Add(ElapsedTicks); - ExecuteNext(track); - if (track.Stopped) - { - break; - } - else - { - ElapsedTicks += track.Rest; - track.Rest = 0; - } - } - } - if (ElapsedTicks > MaxTicks) - { - _longestTrack = trackIndex; - MaxTicks = ElapsedTicks; - } - track.StopAllChannels(); - } - } - public void LoadSong(long index) - { - if (_tracks != null) - { - for (int i = 0; i < _tracks.Length; i++) - { - _tracks[i].StopAllChannels(); - } - _tracks = null; - } - string bgm = _config.BGMFiles[index]; - _localSWD = new SWD(Path.ChangeExtension(bgm, "swd")); - _smdFile = File.ReadAllBytes(bgm); - using (var reader = new EndianBinaryReader(new MemoryStream(_smdFile))) - { - SMD.Header header = reader.ReadObject(); - SMD.ISongChunk songChunk; - switch (header.Version) - { - case 0x402: - { - songChunk = reader.ReadObject(); - break; - } - case 0x415: - { - songChunk = reader.ReadObject(); - break; - } - default: throw new Exception(string.Format(Strings.ErrorDSEInvalidHeaderVersion, header.Version)); - } - _tracks = new Track[songChunk.NumTracks]; - Events = new List[songChunk.NumTracks]; - for (byte trackIndex = 0; trackIndex < songChunk.NumTracks; trackIndex++) - { - Events[trackIndex] = new List(); - bool EventExists(long offset) - { - return Events[trackIndex].Any(e => e.Offset == offset); - } - - long chunkStart = reader.BaseStream.Position; - reader.BaseStream.Position += 0x14; // Skip header - _tracks[trackIndex] = new Track(trackIndex, reader.BaseStream.Position); - - uint lastNoteDuration = 0, lastRest = 0; - bool cont = true; - while (cont) - { - long offset = reader.BaseStream.Position; - void AddEvent(ICommand command) - { - Events[trackIndex].Add(new SongEvent(offset, command)); - } - byte cmd = reader.ReadByte(); - if (cmd <= 0x7F) - { - byte arg = reader.ReadByte(); - int numParams = (arg & 0xC0) >> 6; - int oct = ((arg & 0x30) >> 4) - 2; - int k = arg & 0xF; - if (k < 12) - { - uint duration; - if (numParams == 0) - { - duration = lastNoteDuration; - } - else // Big Endian reading of 8, 16, or 24 bits - { - duration = 0; - for (int b = 0; b < numParams; b++) - { - duration = (duration << 8) | reader.ReadByte(); - } - lastNoteDuration = duration; - } - if (!EventExists(offset)) - { - AddEvent(new NoteCommand { Key = (byte)k, OctaveChange = (sbyte)oct, Velocity = cmd, Duration = duration }); - } - } - else - { - throw new Exception(string.Format(Strings.ErrorDSEInvalidKey, trackIndex, offset, k)); - } - } - else if (cmd >= 0x80 && cmd <= 0x8F) - { - lastRest = Utils.FixedRests[cmd - 0x80]; - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - } - else // 0x90-0xFF - { - // TODO: 0x95 - a rest that may or may not repeat depending on some condition within channels - // TODO: 0x9E - may or may not jump somewhere else depending on an unknown structure - switch (cmd) - { - case 0x90: - { - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x91: - { - lastRest = (uint)(lastRest + reader.ReadSByte()); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x92: - { - lastRest = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x93: - { - lastRest = reader.ReadUInt16(); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x94: - { - lastRest = (uint)(reader.ReadByte() | (reader.ReadByte() << 8) | (reader.ReadByte() << 16)); - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = lastRest }); - } - break; - } - case 0x96: - case 0x97: - case 0x9A: - case 0x9B: - case 0x9F: - case 0xA2: - case 0xA3: - case 0xA6: - case 0xA7: - case 0xAD: - case 0xAE: - case 0xB7: - case 0xB8: - case 0xB9: - case 0xBA: - case 0xBB: - case 0xBD: - case 0xC1: - case 0xC2: - case 0xC4: - case 0xC5: - case 0xC6: - case 0xC7: - case 0xC8: - case 0xC9: - case 0xCA: - case 0xCC: - case 0xCD: - case 0xCE: - case 0xCF: - case 0xD9: - case 0xDA: - case 0xDE: - case 0xE6: - case 0xEB: - case 0xEE: - case 0xF4: - case 0xF5: - case 0xF7: - case 0xF9: - case 0xFA: - case 0xFB: - case 0xFC: - case 0xFD: - case 0xFE: - case 0xFF: - { - if (!EventExists(offset)) - { - AddEvent(new InvalidCommand { Command = cmd }); - } - break; - } - case 0x98: - { - if (!EventExists(offset)) - { - AddEvent(new FinishCommand()); - } - cont = false; - break; - } - case 0x99: - { - if (!EventExists(offset)) - { - AddEvent(new LoopStartCommand { Offset = reader.BaseStream.Position }); - } - break; - } - case 0xA0: - { - byte octave = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new OctaveSetCommand { Octave = octave }); - } - break; - } - case 0xA1: - { - sbyte change = reader.ReadSByte(); - if (!EventExists(offset)) - { - AddEvent(new OctaveAddCommand { OctaveChange = change }); - } - break; - } - case 0xA4: - case 0xA5: // The code for these two is identical - { - byte tempoArg = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new TempoCommand { Command = cmd, Tempo = tempoArg }); - } - break; - } - case 0xAB: - { - byte[] bytes = reader.ReadBytes(1); - if (!EventExists(offset)) - { - AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); - } - break; - } - case 0xAC: - { - byte voice = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = voice }); - } - break; - } - case 0xCB: - case 0xF8: - { - byte[] bytes = reader.ReadBytes(2); - if (!EventExists(offset)) - { - AddEvent(new SkipBytesCommand { Command = cmd, SkippedBytes = bytes }); - } - break; - } - case 0xD7: - { - ushort bend = reader.ReadUInt16(); - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = bend }); - } - break; - } - case 0xE0: - { - byte volume = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new VolumeCommand { Volume = volume }); - } - break; - } - case 0xE3: - { - byte expression = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new ExpressionCommand { Expression = expression }); - } - break; - } - case 0xE8: - { - byte panArg = reader.ReadByte(); - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = (sbyte)(panArg - 0x40) }); - } - break; - } - case 0x9D: - case 0xB0: - case 0xC0: - { - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = Array.Empty() }); - } - break; - } - case 0x9C: - case 0xA9: - case 0xAA: - case 0xB1: - case 0xB2: - case 0xB3: - case 0xB5: - case 0xB6: - case 0xBC: - case 0xBE: - case 0xBF: - case 0xC3: - case 0xD0: - case 0xD1: - case 0xD2: - case 0xDB: - case 0xDF: - case 0xE1: - case 0xE7: - case 0xE9: - case 0xEF: - case 0xF6: - { - byte[] args = reader.ReadBytes(1); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - case 0xA8: - case 0xB4: - case 0xD3: - case 0xD5: - case 0xD6: - case 0xD8: - case 0xF2: - { - byte[] args = reader.ReadBytes(2); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - case 0xAF: - case 0xD4: - case 0xE2: - case 0xEA: - case 0xF3: - { - byte[] args = reader.ReadBytes(3); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - case 0xDD: - case 0xE5: - case 0xED: - case 0xF1: - { - byte[] args = reader.ReadBytes(4); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - case 0xDC: - case 0xE4: - case 0xEC: - case 0xF0: - { - byte[] args = reader.ReadBytes(5); - if (!EventExists(offset)) - { - AddEvent(new UnknownCommand { Command = cmd, Args = args }); - } - break; - } - default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, trackIndex, offset, cmd)); - } - } - } - uint chunkLength = reader.ReadUInt32(chunkStart + 0xC); - reader.BaseStream.Position += chunkLength; - // Align 4 - while (reader.BaseStream.Position % 4 != 0) - { - reader.BaseStream.Position++; - } - } - SetTicks(); - } - } - public void SetCurrentPosition(long ticks) - { - if (_tracks == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - while (true) - { - if (ElapsedTicks == ticks) - { - goto finish; - } - else - { - while (_tempoStack >= 240) - { - _tempoStack -= 240; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (!track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track); - } - } - } - ElapsedTicks++; - if (ElapsedTicks == ticks) - { - goto finish; - } - } - _tempoStack += _tempo; - } - } - finish: - for (int i = 0; i < _tracks.Length; i++) - { - _tracks[i].StopAllChannels(); - } - Pause(); - } - } - public void Play() - { - if (_tracks == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); - } - } - public void Pause() - { - if (State == PlayerState.Playing) - { - State = PlayerState.Paused; - WaitThread(); - } - else if (State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.Playing; - CreateThread(); - } - } - public void Stop() - { - if (State == PlayerState.Playing || State == PlayerState.Paused) - { - State = PlayerState.Stopped; - WaitThread(); - } - } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() - { - if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.ShutDown; - WaitThread(); - } - } - public void GetSongState(UI.SongInfoControl.SongInfo info) - { - info.Tempo = _tempo; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - UI.SongInfoControl.SongInfo.Track tin = info.Tracks[trackIndex]; - tin.Position = track.CurOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.Type = "PCM"; - tin.Volume = track.Volume; - tin.PitchBend = track.PitchBend; - tin.Extra = track.Octave; - tin.Panpot = track.Panpot; - - Channel[] channels = track.Channels.ToArray(); - if (channels.Length == 0) - { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; - //tin.Type = string.Empty; - } - else - { - int numKeys = 0; - float left = 0f; - float right = 0f; - for (int j = 0; j < channels.Length; j++) - { - Channel c = channels[j]; - if (!Utils.IsStateRemovable(c.State)) - { - tin.Keys[numKeys++] = c.Key; - } - float a = (float)(-c.Panpot + 0x40) / 0x80 * c.Volume / 0x7F; - if (a > left) - { - left = a; - } - a = (float)(c.Panpot + 0x40) / 0x80 * c.Volume / 0x7F; - if (a > right) - { - right = a; - } - } - tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array - tin.LeftVolume = left; - tin.RightVolume = right; - //tin.Type = string.Join(", ", channels.Select(c => c.State.ToString())); - } - } - } - - private void ExecuteNext(Track track) - { - byte cmd = _smdFile[track.CurOffset++]; - if (cmd <= 0x7F) - { - byte arg = _smdFile[track.CurOffset++]; - int numParams = (arg & 0xC0) >> 6; - int oct = ((arg & 0x30) >> 4) - 2; - int k = arg & 0xF; - if (k < 12) - { - uint duration; - if (numParams == 0) - { - duration = track.LastNoteDuration; - } - else - { - duration = 0; - for (int b = 0; b < numParams; b++) - { - duration = (duration << 8) | _smdFile[track.CurOffset++]; - } - track.LastNoteDuration = duration; - } - Channel channel = _mixer.AllocateChannel(); - channel.Stop(); - track.Octave = (byte)(track.Octave + oct); - if (channel.StartPCM(_localSWD, _masterSWD, track.Voice, k + (12 * track.Octave), duration)) - { - channel.NoteVelocity = cmd; - channel.Owner = track; - track.Channels.Add(channel); - } - } - else - { - throw new Exception(string.Format(Strings.ErrorDSEInvalidKey, track.Index, track.CurOffset, k)); - } - } - else if (cmd >= 0x80 && cmd <= 0x8F) - { - track.LastRest = Utils.FixedRests[cmd - 0x80]; - track.Rest = track.LastRest; - } - else // 0x90-0xFF - { - // TODO: 0x95, 0x9E - switch (cmd) - { - case 0x90: - { - track.Rest = track.LastRest; - break; - } - case 0x91: - { - track.LastRest = (uint)(track.LastRest + (sbyte)_smdFile[track.CurOffset++]); - track.Rest = track.LastRest; - break; - } - case 0x92: - { - track.LastRest = _smdFile[track.CurOffset++]; - track.Rest = track.LastRest; - break; - } - case 0x93: - { - track.LastRest = (uint)(_smdFile[track.CurOffset++] | (_smdFile[track.CurOffset++] << 8)); - track.Rest = track.LastRest; - break; - } - case 0x94: - { - track.LastRest = (uint)(_smdFile[track.CurOffset++] | (_smdFile[track.CurOffset++] << 8) | (_smdFile[track.CurOffset++] << 16)); - track.Rest = track.LastRest; - break; - } - case 0x96: - case 0x97: - case 0x9A: - case 0x9B: - case 0x9F: - case 0xA2: - case 0xA3: - case 0xA6: - case 0xA7: - case 0xAD: - case 0xAE: - case 0xB7: - case 0xB8: - case 0xB9: - case 0xBA: - case 0xBB: - case 0xBD: - case 0xC1: - case 0xC2: - case 0xC4: - case 0xC5: - case 0xC6: - case 0xC7: - case 0xC8: - case 0xC9: - case 0xCA: - case 0xCC: - case 0xCD: - case 0xCE: - case 0xCF: - case 0xD9: - case 0xDA: - case 0xDE: - case 0xE6: - case 0xEB: - case 0xEE: - case 0xF4: - case 0xF5: - case 0xF7: - case 0xF9: - case 0xFA: - case 0xFB: - case 0xFC: - case 0xFD: - case 0xFE: - case 0xFF: - { - track.Stopped = true; - break; - } - case 0x98: - { - if (track.LoopOffset == -1) - { - track.Stopped = true; - } - else - { - track.CurOffset = track.LoopOffset; - } - break; - } - case 0x99: - { - track.LoopOffset = track.CurOffset; - break; - } - case 0xA0: - { - track.Octave = _smdFile[track.CurOffset++]; - break; - } - case 0xA1: - { - track.Octave = (byte)(track.Octave + (sbyte)_smdFile[track.CurOffset++]); - break; - } - case 0xA4: - case 0xA5: - { - _tempo = _smdFile[track.CurOffset++]; - break; - } - case 0xAB: - { - track.CurOffset++; - break; - } - case 0xAC: - { - track.Voice = _smdFile[track.CurOffset++]; - break; - } - case 0xCB: - case 0xF8: - { - track.CurOffset += 2; - break; - } - case 0xD7: - { - track.PitchBend = (ushort)(_smdFile[track.CurOffset++] | (_smdFile[track.CurOffset++] << 8)); - break; - } - case 0xE0: - { - track.Volume = _smdFile[track.CurOffset++]; - break; - } - case 0xE3: - { - track.Expression = _smdFile[track.CurOffset++]; - break; - } - case 0xE8: - { - track.Panpot = (sbyte)(_smdFile[track.CurOffset++] - 0x40); - break; - } - case 0x9D: - case 0xB0: - case 0xC0: - { - break; - } - case 0x9C: - case 0xA9: - case 0xAA: - case 0xB1: - case 0xB2: - case 0xB3: - case 0xB5: - case 0xB6: - case 0xBC: - case 0xBE: - case 0xBF: - case 0xC3: - case 0xD0: - case 0xD1: - case 0xD2: - case 0xDB: - case 0xDF: - case 0xE1: - case 0xE7: - case 0xE9: - case 0xEF: - case 0xF6: - { - track.CurOffset++; - break; - } - case 0xA8: - case 0xB4: - case 0xD3: - case 0xD5: - case 0xD6: - case 0xD8: - case 0xF2: - { - track.CurOffset += 2; - break; - } - case 0xAF: - case 0xD4: - case 0xE2: - case 0xEA: - case 0xF3: - { - track.CurOffset += 3; - break; - } - case 0xDD: - case 0xE5: - case 0xED: - case 0xF1: - { - track.CurOffset += 4; - break; - } - case 0xDC: - case 0xE4: - case 0xEC: - case 0xF0: - { - track.CurOffset += 5; - break; - } - default: throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, track.Index, track.CurOffset, cmd)); - } - } - } - - private void Tick() - { - _time.Start(); - while (true) - { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - goto stop; - } - - void MixerProcess() - { - _mixer.ChannelTick(); - _mixer.Process(playing, recording); - } - - while (_tempoStack >= 240) - { - _tempoStack -= 240; - bool allDone = true; - for (int trackIndex = 0; trackIndex < _tracks.Length; trackIndex++) - { - Track track = _tracks[trackIndex]; - track.Tick(); - while (track.Rest == 0 && !track.Stopped) - { - ExecuteNext(track); - } - if (trackIndex == _longestTrack) - { - if (ElapsedTicks == MaxTicks) - { - if (!track.Stopped) - { - List evs = Events[trackIndex]; - for (int i = 0; i < evs.Count; i++) - { - SongEvent ev = evs[i]; - if (ev.Offset == track.CurOffset) - { - ElapsedTicks = ev.Ticks[0] - track.Rest; - break; - } - } - _elapsedLoops++; - if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) - { - _mixer.BeginFadeOut(); - } - } - } - else - { - ElapsedTicks++; - } - } - if (!track.Stopped || track.Channels.Count != 0) - { - allDone = false; - } - } - if (_mixer.IsFadeDone()) - { - allDone = true; - } - if (allDone) - { - MixerProcess(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - } - } - _tempoStack += _tempo; - MixerProcess(); - if (playing) - { - _time.Wait(); - } - } - stop: - _time.Stop(); - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/SMD.cs b/VG Music Studio.backup/Core/NDS/DSE/SMD.cs deleted file mode 100644 index 706cb06c..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/SMD.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Kermalis.EndianBinaryIO; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class SMD - { - public class Header - { - [BinaryStringFixedLength(4)] - public string Type { get; set; } // "smdb" or "smdl" - [BinaryArrayFixedLength(4)] - public byte[] Unknown1 { get; set; } - public uint Length { get; set; } - public ushort Version { get; set; } - [BinaryArrayFixedLength(10)] - public byte[] Unknown2 { get; set; } - public ushort Year { get; set; } - public byte Month { get; set; } - public byte Day { get; set; } - public byte Hour { get; set; } - public byte Minute { get; set; } - public byte Second { get; set; } - public byte Centisecond { get; set; } - [BinaryStringFixedLength(16)] - public string Label { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown3 { get; set; } - } - - public interface ISongChunk - { - byte NumTracks { get; } - } - public class SongChunk_V402 : ISongChunk - { - [BinaryStringFixedLength(4)] - public string Type { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown1 { get; set; } - public byte NumTracks { get; set; } - public byte NumChannels { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown2 { get; set; } - public sbyte MasterVolume { get; set; } - public sbyte MasterPanpot { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown3 { get; set; } - } - public class SongChunk_V415 : ISongChunk - { - [BinaryStringFixedLength(4)] - public string Type { get; set; } - [BinaryArrayFixedLength(18)] - public byte[] Unknown1 { get; set; } - public byte NumTracks { get; set; } - public byte NumChannels { get; set; } - [BinaryArrayFixedLength(40)] - public byte[] Unknown2 { get; set; } - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/SWD.cs b/VG Music Studio.backup/Core/NDS/DSE/SWD.cs deleted file mode 100644 index 4e9f984a..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/SWD.cs +++ /dev/null @@ -1,471 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class SWD - { - public interface IHeader - { - - } - private class Header_V402 : IHeader - { - [BinaryArrayFixedLength(10)] - public byte[] Unknown1 { get; set; } - public ushort Year { get; set; } - public byte Month { get; set; } - public byte Day { get; set; } - public byte Hour { get; set; } - public byte Minute { get; set; } - public byte Second { get; set; } - public byte Centisecond { get; set; } - [BinaryStringFixedLength(16)] - public string Label { get; set; } - [BinaryArrayFixedLength(22)] - public byte[] Unknown2 { get; set; } - public byte NumWAVISlots { get; set; } - public byte NumPRGISlots { get; set; } - public byte NumKeyGroups { get; set; } - [BinaryArrayFixedLength(7)] - public byte[] Padding { get; set; } - } - private class Header_V415 : IHeader - { - [BinaryArrayFixedLength(10)] - public byte[] Unknown1 { get; set; } - public ushort Year { get; set; } - public byte Month { get; set; } - public byte Day { get; set; } - public byte Hour { get; set; } - public byte Minute { get; set; } - public byte Second { get; set; } - public byte Centisecond { get; set; } - [BinaryStringFixedLength(16)] - public string Label { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown2 { get; set; } - public uint PCMDLength { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } - public ushort NumWAVISlots { get; set; } - public ushort NumPRGISlots { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown4 { get; set; } - public uint WAVILength { get; set; } - } - - public interface ISplitEntry - { - byte LowKey { get; } - byte HighKey { get; } - int SampleId { get; } - byte SampleRootKey { get; } - sbyte SampleTranspose { get; } - byte AttackVolume { get; set; } - byte Attack { get; set; } - byte Decay { get; set; } - byte Sustain { get; set; } - byte Hold { get; set; } - byte Decay2 { get; set; } - byte Release { get; set; } - } - public class SplitEntry_V402 : ISplitEntry - { - public ushort Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public byte LowKey { get; set; } - public byte HighKey { get; set; } - public byte LowKey2 { get; set; } - public byte HighKey2 { get; set; } - public byte LowVelocity { get; set; } - public byte HighVelocity { get; set; } - public byte LowVelocity2 { get; set; } - public byte HighVelocity2 { get; set; } - [BinaryArrayFixedLength(5)] - public byte[] Unknown2 { get; set; } - public byte SampleId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } - public byte SampleRootKey { get; set; } - public sbyte SampleTranspose { get; set; } - public byte SampleVolume { get; set; } - public sbyte SamplePanpot { get; set; } - public byte KeyGroupId { get; set; } - [BinaryArrayFixedLength(15)] - public byte[] Unknown4 { get; set; } - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown5 { get; set; } - - int ISplitEntry.SampleId => SampleId; - } - public class SplitEntry_V415 : ISplitEntry - { - public ushort Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public byte LowKey { get; set; } - public byte HighKey { get; set; } - public byte LowKey2 { get; set; } - public byte HighKey2 { get; set; } - public byte LowVelocity { get; set; } - public byte HighVelocity { get; set; } - public byte LowVelocity2 { get; set; } - public byte HighVelocity2 { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown2 { get; set; } - public ushort SampleId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown3 { get; set; } - public byte SampleRootKey { get; set; } - public sbyte SampleTranspose { get; set; } - public byte SampleVolume { get; set; } - public sbyte SamplePanpot { get; set; } - public byte KeyGroupId { get; set; } - [BinaryArrayFixedLength(13)] - public byte[] Unknown4 { get; set; } - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown5 { get; set; } - - int ISplitEntry.SampleId => SampleId; - } - - public interface IProgramInfo - { - ISplitEntry[] SplitEntries { get; } - } - public class ProgramInfo_V402 : IProgramInfo - { - public byte Id { get; set; } - public byte NumSplits { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public byte Volume { get; set; } - public byte Panpot { get; set; } - [BinaryArrayFixedLength(5)] - public byte[] Unknown2 { get; set; } - public byte NumLFOs { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown3 { get; set; } - [BinaryArrayFixedLength(16)] - public KeyGroup[] KeyGroups { get; set; } - [BinaryArrayVariableLength(nameof(NumLFOs))] - public LFOInfo LFOInfos { get; set; } - [BinaryArrayVariableLength(nameof(NumSplits))] - public SplitEntry_V402[] SplitEntries { get; set; } - - [BinaryIgnore] - ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; - } - public class ProgramInfo_V415 : IProgramInfo - { - public ushort Id { get; set; } - public ushort NumSplits { get; set; } - public byte Volume { get; set; } - public byte Panpot { get; set; } - [BinaryArrayFixedLength(5)] - public byte[] Unknown1 { get; set; } - public byte NumLFOs { get; set; } - [BinaryArrayFixedLength(4)] - public byte[] Unknown2 { get; set; } - [BinaryArrayVariableLength(nameof(NumLFOs))] - public LFOInfo[] LFOInfos { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown3 { get; set; } - [BinaryArrayVariableLength(nameof(NumSplits))] - public SplitEntry_V415[] SplitEntries { get; set; } - - [BinaryIgnore] - ISplitEntry[] IProgramInfo.SplitEntries => SplitEntries; - } - - public interface IWavInfo - { - byte RootKey { get; } - sbyte Transpose { get; } - SampleFormat SampleFormat { get; } - bool Loop { get; } - uint SampleRate { get; } - uint SampleOffset { get; } - uint LoopStart { get; } - uint LoopEnd { get; } - byte EnvMult { get; } - byte AttackVolume { get; } - byte Attack { get; } - byte Decay { get; } - byte Sustain { get; } - byte Hold { get; } - byte Decay2 { get; } - byte Release { get; } - } - public class WavInfo_V402 : IWavInfo - { - public byte Unknown1 { get; set; } - public byte Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown2 { get; set; } - public byte RootKey { get; set; } - public sbyte Transpose { get; set; } - public byte Volume { get; set; } - public sbyte Panpot { get; set; } - public SampleFormat SampleFormat { get; set; } - [BinaryArrayFixedLength(7)] - public byte[] Unknown3 { get; set; } - public bool Loop { get; set; } - public uint SampleRate { get; set; } - public uint SampleOffset { get; set; } - public uint LoopStart { get; set; } - public uint LoopEnd { get; set; } - [BinaryArrayFixedLength(16)] - public byte[] Unknown4 { get; set; } - public byte EnvOn { get; set; } - public byte EnvMult { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown5 { get; set; } - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown6 { get; set; } - } - public class WavInfo_V415 : IWavInfo - { - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public ushort Id { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown2 { get; set; } - public byte RootKey { get; set; } - public sbyte Transpose { get; set; } - public byte Volume { get; set; } - public sbyte Panpot { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown3 { get; set; } - public ushort Version { get; set; } - public SampleFormat SampleFormat { get; set; } - public byte Unknown4 { get; set; } - public bool Loop { get; set; } - public byte Unknown5 { get; set; } - public byte SamplesPer32Bits { get; set; } - public byte Unknown6 { get; set; } - public byte BitDepth { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown7 { get; set; } - public uint SampleRate { get; set; } - public uint SampleOffset { get; set; } - public uint LoopStart { get; set; } - public uint LoopEnd { get; set; } - public byte EnvOn { get; set; } - public byte EnvMult { get; set; } - [BinaryArrayFixedLength(6)] - public byte[] Unknown8 { get; set; } - public byte AttackVolume { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Hold { get; set; } - public byte Decay2 { get; set; } - public byte Release { get; set; } - public byte Unknown9 { get; set; } - } - - public class SampleBlock - { - public IWavInfo WavInfo; - public byte[] Data; - } - public class ProgramBank - { - public IProgramInfo[] ProgramInfos; - public KeyGroup[] KeyGroups; - } - public class KeyGroup - { - public ushort Id { get; set; } - public byte Poly { get; set; } - public byte Priority { get; set; } - public byte Low { get; set; } - public byte High { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown { get; set; } - } - public class LFOInfo - { - [BinaryArrayFixedLength(16)] - public byte[] Unknown { get; set; } - } - - public string Type; // "swdb" or "swdl" - public byte[] Unknown; - public uint Length; - public ushort Version; - public IHeader Header; - - public ProgramBank Programs; - public SampleBlock[] Samples; - - public SWD(string path) - { - using (var reader = new EndianBinaryReader(new MemoryStream(File.ReadAllBytes(path)))) - { - Type = reader.ReadString(4, false); - Unknown = reader.ReadBytes(4); - Length = reader.ReadUInt32(); - Version = reader.ReadUInt16(); - switch (Version) - { - case 0x402: - { - Header_V402 header = reader.ReadObject(); - Header = header; - Programs = ReadPrograms(reader, header.NumPRGISlots); - Samples = ReadSamples(reader, header.NumWAVISlots); - break; - } - case 0x415: - { - Header_V415 header = reader.ReadObject(); - Header = header; - Programs = ReadPrograms(reader, header.NumPRGISlots); - if (header.PCMDLength != 0 && (header.PCMDLength & 0xFFFF0000) != 0xAAAA0000) - { - Samples = ReadSamples(reader, header.NumWAVISlots); - } - break; - } - default: throw new InvalidDataException(); - } - } - } - - private static long FindChunk(EndianBinaryReader reader, string chunk) - { - long pos = -1; - long oldPosition = reader.BaseStream.Position; - reader.BaseStream.Position = 0; - while (reader.BaseStream.Position < reader.BaseStream.Length) - { - string str = reader.ReadString(4, false); - if (str == chunk) - { - pos = reader.BaseStream.Position - 4; - break; - } - switch (str) - { - case "swdb": - case "swdl": - { - reader.BaseStream.Position += 0x4C; - break; - } - default: - { - reader.BaseStream.Position += 0x8; - uint length = reader.ReadUInt32(); - reader.BaseStream.Position += length; - // Align 4 - while (reader.BaseStream.Position % 4 != 0) - { - reader.BaseStream.Position++; - } - break; - } - } - } - reader.BaseStream.Position = oldPosition; - return pos; - } - - private static SampleBlock[] ReadSamples(EndianBinaryReader reader, int numWAVISlots) where T : IWavInfo, new() - { - long waviChunkOffset = FindChunk(reader, "wavi"); - long pcmdChunkOffset = FindChunk(reader, "pcmd"); - if (waviChunkOffset == -1 || pcmdChunkOffset == -1) - { - throw new InvalidDataException(); - } - else - { - waviChunkOffset += 0x10; - pcmdChunkOffset += 0x10; - var samples = new SampleBlock[numWAVISlots]; - for (int i = 0; i < numWAVISlots; i++) - { - ushort offset = reader.ReadUInt16(waviChunkOffset + (2 * i)); - if (offset != 0) - { - T wavInfo = reader.ReadObject(offset + waviChunkOffset); - samples[i] = new SampleBlock - { - WavInfo = wavInfo, - Data = reader.ReadBytes((int)((wavInfo.LoopStart + wavInfo.LoopEnd) * 4), pcmdChunkOffset + wavInfo.SampleOffset) - }; - } - } - return samples; - } - } - private static ProgramBank ReadPrograms(EndianBinaryReader reader, int numPRGISlots) where T : IProgramInfo, new() - { - long chunkOffset = FindChunk(reader, "prgi"); - if (chunkOffset == -1) - { - return null; - } - else - { - chunkOffset += 0x10; - var programInfos = new IProgramInfo[numPRGISlots]; - for (int i = 0; i < programInfos.Length; i++) - { - ushort offset = reader.ReadUInt16(chunkOffset + (2 * i)); - if (offset != 0) - { - programInfos[i] = reader.ReadObject(offset + chunkOffset); - } - } - return new ProgramBank - { - ProgramInfos = programInfos, - KeyGroups = ReadKeyGroups(reader) - }; - } - } - private static KeyGroup[] ReadKeyGroups(EndianBinaryReader reader) - { - long chunkOffset = FindChunk(reader, "kgrp"); - if (chunkOffset == -1) - { - return Array.Empty(); - } - else - { - uint chunkLength = reader.ReadUInt32(chunkOffset + 0xC); - var keyGroups = new KeyGroup[chunkLength / 8]; // 8 is the size of a KeyGroup - for (int i = 0; i < keyGroups.Length; i++) - { - keyGroups[i] = reader.ReadObject(); - } - return keyGroups; - } - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Track.cs b/VG Music Studio.backup/Core/NDS/DSE/Track.cs deleted file mode 100644 index 9a330cab..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/Track.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal class Track - { - public readonly byte Index; - private readonly long _startOffset; - public byte Octave; - public byte Voice; - public byte Expression; - public byte Volume; - public sbyte Panpot; - public uint Rest; - public ushort PitchBend; - public long CurOffset; - public long LoopOffset; - public bool Stopped; - public uint LastNoteDuration; - public uint LastRest; - - public readonly List Channels = new List(0x10); - - public Track(byte i, long startOffset) - { - Index = i; - _startOffset = startOffset; - } - - public void Init() - { - Expression = 0; - Voice = 0; - Volume = 0; - Octave = 4; - Panpot = 0; - Rest = 0; - PitchBend = 0; - CurOffset = _startOffset; - LoopOffset = -1; - Stopped = false; - LastNoteDuration = 0; - LastRest = 0; - StopAllChannels(); - } - - public void Tick() - { - if (Rest > 0) - { - Rest--; - } - for (int i = 0; i < Channels.Count; i++) - { - Channel c = Channels[i]; - if (c.NoteLength > 0) - { - c.NoteLength--; - } - } - } - - public void StopAllChannels() - { - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - chans[i].Stop(); - } - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/DSE/Utils.cs b/VG Music Studio.backup/Core/NDS/DSE/Utils.cs deleted file mode 100644 index 1f16b1a5..00000000 --- a/VG Music Studio.backup/Core/NDS/DSE/Utils.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.DSE -{ - internal static class Utils - { - public static short[] Duration16 = new short[128] - { - 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, - 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, - 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, - 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F, - 0x0020, 0x0023, 0x0028, 0x002D, 0x0033, 0x0039, 0x0040, 0x0048, - 0x0050, 0x0058, 0x0062, 0x006D, 0x0078, 0x0083, 0x0090, 0x009E, - 0x00AC, 0x00BC, 0x00CC, 0x00DE, 0x00F0, 0x0104, 0x0119, 0x012F, - 0x0147, 0x0160, 0x017A, 0x0196, 0x01B3, 0x01D2, 0x01F2, 0x0214, - 0x0238, 0x025E, 0x0285, 0x02AE, 0x02D9, 0x0307, 0x0336, 0x0367, - 0x039B, 0x03D1, 0x0406, 0x0442, 0x047E, 0x04C4, 0x0500, 0x0546, - 0x058C, 0x0622, 0x0672, 0x06CC, 0x071C, 0x0776, 0x07DA, 0x0834, - 0x0898, 0x0906, 0x096A, 0x09D8, 0x0A50, 0x0ABE, 0x0B40, 0x0BB8, - 0x0C3A, 0x0CBC, 0x0D48, 0x0DDE, 0x0E6A, 0x0F00, 0x0FA0, 0x1040, - 0x10EA, 0x1194, 0x123E, 0x12F2, 0x13B0, 0x146E, 0x1536, 0x15FE, - 0x16D0, 0x17A2, 0x187E, 0x195A, 0x1A40, 0x1B30, 0x1C20, 0x1D1A, - 0x1E1E, 0x1F22, 0x2030, 0x2148, 0x2260, 0x2382, 0x2710, 0x7FFF - }; - public static int[] Duration32 = new int[128] - { - 0x00000000, 0x00000004, 0x00000007, 0x0000000A, 0x0000000F, 0x00000015, 0x0000001C, 0x00000024, - 0x0000002E, 0x0000003A, 0x00000048, 0x00000057, 0x00000068, 0x0000007B, 0x00000091, 0x000000A8, - 0x00000185, 0x000001BE, 0x000001FC, 0x0000023F, 0x00000288, 0x000002D6, 0x0000032A, 0x00000385, - 0x000003E5, 0x0000044C, 0x000004BA, 0x0000052E, 0x000005A9, 0x0000062C, 0x000006B5, 0x00000746, - 0x00000BCF, 0x00000CC0, 0x00000DBD, 0x00000EC6, 0x00000FDC, 0x000010FF, 0x0000122F, 0x0000136C, - 0x000014B6, 0x0000160F, 0x00001775, 0x000018EA, 0x00001A6D, 0x00001BFF, 0x00001DA0, 0x00001F51, - 0x00002C16, 0x00002E80, 0x00003100, 0x00003395, 0x00003641, 0x00003902, 0x00003BDB, 0x00003ECA, - 0x000041D0, 0x000044EE, 0x00004824, 0x00004B73, 0x00004ED9, 0x00005259, 0x000055F2, 0x000059A4, - 0x000074CC, 0x000079AB, 0x00007EAC, 0x000083CE, 0x00008911, 0x00008E77, 0x000093FF, 0x000099AA, - 0x00009F78, 0x0000A56A, 0x0000AB80, 0x0000B1BB, 0x0000B81A, 0x0000BE9E, 0x0000C547, 0x0000CC17, - 0x0000FD42, 0x000105CB, 0x00010E82, 0x00011768, 0x0001207E, 0x000129C4, 0x0001333B, 0x00013CE2, - 0x000146BB, 0x000150C5, 0x00015B02, 0x00016572, 0x00017015, 0x00017AEB, 0x000185F5, 0x00019133, - 0x0001E16D, 0x0001EF07, 0x0001FCE0, 0x00020AF7, 0x0002194F, 0x000227E6, 0x000236BE, 0x000245D7, - 0x00025532, 0x000264CF, 0x000274AE, 0x000284D0, 0x00029536, 0x0002A5E0, 0x0002B6CE, 0x0002C802, - 0x000341B0, 0x000355F8, 0x00036A90, 0x00037F79, 0x000394B4, 0x0003AA41, 0x0003C021, 0x0003D654, - 0x0003ECDA, 0x000403B5, 0x00041AE5, 0x0004326A, 0x00044A45, 0x00046277, 0x00047B00, 0x7FFFFFFF - }; - public static readonly byte[] FixedRests = new byte[0x10] - { - 96, 72, 64, 48, 36, 32, 24, 18, 16, 12, 9, 8, 6, 4, 3, 2 - }; - - public static bool IsStateRemovable(EnvelopeState state) - { - return state == EnvelopeState.Two || state >= EnvelopeState.Seven; - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Channel.cs b/VG Music Studio.backup/Core/NDS/SDAT/Channel.cs deleted file mode 100644 index 95b89010..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/Channel.cs +++ /dev/null @@ -1,387 +0,0 @@ -using System; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Channel - { - public readonly byte Index; - - public Track Owner; - public InstrumentType Type; - public EnvelopeState State; - public bool AutoSweep; - public byte BaseKey; - public byte Key; - public byte NoteVelocity; - public sbyte StartingPan; - public sbyte Pan; - public int SweepCounter; - public int SweepLength; - public short SweepPitch; - public int Velocity; // The SEQ Player treats 0 as the 100% amplitude value and -92544 (-723*128) as the 0% amplitude value. The starting ampltitude is 0% (-92544). - public byte Volume; // From 0x00-0x7F (Calculated from Utils) - public ushort BaseTimer; - public ushort Timer; - public int NoteDuration; - - private byte _attack; - private int _sustain; - private ushort _decay; - private ushort _release; - public byte LFORange; - public byte LFOSpeed; - public byte LFODepth; - public ushort LFODelay; - public ushort LFOPhase; - public int LFOParam; - public ushort LFODelayCount; - public LFOType LFOType; - public byte Priority; - - private int _pos; - private short _prevLeft; - private short _prevRight; - - // PCM8, PCM16, ADPCM - private SWAR.SWAV _swav; - // PCM8, PCM16 - private int _dataOffset; - // ADPCM - private ADPCMDecoder _adpcmDecoder; - private short _adpcmLoopLastSample; - private short _adpcmLoopStepIndex; - // PSG - private byte _psgDuty; - private int _psgCounter; - // Noise - private ushort _noiseCounter; - - public Channel(byte i) - { - Index = i; - } - - public void StartPCM(SWAR.SWAV swav, int noteDuration) - { - Type = InstrumentType.PCM; - _dataOffset = 0; - _swav = swav; - if (swav.Format == SWAVFormat.ADPCM) - { - _adpcmDecoder = new ADPCMDecoder(swav.Samples); - } - BaseTimer = swav.Timer; - Start(noteDuration); - } - public void StartPSG(byte duty, int noteDuration) - { - Type = InstrumentType.PSG; - _psgCounter = 0; - _psgDuty = duty; - BaseTimer = 8006; - Start(noteDuration); - } - public void StartNoise(int noteLength) - { - Type = InstrumentType.Noise; - _noiseCounter = 0x7FFF; - BaseTimer = 8006; - Start(noteLength); - } - - private void Start(int noteDuration) - { - State = EnvelopeState.Attack; - Velocity = -92544; - _pos = 0; - _prevLeft = _prevRight = 0; - NoteDuration = noteDuration; - } - - public void Stop() - { - if (Owner != null) - { - Owner.Channels.Remove(this); - } - Owner = null; - Volume = 0; - Priority = 0; - } - - public int SweepMain() - { - if (SweepPitch != 0 && SweepCounter < SweepLength) - { - int sweep = (int)(Math.BigMul(SweepPitch, SweepLength - SweepCounter) / SweepLength); - if (AutoSweep) - { - SweepCounter++; - } - return sweep; - } - else - { - return 0; - } - } - public void LFOTick() - { - if (LFODelayCount > 0) - { - LFODelayCount--; - LFOPhase = 0; - } - else - { - int param = LFORange * Utils.Sin(LFOPhase >> 8) * LFODepth; - if (LFOType == LFOType.Volume) - { - param = (param * 60) >> 14; - } - else - { - param >>= 8; - } - LFOParam = param; - int counter = LFOPhase + (LFOSpeed << 6); // "<< 6" is "* 0x40" - while (counter >= 0x8000) - { - counter -= 0x8000; - } - LFOPhase = (ushort)counter; - } - } - - public void SetAttack(int a) - { - _attack = Utils.AttackTable[a]; - } - public void SetDecay(int d) - { - _decay = Utils.DecayTable[d]; - } - public void SetSustain(byte s) - { - _sustain = Utils.SustainTable[s]; - } - public void SetRelease(int r) - { - _release = Utils.DecayTable[r]; - } - public void StepEnvelope() - { - switch (State) - { - case EnvelopeState.Attack: - { - Velocity = _attack * Velocity / 0xFF; - if (Velocity == 0) - { - State = EnvelopeState.Decay; - } - break; - } - case EnvelopeState.Decay: - { - Velocity -= _decay; - if (Velocity <= _sustain) - { - State = EnvelopeState.Sustain; - Velocity = _sustain; - } - break; - } - case EnvelopeState.Release: - { - Velocity -= _release; - if (Velocity < -92544) - { - Velocity = -92544; - } - break; - } - } - } - - /// EmulateProcess doesn't care about samples that loop; it only cares about ones that force the track to wait for them to end - public void EmulateProcess() - { - if (Timer != 0) - { - int numSamples = (_pos + 0x100) / Timer; - _pos = (_pos + 0x100) % Timer; - for (int i = 0; i < numSamples; i++) - { - if (Type == InstrumentType.PCM && !_swav.DoesLoop) - { - switch (_swav.Format) - { - case SWAVFormat.PCM8: - { - if (_dataOffset >= _swav.Samples.Length) - { - Stop(); - } - else - { - _dataOffset++; - } - return; - } - case SWAVFormat.PCM16: - { - if (_dataOffset >= _swav.Samples.Length) - { - Stop(); - } - else - { - _dataOffset += 2; - } - return; - } - case SWAVFormat.ADPCM: - { - if (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) - { - Stop(); - } - else - { - // This is a faster emulation of adpcmDecoder.GetSample() without caring about the sample - if (_adpcmDecoder.OnSecondNibble) - { - _adpcmDecoder.DataOffset++; - } - _adpcmDecoder.OnSecondNibble = !_adpcmDecoder.OnSecondNibble; - } - return; - } - } - } - } - } - } - public void Process(out short left, out short right) - { - if (Timer != 0) - { - int numSamples = (_pos + 0x100) / Timer; - _pos = (_pos + 0x100) % Timer; - // prevLeft and prevRight are stored because numSamples can be 0. - for (int i = 0; i < numSamples; i++) - { - short samp; - switch (Type) - { - case InstrumentType.PCM: - { - switch (_swav.Format) - { - case SWAVFormat.PCM8: - { - // If hit end - if (_dataOffset >= _swav.Samples.Length) - { - if (_swav.DoesLoop) - { - _dataOffset = _swav.LoopOffset * 4; - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = (short)((sbyte)_swav.Samples[_dataOffset++] << 8); - break; - } - case SWAVFormat.PCM16: - { - // If hit end - if (_dataOffset >= _swav.Samples.Length) - { - if (_swav.DoesLoop) - { - _dataOffset = _swav.LoopOffset * 4; - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = (short)(_swav.Samples[_dataOffset++] | (_swav.Samples[_dataOffset++] << 8)); - break; - } - case SWAVFormat.ADPCM: - { - // If just looped - if (_swav.DoesLoop && _adpcmDecoder.DataOffset == _swav.LoopOffset * 4 && !_adpcmDecoder.OnSecondNibble) - { - _adpcmLoopLastSample = _adpcmDecoder.LastSample; - _adpcmLoopStepIndex = _adpcmDecoder.StepIndex; - } - // If hit end - if (_adpcmDecoder.DataOffset >= _swav.Samples.Length && !_adpcmDecoder.OnSecondNibble) - { - if (_swav.DoesLoop) - { - _adpcmDecoder.DataOffset = _swav.LoopOffset * 4; - _adpcmDecoder.StepIndex = _adpcmLoopStepIndex; - _adpcmDecoder.LastSample = _adpcmLoopLastSample; - _adpcmDecoder.OnSecondNibble = false; - } - else - { - left = right = _prevLeft = _prevRight = 0; - Stop(); - return; - } - } - samp = _adpcmDecoder.GetSample(); - break; - } - default: samp = 0; break; - } - break; - } - case InstrumentType.PSG: - { - samp = _psgCounter <= _psgDuty ? short.MinValue : short.MaxValue; - _psgCounter++; - if (_psgCounter >= 8) - { - _psgCounter = 0; - } - break; - } - case InstrumentType.Noise: - { - if ((_noiseCounter & 1) != 0) - { - _noiseCounter = (ushort)((_noiseCounter >> 1) ^ 0x6000); - samp = -0x7FFF; - } - else - { - _noiseCounter = (ushort)(_noiseCounter >> 1); - samp = 0x7FFF; - } - break; - } - default: samp = 0; break; - } - samp = (short)(samp * Volume / 0x7F); - _prevLeft = (short)(samp * (-Pan + 0x40) / 0x80); - _prevRight = (short)(samp * (Pan + 0x40) / 0x80); - } - } - left = _prevLeft; - right = _prevRight; - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Commands.cs b/VG Music Studio.backup/Core/NDS/SDAT/Commands.cs deleted file mode 100644 index 7a83fe13..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/Commands.cs +++ /dev/null @@ -1,439 +0,0 @@ -using System; -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal abstract class SDATCommand - { - public bool RandMod { get; set; } - public bool VarMod { get; set; } - - protected string GetValues(int value, string ifNot) - { - return RandMod ? $"[{(short)value}, {(short)(value >> 16)}]" - : VarMod ? $"[{(byte)value}]" - : ifNot; - } - } - - internal class AllocTracksCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Alloc Tracks"; - public string Arguments => $"{Convert.ToString(Tracks, 2).PadLeft(16, '0')}b"; - - public ushort Tracks { get; set; } - } - internal class CallCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Call"; - public string Arguments => $"0x{Offset:X4}"; - - public int Offset { get; set; } - } - internal class FinishCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Finish"; - public string Arguments => string.Empty; - } - internal class ForceAttackCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "Force Attack"; - public string Arguments => GetValues(Attack, Attack.ToString()); - - public int Attack { get; set; } - } - internal class ForceDecayCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "Force Decay"; - public string Arguments => GetValues(Decay, Decay.ToString()); - - public int Decay { get; set; } - } - internal class ForceReleaseCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "Force Release"; - public string Arguments => GetValues(Release, Release.ToString()); - - public int Release { get; set; } - } - internal class ForceSustainCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "Force Sustain"; - public string Arguments => GetValues(Sustain, Sustain.ToString()); - - public int Sustain { get; set; } - } - internal class JumpCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Jump"; - public string Arguments => $"0x{Offset:X4}"; - - public int Offset { get; set; } - } - internal class LFODelayCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Delay"; - public string Arguments => GetValues(Delay, Delay.ToString()); - - public int Delay { get; set; } - } - internal class LFODepthCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Depth"; - public string Arguments => GetValues(Depth, Depth.ToString()); - - public int Depth { get; set; } - } - internal class LFORangeCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Range"; - public string Arguments => GetValues(Range, Range.ToString()); - - public int Range { get; set; } - } - internal class LFOSpeedCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Speed"; - public string Arguments => GetValues(Speed, Speed.ToString()); - - public int Speed { get; set; } - } - internal class LFOTypeCommand : SDATCommand, ICommand - { - public Color Color => Color.LightSteelBlue; - public string Label => "LFO Type"; - public string Arguments => GetValues(Type, Type.ToString()); - - public int Type { get; set; } - } - internal class LoopEndCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Loop End"; - public string Arguments => string.Empty; - } - internal class LoopStartCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Loop Start"; - public string Arguments => GetValues(NumLoops, NumLoops.ToString()); - - public int NumLoops { get; set; } - } - internal class ModIfCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "If Modifier"; - public string Arguments => string.Empty; - } - internal class ModRandCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Rand Modifier"; - public string Arguments => string.Empty; - } - internal class ModVarCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Modifier"; - public string Arguments => string.Empty; - } - internal class MonophonyCommand : SDATCommand, ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Monophony Toggle"; - public string Arguments => GetValues(Mono, (Mono == 1).ToString()); - - public int Mono { get; set; } - } - internal class NoteComand : SDATCommand, ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Note"; - public string Arguments => $"{Util.Utils.GetNoteName(Key)}, {Velocity}, {GetValues(Duration, Duration.ToString())}"; - - public byte Key { get; set; } - public byte Velocity { get; set; } - public int Duration { get; set; } - } - internal class OpenTrackCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Open Track"; - public string Arguments => $"{Track}, 0x{Offset:X4}"; - - public int Track { get; set; } - public int Offset { get; set; } - } - internal class PanpotCommand : SDATCommand, ICommand - { - public Color Color => Color.GreenYellow; - public string Label => "Panpot"; - public string Arguments => GetValues(Panpot, Panpot.ToString()); - - public int Panpot { get; set; } - } - internal class PitchBendCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend"; - public string Arguments => GetValues(Bend, Bend.ToString()); - - public int Bend { get; set; } - } - internal class PitchBendRangeCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Pitch Bend Range"; - public string Arguments => GetValues(Range, Range.ToString()); - - public int Range { get; set; } - } - internal class PlayerVolumeCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Player Volume"; - public string Arguments => GetValues(Volume, Volume.ToString()); - - public int Volume { get; set; } - } - internal class PortamentoControlCommand : SDATCommand, ICommand - { - public Color Color => Color.HotPink; - public string Label => "Portamento Control"; - public string Arguments => GetValues(Portamento, Portamento.ToString()); - - public int Portamento { get; set; } - } - internal class PortamentoToggleCommand : SDATCommand, ICommand - { - public Color Color => Color.HotPink; - public string Label => "Portamento Toggle"; - public string Arguments => GetValues(Portamento, (Portamento == 1).ToString()); - - public int Portamento { get; set; } - } - internal class PortamentoTimeCommand : SDATCommand, ICommand - { - public Color Color => Color.HotPink; - public string Label => "Portamento Time"; - public string Arguments => GetValues(Time, Time.ToString()); - - public int Time { get; set; } - } - internal class PriorityCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Priority"; - public string Arguments => GetValues(Priority, Priority.ToString()); - - public int Priority { get; set; } - } - internal class RestCommand : SDATCommand, ICommand - { - public Color Color => Color.PaleVioletRed; - public string Label => "Rest"; - public string Arguments => GetValues(Rest, Rest.ToString()); - - public int Rest { get; set; } - } - internal class ReturnCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumSpringGreen; - public string Label => "Return"; - public string Arguments => string.Empty; - } - internal class SweepPitchCommand : SDATCommand, ICommand - { - public Color Color => Color.MediumPurple; - public string Label => "Sweep Pitch"; - public string Arguments => GetValues(Pitch, Pitch.ToString()); - - public int Pitch { get; set; } - } - internal class TempoCommand : SDATCommand, ICommand - { - public Color Color => Color.DeepSkyBlue; - public string Label => "Tempo"; - public string Arguments => GetValues(Tempo, Tempo.ToString()); - - public int Tempo { get; set; } - } - internal class TieCommand : SDATCommand, ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Tie"; - public string Arguments => GetValues(Tie, (Tie == 1).ToString()); - - public int Tie { get; set; } - } - internal class TrackExpressionCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Track Expression"; - public string Arguments => GetValues(Expression, Expression.ToString()); - - public int Expression { get; set; } - } - internal class TrackVolumeCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Track Volume"; - public string Arguments => GetValues(Volume, Volume.ToString()); - - public int Volume { get; set; } - } - internal class TransposeCommand : SDATCommand, ICommand - { - public Color Color => Color.SkyBlue; - public string Label => "Transpose"; - public string Arguments => GetValues(Transpose, Transpose.ToString()); - - public int Transpose { get; set; } - } - internal class VarAddCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Add"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpEECommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var =="; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpGECommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var >="; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpGGCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var >"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpLECommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var <="; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpLLCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var <"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarCmpNECommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var !="; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarDivCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Div"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarMulCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Mul"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarPrintCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Print"; - public string Arguments => GetValues(Variable, Variable.ToString()); - - public int Variable { get; set; } - } - internal class VarRandCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Rand"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarSetCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Set"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarShiftCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Shift"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VarSubCommand : SDATCommand, ICommand - { - public Color Color => Color.SteelBlue; - public string Label => "Var Sub"; - public string Arguments => $"{Variable}, {GetValues(Argument, Argument.ToString())}"; - - public byte Variable { get; set; } - public int Argument { get; set; } - } - internal class VoiceCommand : SDATCommand, ICommand - { - public Color Color => Color.DarkSalmon; - public string Label => "Voice"; - public string Arguments => GetValues(Voice, Voice.ToString()); - - public int Voice { get; set; } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Config.cs b/VG Music Studio.backup/Core/NDS/SDAT/Config.cs deleted file mode 100644 index f7f52813..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/Config.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using System; -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Config : Core.Config - { - public readonly SDAT SDAT; - - public Config(SDAT sdat) - { - if (sdat.INFOBlock.SequenceInfos.NumEntries == 0) - { - throw new Exception(Strings.ErrorSDATNoSequences); - } - SDAT = sdat; - var songs = new List(sdat.INFOBlock.SequenceInfos.NumEntries); - for (int i = 0; i < sdat.INFOBlock.SequenceInfos.NumEntries; i++) - { - if (sdat.INFOBlock.SequenceInfos.Entries[i] != null) - { - songs.Add(new Song(i, sdat.SYMBBlock is null ? i.ToString() : sdat.SYMBBlock.SequenceSymbols.Entries[i])); - } - } - Playlists.Add(new Playlist(Strings.PlaylistMusic, songs)); - } - - public override string GetGameName() - { - return "SDAT"; - } - public override string GetSongName(long index) - { - return SDAT.SYMBBlock is null || index < 0 || index >= SDAT.SYMBBlock.SequenceSymbols.NumEntries - ? index.ToString() - : '\"' + SDAT.SYMBBlock.SequenceSymbols.Entries[index] + '\"'; - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Enums.cs b/VG Music Studio.backup/Core/NDS/SDAT/Enums.cs deleted file mode 100644 index 9f4aa423..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/Enums.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal enum EnvelopeState : byte - { - Attack, - Decay, - Sustain, - Release - } - internal enum ArgType : byte - { - None, - Byte, - Short, - VarLen, - Rand, - PlayerVar - } - - internal enum LFOType : byte - { - Pitch, - Volume, - Panpot - } - internal enum InstrumentType : byte - { - PCM = 0x1, - PSG = 0x2, - Noise = 0x3, - Drum = 0x10, - KeySplit = 0x11 - } - internal enum SWAVFormat : byte - { - PCM8, - PCM16, - ADPCM - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/FileHeader.cs b/VG Music Studio.backup/Core/NDS/SDAT/FileHeader.cs deleted file mode 100644 index 50b863e5..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/FileHeader.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class FileHeader : IBinarySerializable - { - public string FileType; - public ushort Endianness; - public ushort Version; - public int FileSize; - public ushort HeaderSize; // 16 - public ushort NumBlocks; - - public void Read(EndianBinaryReader er) - { - FileType = er.ReadString(4, false); - er.Endianness = EndianBinaryIO.Endianness.BigEndian; - Endianness = er.ReadUInt16(); - er.Endianness = Endianness == 0xFFFE ? EndianBinaryIO.Endianness.LittleEndian : EndianBinaryIO.Endianness.BigEndian; - Version = er.ReadUInt16(); - FileSize = er.ReadInt32(); - HeaderSize = er.ReadUInt16(); - NumBlocks = er.ReadUInt16(); - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs b/VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs deleted file mode 100644 index a42b4e43..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/Mixer.cs +++ /dev/null @@ -1,252 +0,0 @@ -using NAudio.Wave; -using System; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Mixer : Core.Mixer - { - private readonly float _samplesReciprocal; - private readonly int _samplesPerBuffer; - private bool _isFading; - private long _fadeMicroFramesLeft; - private float _fadePos; - private float _fadeStepPerMicroframe; - - public Channel[] Channels; - private readonly BufferedWaveProvider _buffer; - - public Mixer() - { - // The sampling frequency of the mixer is 1.04876 MHz with an amplitude resolution of 24 bits, but the sampling frequency after mixing with PWM modulation is 32.768 kHz with an amplitude resolution of 10 bits. - // - gbatek - // I'm not using either of those because the samples per buffer leads to an overflow eventually - const int sampleRate = 65456; - _samplesPerBuffer = 341; // TODO - _samplesReciprocal = 1f / _samplesPerBuffer; - - Channels = new Channel[0x10]; - for (byte i = 0; i < 0x10; i++) - { - Channels[i] = new Channel(i); - } - - _buffer = new BufferedWaveProvider(new WaveFormat(sampleRate, 16, 2)) - { - DiscardOnBufferOverflow = true, - BufferLength = _samplesPerBuffer * 64 - }; - Init(_buffer); - } - public override void Dispose() - { - base.Dispose(); - CloseWaveWriter(); - } - - private static readonly int[] _pcmChanOrder = new int[] { 4, 5, 6, 7, 2, 0, 3, 1, 8, 9, 10, 11, 14, 12, 15, 13 }; - private static readonly int[] _psgChanOrder = new int[] { 8, 9, 10, 11, 12, 13 }; - private static readonly int[] _noiseChanOrder = new int[] { 14, 15 }; - public Channel AllocateChannel(InstrumentType type, Track track) - { - int[] allowedChannels; - switch (type) - { - case InstrumentType.PCM: allowedChannels = _pcmChanOrder; break; - case InstrumentType.PSG: allowedChannels = _psgChanOrder; break; - case InstrumentType.Noise: allowedChannels = _noiseChanOrder; break; - default: return null; - } - Channel nChan = null; - for (int i = 0; i < allowedChannels.Length; i++) - { - Channel c = Channels[allowedChannels[i]]; - if (nChan != null && c.Priority >= nChan.Priority && (c.Priority != nChan.Priority || nChan.Volume <= c.Volume)) - { - continue; - } - nChan = c; - } - if (nChan == null || track.Priority < nChan.Priority) - { - return null; - } - return nChan; - } - - public void ChannelTick() - { - for (int i = 0; i < 0x10; i++) - { - Channel chan = Channels[i]; - if (chan.Owner != null) - { - chan.StepEnvelope(); - if (chan.NoteDuration == 0 && !chan.Owner.WaitingForNoteToFinishBeforeContinuingXD) - { - chan.Priority = 1; - chan.State = EnvelopeState.Release; - } - int vol = Utils.SustainTable[chan.NoteVelocity] + chan.Velocity + chan.Owner.GetVolume(); - int pitch = ((chan.Key - chan.BaseKey) << 6) + chan.SweepMain() + chan.Owner.GetPitch(); // "<< 6" is "* 0x40" - int pan = 0; - chan.LFOTick(); - switch (chan.LFOType) - { - case LFOType.Pitch: pitch += chan.LFOParam; break; - case LFOType.Volume: vol += chan.LFOParam; break; - case LFOType.Panpot: pan += chan.LFOParam; break; - } - if (chan.State == EnvelopeState.Release && vol <= -92544) - { - chan.Stop(); - } - else - { - chan.Volume = Utils.GetChannelVolume(vol); - chan.Timer = Utils.GetChannelTimer(chan.BaseTimer, pitch); - int p = chan.StartingPan + chan.Owner.GetPan() + pan; - if (p < -0x40) - { - p = -0x40; - } - else if (p > 0x3F) - { - p = 0x3F; - } - chan.Pan = (sbyte)p; - } - } - } - } - - public void BeginFadeIn() - { - _fadePos = 0f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); - _fadeStepPerMicroframe = 1f / _fadeMicroFramesLeft; - _isFading = true; - } - public void BeginFadeOut() - { - _fadePos = 1f; - _fadeMicroFramesLeft = (long)(GlobalConfig.Instance.PlaylistFadeOutMilliseconds / 1000.0 * 192); - _fadeStepPerMicroframe = -1f / _fadeMicroFramesLeft; - _isFading = true; - } - public bool IsFading() - { - return _isFading; - } - public bool IsFadeDone() - { - return _isFading && _fadeMicroFramesLeft == 0; - } - public void ResetFade() - { - _isFading = false; - _fadeMicroFramesLeft = 0; - } - - private WaveFileWriter _waveWriter; - public void CreateWaveWriter(string fileName) - { - _waveWriter = new WaveFileWriter(fileName, _buffer.WaveFormat); - } - public void CloseWaveWriter() - { - _waveWriter?.Dispose(); - } - public void EmulateProcess() - { - for (int i = 0; i < _samplesPerBuffer; i++) - { - for (int j = 0; j < 0x10; j++) - { - Channel chan = Channels[j]; - if (chan.Owner != null) - { - chan.EmulateProcess(); - } - } - } - } - public void Process(bool output, bool recording) - { - float masterStep; - float masterLevel; - if (_isFading && _fadeMicroFramesLeft == 0) - { - masterStep = 0; - masterLevel = 0; - } - else - { - float fromMaster = 1f; - float toMaster = 1f; - if (_fadeMicroFramesLeft > 0) - { - const float scale = 10f / 6f; - fromMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadePos += _fadeStepPerMicroframe; - toMaster *= (_fadePos < 0f) ? 0f : (float)Math.Pow(_fadePos, scale); - _fadeMicroFramesLeft--; - } - masterStep = (toMaster - fromMaster) * _samplesReciprocal; - masterLevel = fromMaster; - } - byte[] b = new byte[4]; - for (int i = 0; i < _samplesPerBuffer; i++) - { - int left = 0, - right = 0; - for (int j = 0; j < 0x10; j++) - { - Channel chan = Channels[j]; - if (chan.Owner != null) - { - bool muted = Mutes[chan.Owner.Index]; // Get mute first because chan.Process() can call chan.Stop() which sets chan.Owner to null - chan.Process(out short channelLeft, out short channelRight); - if (!muted) - { - left += channelLeft; - right += channelRight; - } - } - } - float f = left * masterLevel; - if (f < short.MinValue) - { - f = short.MinValue; - } - else if (f > short.MaxValue) - { - f = short.MaxValue; - } - left = (int)f; - b[0] = (byte)left; - b[1] = (byte)(left >> 8); - f = right * masterLevel; - if (f < short.MinValue) - { - f = short.MinValue; - } - else if (f > short.MaxValue) - { - f = short.MaxValue; - } - right = (int)f; - b[2] = (byte)right; - b[3] = (byte)(right >> 8); - masterLevel += masterStep; - if (output) - { - _buffer.AddSamples(b, 0, 4); - } - if (recording) - { - _waveWriter.Write(b, 0, 4); - } - } - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Player.cs b/VG Music Studio.backup/Core/NDS/SDAT/Player.cs deleted file mode 100644 index 2722786f..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/Player.cs +++ /dev/null @@ -1,1680 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Player : IPlayer - { - public readonly byte Priority = 0x40; - private readonly short[] _vars = new short[0x20]; // 16 player variables, then 16 global variables - private readonly Track[] _tracks = new Track[0x10]; - private readonly Mixer _mixer; - private readonly Config _config; - private readonly TimeBarrier _time; - private Thread _thread; - private int _randSeed; - private Random _rand; - private SDAT.INFO.SequenceInfo _seqInfo; - private SSEQ _sseq; - private SBNK _sbnk; - public byte Volume; - private ushort _tempo; - private int _tempoStack; - private long _elapsedLoops; - - public List[] Events { get; private set; } - public long MaxTicks { get; private set; } - public long ElapsedTicks { get; private set; } - public bool ShouldFadeOut { get; set; } - public long NumLoops { get; set; } - private int _longestTrack; - - public PlayerState State { get; private set; } - public event SongEndedEvent SongEnded; - - public Player(Mixer mixer, Config config) - { - for (byte i = 0; i < 0x10; i++) - { - _tracks[i] = new Track(i, this); - } - _mixer = mixer; - _config = config; - - _time = new TimeBarrier(192); - } - private void CreateThread() - { - _thread = new Thread(Tick) { Name = "SDAT Player Tick" }; - _thread.Start(); - } - private void WaitThread() - { - if (_thread != null && (_thread.ThreadState == ThreadState.Running || _thread.ThreadState == ThreadState.WaitSleepJoin)) - { - _thread.Join(); - } - } - - private void InitEmulation() - { - _tempo = 120; // Confirmed: default tempo is 120 (MKDS 75) - _tempoStack = 0; - _elapsedLoops = 0; - ElapsedTicks = 0; - _mixer.ResetFade(); - Volume = _seqInfo.Volume; - _rand = new Random(_randSeed); - for (int i = 0; i < 0x10; i++) - { - _tracks[i].Init(); - } - // Initialize player and global variables. Global variables should not have a global effect in this program. - for (int i = 0; i < 0x20; i++) - { - _vars[i] = i % 8 == 0 ? short.MaxValue : (short)0; - } - } - private void SetTicks() - { - // TODO: (NSMB 81) (Spirit Tracks 18) does not count all ticks because the songs keep jumping backwards while changing vars and then using ModIfCommand to change events - MaxTicks = 0; - for (int i = 0; i < 0x10; i++) - { - if (Events[i] != null) - { - Events[i] = Events[i].OrderBy(e => e.Offset).ToList(); - } - } - InitEmulation(); - bool[] done = new bool[0x10]; // We use this instead of track.Stopped just to be certain that emulating Monophony works as intended - while (_tracks.Any(t => t.Allocated && t.Enabled && !done[t.Index])) - { - while (_tempoStack >= 240) - { - _tempoStack -= 240; - for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) - { - Track track = _tracks[trackIndex]; - List evs = Events[trackIndex]; - if (track.Enabled && !track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) - { - SongEvent e = evs.Single(ev => ev.Offset == track.DataOffset); - ExecuteNext(track); - if (!done[trackIndex]) - { - e.Ticks.Add(ElapsedTicks); - bool b; - if (track.Stopped) - { - b = true; - } - else - { - SongEvent newE = evs.Single(ev => ev.Offset == track.DataOffset); - b = (track.CallStackDepth == 0 && newE.Ticks.Count > 0) // If we already counted the tick of this event and we're not looping/calling - || (track.CallStackDepth != 0 && track.CallStackLoops.All(l => l == 0) && newE.Ticks.Count > 0); // If we have "LoopStart (0)" and already counted the tick of this event - } - if (b) - { - done[trackIndex] = true; - if (ElapsedTicks > MaxTicks) - { - _longestTrack = trackIndex; - MaxTicks = ElapsedTicks; - } - } - } - } - } - } - ElapsedTicks++; - } - _tempoStack += _tempo; - _mixer.ChannelTick(); - _mixer.EmulateProcess(); - } - for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) - { - _tracks[trackIndex].StopAllChannels(); - } - } - public void LoadSong(long index) - { - Stop(); - SDAT.INFO.SequenceInfo oldSeqInfo = _seqInfo; - _seqInfo = _config.SDAT.INFOBlock.SequenceInfos.Entries[index]; - if (_seqInfo == null) - { - _sseq = null; - _sbnk = null; - Events = null; - } - else - { - if (oldSeqInfo == null || _seqInfo.Bank != oldSeqInfo.Bank) - { - _voiceTypeCache = new string[byte.MaxValue + 1]; - } - _sseq = new SSEQ(_config.SDAT.FATBlock.Entries[_seqInfo.FileId].Data); - SDAT.INFO.BankInfo bankInfo = _config.SDAT.INFOBlock.BankInfos.Entries[_seqInfo.Bank]; - _sbnk = new SBNK(_config.SDAT.FATBlock.Entries[bankInfo.FileId].Data); - for (int i = 0; i < 4; i++) - { - if (bankInfo.SWARs[i] != 0xFFFF) - { - _sbnk.SWARs[i] = new SWAR(_config.SDAT.FATBlock.Entries[_config.SDAT.INFOBlock.WaveArchiveInfos.Entries[bankInfo.SWARs[i]].FileId].Data); - } - } - _randSeed = new Random().Next(); - - // RECURSION INCOMING - Events = new List[0x10]; - AddTrackEvents(0, 0); - void AddTrackEvents(int i, int trackStartOffset) - { - if (Events[i] == null) - { - Events[i] = new List(); - } - int callStackDepth = 0; - AddEvents(trackStartOffset); - bool EventExists(long offset) - { - return Events[i].Any(e => e.Offset == offset); - } - void AddEvents(int startOffset) - { - int dataOffset = startOffset; - int ReadArg(ArgType type) - { - switch (type) - { - case ArgType.Byte: - { - return _sseq.Data[dataOffset++]; - } - case ArgType.Short: - { - return _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8); - } - case ArgType.VarLen: - { - int read = 0, value = 0; - byte b; - do - { - b = _sseq.Data[dataOffset++]; - value = (value << 7) | (b & 0x7F); - read++; - } - while (read < 4 && (b & 0x80) != 0); - return value; - } - case ArgType.Rand: - { - // Combine min and max into one int - return _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16) | (_sseq.Data[dataOffset++] << 24); - } - case ArgType.PlayerVar: - { - // Return var index - return _sseq.Data[dataOffset++]; - } - default: throw new Exception(); - } - } - bool cont = true; - while (cont) - { - bool @if = false; - int offset = dataOffset; - ArgType argOverrideType = ArgType.None; - again: - byte cmd = _sseq.Data[dataOffset++]; - void AddEvent(T command) where T : SDATCommand, ICommand - { - command.RandMod = argOverrideType == ArgType.Rand; - command.VarMod = argOverrideType == ArgType.PlayerVar; - Events[i].Add(new SongEvent(offset, command)); - } - void Invalid() - { - throw new Exception(string.Format(Strings.ErrorAlphaDreamDSEMP2KSDATInvalidCommand, i, offset, cmd)); - } - - if (cmd <= 0x7F) - { - byte velocity = _sseq.Data[dataOffset++]; - int duration = ReadArg(argOverrideType == ArgType.None ? ArgType.VarLen : argOverrideType); - if (!EventExists(offset)) - { - AddEvent(new NoteComand { Key = cmd, Velocity = velocity, Duration = duration }); - } - } - else - { - int cmdGroup = cmd & 0xF0; - if (cmdGroup == 0x80) - { - int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.VarLen : argOverrideType); - switch (cmd) - { - case 0x80: - { - if (!EventExists(offset)) - { - AddEvent(new RestCommand { Rest = arg }); - } - break; - } - case 0x81: // RAND PROGRAM: [BW2 (2249)] - { - if (!EventExists(offset)) - { - AddEvent(new VoiceCommand { Voice = arg }); // TODO: Bank change - } - break; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0x90) - { - switch (cmd) - { - case 0x93: - { - int trackIndex = _sseq.Data[dataOffset++]; - int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); - if (!EventExists(offset)) - { - AddEvent(new OpenTrackCommand { Track = trackIndex, Offset = offset24bit }); - AddTrackEvents(trackIndex, offset24bit); - } - break; - } - case 0x94: - { - int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); - if (!EventExists(offset)) - { - AddEvent(new JumpCommand { Offset = offset24bit }); - if (!EventExists(offset24bit)) - { - AddEvents(offset24bit); - } - } - if (!@if) - { - cont = false; - } - break; - } - case 0x95: - { - int offset24bit = _sseq.Data[dataOffset++] | (_sseq.Data[dataOffset++] << 8) | (_sseq.Data[dataOffset++] << 16); - if (!EventExists(offset)) - { - AddEvent(new CallCommand { Offset = offset24bit }); - } - if (callStackDepth < 3) - { - if (!EventExists(offset24bit)) - { - callStackDepth++; - AddEvents(offset24bit); - } - } - else - { - throw new Exception(string.Format(Strings.ErrorMP2KSDATNestedCalls, i)); - } - break; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0xA0) - { - switch (cmd) - { - case 0xA0: // [New Super Mario Bros (BGM_AMB_CHIKA)] [BW2 (1917, 1918)] - { - if (!EventExists(offset)) - { - AddEvent(new ModRandCommand()); - } - argOverrideType = ArgType.Rand; - offset++; - goto again; - } - case 0xA1: // [New Super Mario Bros (BGM_AMB_SABAKU)] - { - if (!EventExists(offset)) - { - AddEvent(new ModVarCommand()); - } - argOverrideType = ArgType.PlayerVar; - offset++; - goto again; - } - case 0xA2: // [Mario Kart DS (75)] [BW2 (1917, 1918)] - { - if (!EventExists(offset)) - { - AddEvent(new ModIfCommand()); - } - @if = true; - offset++; - goto again; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0xB0) - { - byte varIndex = _sseq.Data[dataOffset++]; - int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.Short : argOverrideType); - switch (cmd) - { - case 0xB0: - { - if (!EventExists(offset)) - { - AddEvent(new VarSetCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB1: - { - if (!EventExists(offset)) - { - AddEvent(new VarAddCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB2: - { - if (!EventExists(offset)) - { - AddEvent(new VarSubCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB3: - { - if (!EventExists(offset)) - { - AddEvent(new VarMulCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB4: - { - if (!EventExists(offset)) - { - AddEvent(new VarDivCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB5: - { - if (!EventExists(offset)) - { - AddEvent(new VarShiftCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB6: // [Mario Kart DS (75)] - { - if (!EventExists(offset)) - { - AddEvent(new VarRandCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB8: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpEECommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xB9: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpGECommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xBA: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpGGCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xBB: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpLECommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xBC: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpLLCommand { Variable = varIndex, Argument = arg }); - } - break; - } - case 0xBD: - { - if (!EventExists(offset)) - { - AddEvent(new VarCmpNECommand { Variable = varIndex, Argument = arg }); - } - break; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0xC0 || cmdGroup == 0xD0) - { - int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.Byte : argOverrideType); - switch (cmd) - { - case 0xC0: - { - if (!EventExists(offset)) - { - AddEvent(new PanpotCommand { Panpot = arg }); - } - break; - } - case 0xC1: - { - if (!EventExists(offset)) - { - AddEvent(new TrackVolumeCommand { Volume = arg }); - } - break; - } - case 0xC2: - { - if (!EventExists(offset)) - { - AddEvent(new PlayerVolumeCommand { Volume = arg }); - } - break; - } - case 0xC3: - { - if (!EventExists(offset)) - { - AddEvent(new TransposeCommand { Transpose = arg }); - } - break; - } - case 0xC4: - { - if (!EventExists(offset)) - { - AddEvent(new PitchBendCommand { Bend = arg }); - } - break; - } - case 0xC5: - { - if (!EventExists(offset)) - { - AddEvent(new PitchBendRangeCommand { Range = arg }); - } - break; - } - case 0xC6: - { - if (!EventExists(offset)) - { - AddEvent(new PriorityCommand { Priority = arg }); - } - break; - } - case 0xC7: - { - if (!EventExists(offset)) - { - AddEvent(new MonophonyCommand { Mono = arg }); - } - break; - } - case 0xC8: - { - if (!EventExists(offset)) - { - AddEvent(new TieCommand { Tie = arg }); - } - break; - } - case 0xC9: - { - if (!EventExists(offset)) - { - AddEvent(new PortamentoControlCommand { Portamento = arg }); - } - break; - } - case 0xCA: - { - if (!EventExists(offset)) - { - AddEvent(new LFODepthCommand { Depth = arg }); - } - break; - } - case 0xCB: - { - if (!EventExists(offset)) - { - AddEvent(new LFOSpeedCommand { Speed = arg }); - } - break; - } - case 0xCC: - { - if (!EventExists(offset)) - { - AddEvent(new LFOTypeCommand { Type = arg }); - } - break; - } - case 0xCD: - { - if (!EventExists(offset)) - { - AddEvent(new LFORangeCommand { Range = arg }); - } - break; - } - case 0xCE: - { - if (!EventExists(offset)) - { - AddEvent(new PortamentoToggleCommand { Portamento = arg }); - } - break; - } - case 0xCF: - { - if (!EventExists(offset)) - { - AddEvent(new PortamentoTimeCommand { Time = arg }); - } - break; - } - case 0xD0: - { - if (!EventExists(offset)) - { - AddEvent(new ForceAttackCommand { Attack = arg }); - } - break; - } - case 0xD1: - { - if (!EventExists(offset)) - { - AddEvent(new ForceDecayCommand { Decay = arg }); - } - break; - } - case 0xD2: - { - if (!EventExists(offset)) - { - AddEvent(new ForceSustainCommand { Sustain = arg }); - } - break; - } - case 0xD3: - { - if (!EventExists(offset)) - { - AddEvent(new ForceReleaseCommand { Release = arg }); - } - break; - } - case 0xD4: - { - if (!EventExists(offset)) - { - AddEvent(new LoopStartCommand { NumLoops = arg }); - } - break; - } - case 0xD5: - { - if (!EventExists(offset)) - { - AddEvent(new TrackExpressionCommand { Expression = arg }); - } - break; - } - case 0xD6: - { - if (!EventExists(offset)) - { - AddEvent(new VarPrintCommand { Variable = arg }); - } - break; - } - default: Invalid(); break; - } - } - else if (cmdGroup == 0xE0) - { - int arg = ReadArg(argOverrideType == ArgType.None ? ArgType.Short : argOverrideType); - switch (cmd) - { - case 0xE0: - { - if (!EventExists(offset)) - { - AddEvent(new LFODelayCommand { Delay = arg }); - } - break; - } - case 0xE1: - { - if (!EventExists(offset)) - { - AddEvent(new TempoCommand { Tempo = arg }); - } - break; - } - case 0xE3: - { - if (!EventExists(offset)) - { - AddEvent(new SweepPitchCommand { Pitch = arg }); - } - break; - } - default: Invalid(); break; - } - } - else // if (cmdGroup == 0xF0) - { - switch (cmd) - { - case 0xFC: // [HGSS(1353)] - { - if (!EventExists(offset)) - { - AddEvent(new LoopEndCommand()); - } - break; - } - case 0xFD: - { - if (!EventExists(offset)) - { - AddEvent(new ReturnCommand()); - } - if (!@if && callStackDepth != 0) - { - cont = false; - callStackDepth--; - } - break; - } - case 0xFE: - { - ushort bits = (ushort)ReadArg(ArgType.Short); - if (!EventExists(offset)) - { - AddEvent(new AllocTracksCommand { Tracks = bits }); - } - break; - } - case 0xFF: - { - if (!EventExists(offset)) - { - AddEvent(new FinishCommand()); - } - if (!@if) - { - cont = false; - } - break; - } - default: Invalid(); break; - } - } - } - } - } - } - SetTicks(); - } - } - public void SetCurrentPosition(long ticks) - { - if (_seqInfo == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - if (State == PlayerState.Playing) - { - Pause(); - } - InitEmulation(); - while (true) - { - if (ElapsedTicks == ticks) - { - goto finish; - } - else - { - while (_tempoStack >= 240) - { - _tempoStack -= 240; - for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (track.Enabled && !track.Stopped) - { - track.Tick(); - while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) - { - ExecuteNext(track); - } - } - } - ElapsedTicks++; - if (ElapsedTicks == ticks) - { - goto finish; - } - } - _tempoStack += _tempo; - _mixer.ChannelTick(); - _mixer.EmulateProcess(); - } - } - finish: - for (int i = 0; i < 0x10; i++) - { - _tracks[i].StopAllChannels(); - } - Pause(); - } - } - public void Play() - { - if (_seqInfo == null) - { - SongEnded?.Invoke(); - } - else if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - Stop(); - InitEmulation(); - State = PlayerState.Playing; - CreateThread(); - } - } - public void Pause() - { - if (State == PlayerState.Playing) - { - State = PlayerState.Paused; - WaitThread(); - } - else if (State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.Playing; - CreateThread(); - } - } - public void Stop() - { - if (State == PlayerState.Playing || State == PlayerState.Paused) - { - State = PlayerState.Stopped; - WaitThread(); - } - } - public void Record(string fileName) - { - _mixer.CreateWaveWriter(fileName); - InitEmulation(); - State = PlayerState.Recording; - CreateThread(); - WaitThread(); - _mixer.CloseWaveWriter(); - } - public void Dispose() - { - if (State == PlayerState.Playing || State == PlayerState.Paused || State == PlayerState.Stopped) - { - State = PlayerState.ShutDown; - WaitThread(); - } - } - private string[] _voiceTypeCache; - public void GetSongState(UI.SongInfoControl.SongInfo info) - { - info.Tempo = _tempo; - for (int i = 0; i < 0x10; i++) - { - Track track = _tracks[i]; - if (track.Enabled) - { - UI.SongInfoControl.SongInfo.Track tin = info.Tracks[i]; - tin.Position = track.DataOffset; - tin.Rest = track.Rest; - tin.Voice = track.Voice; - tin.LFO = track.LFODepth * track.LFORange; - if (_voiceTypeCache[track.Voice] == null) - { - if (_sbnk.NumInstruments <= track.Voice) - { - _voiceTypeCache[track.Voice] = "Empty"; - } - else - { - InstrumentType t = _sbnk.Instruments[track.Voice].Type; - switch (t) - { - case InstrumentType.PCM: _voiceTypeCache[track.Voice] = "PCM"; break; - case InstrumentType.PSG: _voiceTypeCache[track.Voice] = "PSG"; break; - case InstrumentType.Noise: _voiceTypeCache[track.Voice] = "Noise"; break; - case InstrumentType.Drum: _voiceTypeCache[track.Voice] = "Drum"; break; - case InstrumentType.KeySplit: _voiceTypeCache[track.Voice] = "Key Split"; break; - default: _voiceTypeCache[track.Voice] = string.Format("Invalid {0}", (byte)t); break; - } - } - } - tin.Type = _voiceTypeCache[track.Voice]; - tin.Volume = track.Volume; - tin.PitchBend = track.GetPitch(); - tin.Extra = track.Portamento ? track.PortamentoTime : (byte)0; - tin.Panpot = track.GetPan(); - - Channel[] channels = track.Channels.ToArray(); - if (channels.Length == 0) - { - tin.Keys[0] = byte.MaxValue; - tin.LeftVolume = 0f; - tin.RightVolume = 0f; - } - else - { - int numKeys = 0; - float left = 0f; - float right = 0f; - for (int j = 0; j < channels.Length; j++) - { - Channel c = channels[j]; - if (c.State != EnvelopeState.Release) - { - tin.Keys[numKeys++] = c.Key; - } - float a = (float)(-c.Pan + 0x40) / 0x80 * c.Volume / 0x7F; - if (a > left) - { - left = a; - } - a = (float)(c.Pan + 0x40) / 0x80 * c.Volume / 0x7F; - if (a > right) - { - right = a; - } - } - tin.Keys[numKeys] = byte.MaxValue; // There's no way for numKeys to be after the last index in the array - tin.LeftVolume = left; - tin.RightVolume = right; - } - } - } - } - - public void PlayNote(Track track, byte key, byte velocity, int duration) - { - Channel channel = null; - if (track.Tie && track.Channels.Count != 0) - { - channel = track.Channels.Last(); - channel.Key = key; - channel.NoteVelocity = velocity; - } - else - { - SBNK.InstrumentData inst = _sbnk.GetInstrumentData(track.Voice, key); - if (inst != null) - { - InstrumentType type = inst.Type; - channel = _mixer.AllocateChannel(type, track); - if (channel != null) - { - if (track.Tie) - { - duration = -1; - } - SBNK.InstrumentData.DataParam param = inst.Param; - byte release = param.Release; - if (release == 0xFF) - { - duration = -1; - release = 0; - } - bool started = false; - switch (type) - { - case InstrumentType.PCM: - { - ushort[] info = param.Info; - SWAR.SWAV swav = _sbnk.GetSWAV(info[1], info[0]); - if (swav != null) - { - channel.StartPCM(swav, duration); - started = true; - } - break; - } - case InstrumentType.PSG: - { - channel.StartPSG((byte)param.Info[0], duration); - started = true; - break; - } - case InstrumentType.Noise: - { - channel.StartNoise(duration); - started = true; - break; - } - } - channel.Stop(); - if (started) - { - channel.Key = key; - byte baseKey = param.BaseKey; - channel.BaseKey = type != InstrumentType.PCM && baseKey == 0x7F ? (byte)60 : baseKey; - channel.NoteVelocity = velocity; - channel.SetAttack(param.Attack); - channel.SetDecay(param.Decay); - channel.SetSustain(param.Sustain); - channel.SetRelease(release); - channel.StartingPan = (sbyte)(param.Pan - 0x40); - channel.Owner = track; - channel.Priority = track.Priority; - track.Channels.Add(channel); - } - else - { - return; - } - } - } - } - if (channel != null) - { - if (track.Attack != 0xFF) - { - channel.SetAttack(track.Attack); - } - if (track.Decay != 0xFF) - { - channel.SetDecay(track.Decay); - } - if (track.Sustain != 0xFF) - { - channel.SetSustain(track.Sustain); - } - if (track.Release != 0xFF) - { - channel.SetRelease(track.Release); - } - channel.SweepPitch = track.SweepPitch; - if (track.Portamento) - { - channel.SweepPitch += (short)((track.PortamentoKey - key) << 6); // "<< 6" is "* 0x40" - } - if (track.PortamentoTime != 0) - { - channel.SweepLength = (track.PortamentoTime * track.PortamentoTime * Math.Abs(channel.SweepPitch)) >> 11; // ">> 11" is "/ 0x800" - channel.AutoSweep = true; - } - else - { - channel.SweepLength = duration; - channel.AutoSweep = false; - } - channel.SweepCounter = 0; - } - } - private void ExecuteNext(Track track) - { - int ReadArg(ArgType type) - { - if (track.ArgOverrideType != ArgType.None) - { - type = track.ArgOverrideType; - } - switch (type) - { - case ArgType.Byte: - { - return _sseq.Data[track.DataOffset++]; - } - case ArgType.Short: - { - return _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8); - } - case ArgType.VarLen: - { - int read = 0, value = 0; - byte b; - do - { - b = _sseq.Data[track.DataOffset++]; - value = (value << 7) | (b & 0x7F); - read++; - } - while (read < 4 && (b & 0x80) != 0); - return value; - } - case ArgType.Rand: - { - short min = (short)(_sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8)); - short max = (short)(_sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8)); - return _rand.Next(min, max + 1); - } - case ArgType.PlayerVar: - { - byte varIndex = _sseq.Data[track.DataOffset++]; - return _vars[varIndex]; - } - default: throw new Exception(); - } - } - - bool resetOverride = true; - bool resetCmdWork = true; - byte cmd = _sseq.Data[track.DataOffset++]; - if (cmd < 0x80) // Notes - { - byte velocity = _sseq.Data[track.DataOffset++]; - int duration = ReadArg(ArgType.VarLen); - if (track.DoCommandWork) - { - int k = cmd + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - byte key = (byte)k; - PlayNote(track, key, velocity, duration); - track.PortamentoKey = key; - if (track.Mono) - { - track.Rest = duration; - if (duration == 0) - { - track.WaitingForNoteToFinishBeforeContinuingXD = true; - } - } - } - } - else - { - int cmdGroup = cmd & 0xF0; - switch (cmdGroup) - { - case 0x80: - { - int arg = ReadArg(ArgType.VarLen); - if (track.DoCommandWork) - { - switch (cmd) - { - case 0x80: // Rest - { - track.Rest = arg; - break; - } - case 0x81: // Program Change - { - if (arg <= byte.MaxValue) - { - track.Voice = (byte)arg; - } - break; - } - } - } - break; - } - case 0x90: - { - switch (cmd) - { - case 0x93: // Open Track - { - int index = _sseq.Data[track.DataOffset++]; - int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); - if (track.DoCommandWork && track.Index == 0) - { - Track other = _tracks[index]; - if (other.Allocated && !other.Enabled) - { - other.Enabled = true; - other.DataOffset = offset24bit; - } - } - break; - } - case 0x94: // Jump - { - int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); - if (track.DoCommandWork) - { - track.DataOffset = offset24bit; - } - break; - } - case 0x95: // Call - { - int offset24bit = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8) | (_sseq.Data[track.DataOffset++] << 16); - if (track.DoCommandWork && track.CallStackDepth < 3) - { - track.CallStack[track.CallStackDepth] = track.DataOffset; - track.CallStackLoops[track.CallStackDepth] = byte.MaxValue; // This is only necessary for SetTicks() to deal with LoopStart (0) - track.CallStackDepth++; - track.DataOffset = offset24bit; - } - break; - } - } - break; - } - case 0xA0: - { - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xA0: // Rand Mod - { - track.ArgOverrideType = ArgType.Rand; - resetOverride = false; - break; - } - case 0xA1: // Var Mod - { - track.ArgOverrideType = ArgType.PlayerVar; - resetOverride = false; - break; - } - case 0xA2: // If Mod - { - track.DoCommandWork = track.VariableFlag; - resetCmdWork = false; - break; - } - } - } - break; - } - case 0xB0: - { - byte varIndex = _sseq.Data[track.DataOffset++]; - short mathArg = (short)ReadArg(ArgType.Short); - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xB0: // VarSet - { - _vars[varIndex] = mathArg; - break; - } - case 0xB1: // VarAdd - { - _vars[varIndex] += mathArg; - break; - } - case 0xB2: // VarSub - { - _vars[varIndex] -= mathArg; - break; - } - case 0xB3: // VarMul - { - _vars[varIndex] *= mathArg; - break; - } - case 0xB4: // VarDiv - { - if (mathArg != 0) - { - _vars[varIndex] /= mathArg; - } - break; - } - case 0xB5: // VarShift - { - _vars[varIndex] = mathArg < 0 ? (short)(_vars[varIndex] >> -mathArg) : (short)(_vars[varIndex] << mathArg); - break; - } - case 0xB6: // VarRand - { - bool negate = false; - if (mathArg < 0) - { - negate = true; - mathArg = (short)-mathArg; - } - short val = (short)_rand.Next(mathArg + 1); - if (negate) - { - val = (short)-val; - } - _vars[varIndex] = val; - break; - } - case 0xB8: // VarCmpEE - { - track.VariableFlag = _vars[varIndex] == mathArg; - break; - } - case 0xB9: // VarCmpGE - { - track.VariableFlag = _vars[varIndex] >= mathArg; - break; - } - case 0xBA: // VarCmpGG - { - track.VariableFlag = _vars[varIndex] > mathArg; - break; - } - case 0xBB: // VarCmpLE - { - track.VariableFlag = _vars[varIndex] <= mathArg; - break; - } - case 0xBC: // VarCmpLL - { - track.VariableFlag = _vars[varIndex] < mathArg; - break; - } - case 0xBD: // VarCmpNE - { - track.VariableFlag = _vars[varIndex] != mathArg; - break; - } - } - } - break; - } - case 0xC0: - case 0xD0: - { - int cmdArg = ReadArg(ArgType.Byte); - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xC0: // Panpot - { - track.Panpot = (sbyte)(cmdArg - 0x40); - break; - } - case 0xC1: // Track Volume - { - track.Volume = (byte)cmdArg; - break; - } - case 0xC2: // Player Volume - { - Volume = (byte)cmdArg; - break; - } - case 0xC3: // Transpose - { - track.Transpose = (sbyte)cmdArg; - break; - } - case 0xC4: // Pitch Bend - { - track.PitchBend = (sbyte)cmdArg; - break; - } - case 0xC5: // Pitch Bend Range - { - track.PitchBendRange = (byte)cmdArg; - break; - } - case 0xC6: // Priority - { - track.Priority = (byte)(Priority + (byte)cmdArg); - break; - } - case 0xC7: // Mono - { - track.Mono = cmdArg == 1; - break; - } - case 0xC8: // Tie - { - track.Tie = cmdArg == 1; - track.StopAllChannels(); - break; - } - case 0xC9: // Portamento Control - { - int k = cmdArg + track.Transpose; - if (k < 0) - { - k = 0; - } - else if (k > 0x7F) - { - k = 0x7F; - } - track.PortamentoKey = (byte)k; - track.Portamento = true; - break; - } - case 0xCA: // LFO Depth - { - track.LFODepth = (byte)cmdArg; - break; - } - case 0xCB: // LFO Speed - { - track.LFOSpeed = (byte)cmdArg; - break; - } - case 0xCC: // LFO Type - { - track.LFOType = (LFOType)cmdArg; - break; - } - case 0xCD: // LFO Range - { - track.LFORange = (byte)cmdArg; - break; - } - case 0xCE: // Portamento Toggle - { - track.Portamento = cmdArg == 1; - break; - } - case 0xCF: // Portamento Time - { - track.PortamentoTime = (byte)cmdArg; - break; - } - case 0xD0: // Forced Attack - { - track.Attack = (byte)cmdArg; - break; - } - case 0xD1: // Forced Decay - { - track.Decay = (byte)cmdArg; - break; - } - case 0xD2: // Forced Sustain - { - track.Sustain = (byte)cmdArg; - break; - } - case 0xD3: // Forced Release - { - track.Release = (byte)cmdArg; - break; - } - case 0xD4: // Loop Start - { - if (track.CallStackDepth < 3) - { - track.CallStack[track.CallStackDepth] = track.DataOffset; - track.CallStackLoops[track.CallStackDepth] = (byte)cmdArg; - track.CallStackDepth++; - } - break; - } - case 0xD5: // Track Expression - { - track.Expression = (byte)cmdArg; - break; - } - } - } - break; - } - case 0xE0: - { - int cmdArg = ReadArg(ArgType.Short); - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xE0: // LFO Delay - { - track.LFODelay = (ushort)cmdArg; - break; - } - case 0xE1: // Tempo - { - _tempo = (ushort)cmdArg; - break; - } - case 0xE3: // Sweep Pitch - { - track.SweepPitch = (short)cmdArg; - break; - } - } - } - break; - } - case 0xF0: - { - if (track.DoCommandWork) - { - switch (cmd) - { - case 0xFC: // Loop End - { - if (track.CallStackDepth != 0) - { - byte count = track.CallStackLoops[track.CallStackDepth - 1]; - if (count != 0) - { - count--; - track.CallStackLoops[track.CallStackDepth - 1] = count; - if (count == 0) - { - track.CallStackDepth--; - break; - } - } - track.DataOffset = track.CallStack[track.CallStackDepth - 1]; - } - break; - } - case 0xFD: // Return - { - if (track.CallStackDepth != 0) - { - track.CallStackDepth--; - track.DataOffset = track.CallStack[track.CallStackDepth]; - track.CallStackLoops[track.CallStackDepth] = 0; // This is only necessary for SetTicks() to deal with LoopStart (0) - } - break; - } - case 0xFE: // Alloc Tracks - { - // Must be in the beginning of the first track to work - if (track.Index == 0 && track.DataOffset == 1) // == 1 because we read cmd already - { - // Track 1 enabled = bit 1 set, Track 4 enabled = bit 4 set, etc - int trackBits = _sseq.Data[track.DataOffset++] | (_sseq.Data[track.DataOffset++] << 8); - for (int i = 0; i < 0x10; i++) - { - if ((trackBits & (1 << i)) != 0) - { - _tracks[i].Allocated = true; - } - } - } - break; - } - case 0xFF: // Finish - { - track.Stopped = true; - break; - } - } - } - break; - } - } - } - if (resetOverride) - { - track.ArgOverrideType = ArgType.None; - } - if (resetCmdWork) - { - track.DoCommandWork = true; - } - } - - private void Tick() - { - _time.Start(); - while (true) - { - PlayerState state = State; - bool playing = state == PlayerState.Playing; - bool recording = state == PlayerState.Recording; - if (!playing && !recording) - { - goto stop; - } - - void MixerProcess() - { - for (int i = 0; i < 0x10; i++) - { - Track track = _tracks[i]; - if (track.Enabled) - { - track.UpdateChannels(); - } - } - _mixer.ChannelTick(); - _mixer.Process(playing, recording); - } - - while (_tempoStack >= 240) - { - _tempoStack -= 240; - bool allDone = true; - for (int trackIndex = 0; trackIndex < 0x10; trackIndex++) - { - Track track = _tracks[trackIndex]; - if (!track.Enabled) - { - continue; - } - track.Tick(); - while (track.Rest == 0 && !track.WaitingForNoteToFinishBeforeContinuingXD && !track.Stopped) - { - ExecuteNext(track); - } - if (trackIndex == _longestTrack) - { - if (ElapsedTicks == MaxTicks) - { - if (!track.Stopped) - { - List evs = Events[trackIndex]; - for (int i = 0; i < evs.Count; i++) - { - SongEvent ev = evs[i]; - if (ev.Offset == track.DataOffset) - { - //ElapsedTicks = ev.Ticks[0] - track.Rest; - ElapsedTicks = ev.Ticks.Count == 0 ? 0 : ev.Ticks[0] - track.Rest; // Prevent crashes with songs that don't load all ticks yet (See SetTicks()) - break; - } - } - _elapsedLoops++; - if (ShouldFadeOut && !_mixer.IsFading() && _elapsedLoops > NumLoops) - { - _mixer.BeginFadeOut(); - } - } - } - else - { - ElapsedTicks++; - } - } - if (!track.Stopped || track.Channels.Count != 0) - { - allDone = false; - } - } - if (_mixer.IsFadeDone()) - { - allDone = true; - } - if (allDone) - { - MixerProcess(); - State = PlayerState.Stopped; - SongEnded?.Invoke(); - goto stop; - } - } - _tempoStack += _tempo; - MixerProcess(); - if (playing) - { - _time.Wait(); - } - } - stop: - _time.Stop(); - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs b/VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs deleted file mode 100644 index c0b016d8..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/SBNK.cs +++ /dev/null @@ -1,184 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class SBNK - { - public class InstrumentData - { - public class DataParam - { - [BinaryArrayFixedLength(2)] - public ushort[] Info { get; set; } - public byte BaseKey { get; set; } - public byte Attack { get; set; } - public byte Decay { get; set; } - public byte Sustain { get; set; } - public byte Release { get; set; } - public byte Pan { get; set; } - } - - public InstrumentType Type { get; set; } - public byte Padding { get; set; } - public DataParam Param { get; set; } - } - public class Instrument : IBinarySerializable - { - public class DefaultData - { - public InstrumentData.DataParam Param { get; set; } - } - public class DrumSetData : IBinarySerializable - { - public byte MinNote; - public byte MaxNote; - public InstrumentData[] SubInstruments; - - public void Read(EndianBinaryReader er) - { - MinNote = er.ReadByte(); - MaxNote = er.ReadByte(); - SubInstruments = new InstrumentData[MaxNote - MinNote + 1]; - for (int i = 0; i < SubInstruments.Length; i++) - { - SubInstruments[i] = er.ReadObject(); - } - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - public class KeySplitData : IBinarySerializable - { - public byte[] KeyRegions; - public InstrumentData[] SubInstruments; - - public void Read(EndianBinaryReader er) - { - KeyRegions = er.ReadBytes(8); - int numSubInstruments = 0; - for (int i = 0; i < 8; i++) - { - if (KeyRegions[i] == 0) - { - break; - } - numSubInstruments++; - } - SubInstruments = new InstrumentData[numSubInstruments]; - for (int i = 0; i < numSubInstruments; i++) - { - SubInstruments[i] = er.ReadObject(); - } - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public InstrumentType Type; - public ushort DataOffset; - public byte Padding; - - public object Data; - - public void Read(EndianBinaryReader er) - { - Type = er.ReadEnum(); - DataOffset = er.ReadUInt16(); - Padding = er.ReadByte(); - - long p = er.BaseStream.Position; - switch (Type) - { - case InstrumentType.PCM: - case InstrumentType.PSG: - case InstrumentType.Noise: Data = er.ReadObject(DataOffset); break; - case InstrumentType.Drum: Data = er.ReadObject(DataOffset); break; - case InstrumentType.KeySplit: Data = er.ReadObject(DataOffset); break; - default: break; - } - er.BaseStream.Position = p; - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public FileHeader FileHeader { get; set; } // "SBNK" - [BinaryStringFixedLength(4)] - public string BlockType { get; set; } // "DATA" - public int BlockSize { get; set; } - [BinaryArrayFixedLength(32)] - public byte[] Padding { get; set; } - public int NumInstruments { get; set; } - [BinaryArrayVariableLength(nameof(NumInstruments))] - public Instrument[] Instruments { get; set; } - - [BinaryIgnore] - public SWAR[] SWARs { get; } = new SWAR[4]; - - public SBNK(byte[] bytes) - { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) - { - er.ReadIntoObject(this); - } - } - - public InstrumentData GetInstrumentData(int voice, int key) - { - if (voice >= NumInstruments) - { - return null; - } - else - { - switch (Instruments[voice].Type) - { - case InstrumentType.PCM: - case InstrumentType.PSG: - case InstrumentType.Noise: - { - var d = (Instrument.DefaultData)Instruments[voice].Data; - // TODO: Better way? - return new InstrumentData - { - Type = Instruments[voice].Type, - Param = d.Param - }; - } - case InstrumentType.Drum: - { - var d = (Instrument.DrumSetData)Instruments[voice].Data; - return key < d.MinNote || key > d.MaxNote ? null : d.SubInstruments[key - d.MinNote]; - } - case InstrumentType.KeySplit: - { - var d = (Instrument.KeySplitData)Instruments[voice].Data; - for (int i = 0; i < 8; i++) - { - if (key <= d.KeyRegions[i]) - { - return d.SubInstruments[i]; - } - } - return null; - } - default: return null; - } - } - } - - public SWAR.SWAV GetSWAV(int swarIndex, int swavIndex) - { - SWAR swar = SWARs[swarIndex]; - return swar == null || swavIndex >= swar.NumWaves ? null : swar.Waves[swavIndex]; - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs b/VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs deleted file mode 100644 index 0f4c4c7f..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/SDAT.cs +++ /dev/null @@ -1,234 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class SDAT - { - public class SYMB : IBinarySerializable - { - public class Record - { - public int NumEntries; - public int[] EntryOffsets; - - public string[] Entries; - - public Record(EndianBinaryReader er, long baseOffset) - { - NumEntries = er.ReadInt32(); - EntryOffsets = er.ReadInt32s(NumEntries); - - long p = er.BaseStream.Position; - Entries = new string[NumEntries]; - for (int i = 0; i < NumEntries; i++) - { - if (EntryOffsets[i] != 0) - { - Entries[i] = er.ReadStringNullTerminated(baseOffset + EntryOffsets[i]); - } - } - er.BaseStream.Position = p; - } - } - - public string BlockType; // "SYMB" - public int BlockSize; - public int[] RecordOffsets; - public byte[] Padding; - - public Record SequenceSymbols; - //SequenceArchiveSymbols; - public Record BankSymbols; - public Record WaveArchiveSymbols; - //PlayerSymbols; - //GroupSymbols; - //StreamPlayerSymbols; - //StreamSymbols; - - public void Read(EndianBinaryReader er) - { - long baseOffset = er.BaseStream.Position; - BlockType = er.ReadString(4, false); - BlockSize = er.ReadInt32(); - RecordOffsets = er.ReadInt32s(8); - Padding = er.ReadBytes(24); - er.BaseStream.Position = baseOffset + RecordOffsets[0]; - SequenceSymbols = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + RecordOffsets[2]; - BankSymbols = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + RecordOffsets[3]; - WaveArchiveSymbols = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + BlockSize; - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public class INFO : IBinarySerializable - { - public class Record where T : new() - { - public int NumEntries; - public int[] EntryOffsets; - - public T[] Entries; - - public Record(EndianBinaryReader er, long baseOffset) - { - NumEntries = er.ReadInt32(); - EntryOffsets = er.ReadInt32s(NumEntries); - - long p = er.BaseStream.Position; - Entries = new T[NumEntries]; - for (int i = 0; i < NumEntries; i++) - { - if (EntryOffsets[i] != 0) - { - Entries[i] = er.ReadObject(baseOffset + EntryOffsets[i]); - } - } - er.BaseStream.Position = p; - } - } - - public class SequenceInfo - { - public ushort FileId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown1 { get; set; } - public ushort Bank { get; set; } - public byte Volume { get; set; } - public byte ChannelPriority { get; set; } - public byte PlayerPriority { get; set; } - public byte PlayerNum { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown2 { get; set; } - } - public class BankInfo - { - public ushort FileId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown { get; set; } - [BinaryArrayFixedLength(4)] - public ushort[] SWARs { get; set; } - } - public class WaveArchiveInfo - { - public ushort FileId { get; set; } - [BinaryArrayFixedLength(2)] - public byte[] Unknown { get; set; } - } - - public string BlockType; // "INFO" - public int BlockSize; - public int[] InfoOffsets; - - public Record SequenceInfos; - //SequenceArchiveInfos; - public Record BankInfos; - public Record WaveArchiveInfos; - //PlayerInfos; - //GroupInfos; - //StreamPlayerInfos; - //StreamInfos; - - public void Read(EndianBinaryReader er) - { - long baseOffset = er.BaseStream.Position; - BlockType = er.ReadString(4, false); - BlockSize = er.ReadInt32(); - InfoOffsets = er.ReadInt32s(8); - er.ReadBytes(24); - er.BaseStream.Position = baseOffset + InfoOffsets[0]; - SequenceInfos = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + InfoOffsets[2]; - BankInfos = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset + InfoOffsets[3]; - WaveArchiveInfos = new Record(er, baseOffset); - er.BaseStream.Position = baseOffset; - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public class FAT - { - public class FATEntry : IBinarySerializable - { - public int DataOffset; - public int DataLength; - public byte[] Padding; - - public byte[] Data; - - public void Read(EndianBinaryReader er) - { - DataOffset = er.ReadInt32(); - DataLength = er.ReadInt32(); - Padding = er.ReadBytes(8); - - long p = er.BaseStream.Position; - Data = er.ReadBytes(DataLength, DataOffset); - er.BaseStream.Position = p; - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - [BinaryStringFixedLength(4)] - public string BlockType { get; set; } // "FAT " - public int BlockSize { get; set; } - public int NumEntries { get; set; } - [BinaryArrayVariableLength(nameof(NumEntries))] - public FATEntry[] Entries { get; set; } - } - - public FileHeader FileHeader; // "SDAT" - public int SYMBOffset; - public int SYMBLength; - public int INFOOffset; - public int INFOLength; - public int FATOffset; - public int FATLength; - public int FILEOffset; - public int FILELength; - public byte[] Padding; - - public SYMB SYMBBlock; - public INFO INFOBlock; - public FAT FATBlock; - //FILEBlock - - public SDAT(byte[] bytes) - { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) - { - FileHeader = er.ReadObject(); - SYMBOffset = er.ReadInt32(); - SYMBLength = er.ReadInt32(); - INFOOffset = er.ReadInt32(); - INFOLength = er.ReadInt32(); - FATOffset = er.ReadInt32(); - FATLength = er.ReadInt32(); - FILEOffset = er.ReadInt32(); - FILELength = er.ReadInt32(); - Padding = er.ReadBytes(16); - - if (SYMBOffset != 0 && SYMBLength != 0) - { - SYMBBlock = er.ReadObject(SYMBOffset); - } - INFOBlock = er.ReadObject(INFOOffset); - FATBlock = er.ReadObject(FATOffset); - } - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs b/VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs deleted file mode 100644 index 3d97b1e1..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/SSEQ.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class SSEQ - { - public FileHeader FileHeader; // "SSEQ" - public string BlockType; // "DATA" - public int BlockSize; - public int DataOffset; - - public byte[] Data; - - public SSEQ(byte[] bytes) - { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) - { - FileHeader = er.ReadObject(); - BlockType = er.ReadString(4, false); - BlockSize = er.ReadInt32(); - DataOffset = er.ReadInt32(); - - Data = er.ReadBytes(FileHeader.FileSize - DataOffset, DataOffset); - } - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs b/VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs deleted file mode 100644 index c7a982c8..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/SWAR.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Kermalis.EndianBinaryIO; -using System; -using System.IO; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class SWAR - { - public class SWAV : IBinarySerializable - { - public SWAVFormat Format; - public bool DoesLoop; - public ushort SampleRate; - public ushort Timer; // (NDSUtils.ARM7_CLOCK / SampleRate) - public ushort LoopOffset; - public int Length; - - public byte[] Samples; - - public void Read(EndianBinaryReader er) - { - Format = er.ReadEnum(); - DoesLoop = er.ReadBoolean(); - SampleRate = er.ReadUInt16(); - Timer = er.ReadUInt16(); - LoopOffset = er.ReadUInt16(); - Length = er.ReadInt32(); - - Samples = er.ReadBytes((LoopOffset * 4) + (Length * 4)); - } - public void Write(EndianBinaryWriter ew) - { - throw new NotImplementedException(); - } - } - - public FileHeader FileHeader; // "SWAR" - public string BlockType; // "DATA" - public int BlockSize; - public byte[] Padding; - public int NumWaves; - public int[] WaveOffsets; - - public SWAV[] Waves; - - public SWAR(byte[] bytes) - { - using (var er = new EndianBinaryReader(new MemoryStream(bytes))) - { - FileHeader = er.ReadObject(); - BlockType = er.ReadString(4, false); - BlockSize = er.ReadInt32(); - Padding = er.ReadBytes(32); - NumWaves = er.ReadInt32(); - WaveOffsets = er.ReadInt32s(NumWaves); - - Waves = new SWAV[NumWaves]; - for (int i = 0; i < NumWaves; i++) - { - Waves[i] = er.ReadObject(WaveOffsets[i]); - } - } - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Track.cs b/VG Music Studio.backup/Core/NDS/SDAT/Track.cs deleted file mode 100644 index 75802b42..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/Track.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal class Track - { - public readonly byte Index; - private readonly Player _player; - - public bool Allocated; - public bool Enabled; - public bool Stopped; - public bool Tie; - public bool Mono; - public bool Portamento; - public bool WaitingForNoteToFinishBeforeContinuingXD; // Is this necessary? - public byte Voice; - public byte Priority; - public byte Volume; - public byte Expression; - public byte PitchBendRange; - public byte LFORange; - public byte LFOSpeed; - public byte LFODepth; - public ushort LFODelay; - public ushort LFOPhase; - public int LFOParam; - public ushort LFODelayCount; - public LFOType LFOType; - public sbyte PitchBend; - public sbyte Panpot; - public sbyte Transpose; - public byte Attack; - public byte Decay; - public byte Sustain; - public byte Release; - public byte PortamentoKey; - public byte PortamentoTime; - public short SweepPitch; - public int Rest; - public int[] CallStack = new int[3]; - public byte[] CallStackLoops = new byte[3]; - public byte CallStackDepth; - public int DataOffset; - public bool VariableFlag; // Set by variable commands (0xB0 - 0xBD) - public bool DoCommandWork; - public ArgType ArgOverrideType; - - public readonly List Channels = new List(0x10); - - public int GetPitch() - { - //int lfo = LFOType == LFOType.Pitch ? LFOParam : 0; - int lfo = 0; - return (PitchBend * PitchBendRange / 2) + lfo; - } - public int GetVolume() - { - //int lfo = LFOType == LFOType.Volume ? LFOParam : 0; - int lfo = 0; - return Utils.SustainTable[_player.Volume] + Utils.SustainTable[Volume] + Utils.SustainTable[Expression] + lfo; - } - public sbyte GetPan() - { - //int lfo = LFOType == LFOType.Panpot ? LFOParam : 0; - int lfo = 0; - int p = Panpot + lfo; - if (p < -0x40) - { - p = -0x40; - } - else if (p > 0x3F) - { - p = 0x3F; - } - return (sbyte)p; - } - - public Track(byte i, Player player) - { - Index = i; - _player = player; - } - public void Init() - { - Stopped = Tie = WaitingForNoteToFinishBeforeContinuingXD = Portamento = false; - Allocated = Enabled = Index == 0; - DataOffset = 0; - ArgOverrideType = ArgType.None; - Mono = VariableFlag = DoCommandWork = true; - CallStackDepth = 0; - Voice = LFODepth = 0; - PitchBend = Panpot = Transpose = 0; - LFOPhase = LFODelay = LFODelayCount = 0; - LFORange = 1; - LFOSpeed = 0x10; - Priority = (byte)(_player.Priority + 0x40); - Volume = Expression = 0x7F; - Attack = Decay = Sustain = Release = 0xFF; - PitchBendRange = 2; - PortamentoKey = 60; - PortamentoTime = 0; - SweepPitch = 0; - LFOType = LFOType.Pitch; - Rest = 0; - StopAllChannels(); - } - public void LFOTick() - { - if (Channels.Count != 0) - { - if (LFODelayCount > 0) - { - LFODelayCount--; - LFOPhase = 0; - } - else - { - int param = LFORange * Utils.Sin(LFOPhase >> 8) * LFODepth; - if (LFOType == LFOType.Volume) - { - param = (param * 60) >> 14; - } - else - { - param >>= 8; - } - LFOParam = param; - int counter = LFOPhase + (LFOSpeed << 6); // "<< 6" is "* 0x40" - while (counter >= 0x8000) - { - counter -= 0x8000; - } - LFOPhase = (ushort)counter; - } - } - else - { - LFOPhase = 0; - LFOParam = 0; - LFODelayCount = LFODelay; - } - } - public void Tick() - { - if (Rest > 0) - { - Rest--; - } - if (Channels.Count != 0) - { - // TickNotes: - for (int i = 0; i < Channels.Count; i++) - { - Channel c = Channels[i]; - if (c.NoteDuration > 0) - { - c.NoteDuration--; - } - if (!c.AutoSweep && c.SweepCounter < c.SweepLength) - { - c.SweepCounter++; - } - } - } - else - { - WaitingForNoteToFinishBeforeContinuingXD = false; - } - } - public void UpdateChannels() - { - for (int i = 0; i < Channels.Count; i++) - { - Channel c = Channels[i]; - c.LFOType = LFOType; - c.LFOSpeed = LFOSpeed; - c.LFODepth = LFODepth; - c.LFORange = LFORange; - c.LFODelay = LFODelay; - } - } - - public void StopAllChannels() - { - Channel[] chans = Channels.ToArray(); - for (int i = 0; i < chans.Length; i++) - { - chans[i].Stop(); - } - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/SDAT/Utils.cs b/VG Music Studio.backup/Core/NDS/SDAT/Utils.cs deleted file mode 100644 index 289f6012..00000000 --- a/VG Music Studio.backup/Core/NDS/SDAT/Utils.cs +++ /dev/null @@ -1,345 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS.SDAT -{ - internal static class Utils - { - public static readonly byte[] AttackTable = new byte[128] - { - 255, 254, 253, 252, 251, 250, 249, 248, - 247, 246, 245, 244, 243, 242, 241, 240, - 239, 238, 237, 236, 235, 234, 233, 232, - 231, 230, 229, 228, 227, 226, 225, 224, - 223, 222, 221, 220, 219, 218, 217, 216, - 215, 214, 213, 212, 211, 210, 209, 208, - 207, 206, 205, 204, 203, 202, 201, 200, - 199, 198, 197, 196, 195, 194, 193, 192, - 191, 190, 189, 188, 187, 186, 185, 184, - 183, 182, 181, 180, 179, 178, 177, 176, - 175, 174, 173, 172, 171, 170, 169, 168, - 167, 166, 165, 164, 163, 162, 161, 160, - 159, 158, 157, 156, 155, 154, 153, 152, - 151, 150, 149, 148, 147, 143, 137, 132, - 127, 123, 116, 109, 100, 92, 84, 73, - 63, 51, 38, 26, 14, 5, 1, 0 - }; - public static readonly ushort[] DecayTable = new ushort[128] - { - 1, 3, 5, 7, 9, 11, 13, 15, - 17, 19, 21, 23, 25, 27, 29, 31, - 33, 35, 37, 39, 41, 43, 45, 47, - 49, 51, 53, 55, 57, 59, 61, 63, - 65, 67, 69, 71, 73, 75, 77, 79, - 81, 83, 85, 87, 89, 91, 93, 95, - 97, 99, 101, 102, 104, 105, 107, 108, - 110, 111, 113, 115, 116, 118, 120, 122, - 124, 126, 128, 130, 132, 135, 137, 140, - 142, 145, 148, 151, 154, 157, 160, 163, - 167, 171, 175, 179, 183, 187, 192, 197, - 202, 208, 213, 219, 226, 233, 240, 248, - 256, 265, 274, 284, 295, 307, 320, 334, - 349, 366, 384, 404, 427, 452, 480, 512, - 549, 591, 640, 698, 768, 853, 960, 1097, - 1280, 1536, 1920, 2560, 3840, 7680, 15360, 65535 - }; - public static readonly int[] SustainTable = new int[128] - { - -92544, -92416, -92288, -83328, -76928, -71936, -67840, -64384, - -61440, -58880, -56576, -54400, -52480, -50688, -49024, -47488, - -46080, -44672, -43392, -42240, -41088, -40064, -39040, -38016, - -36992, -36096, -35328, -34432, -33664, -32896, -32128, -31360, - -30592, -29952, -29312, -28672, -28032, -27392, -26880, -26240, - -25728, -25088, -24576, -24064, -23552, -23040, -22528, -22144, - -21632, -21120, -20736, -20224, -19840, -19456, -19072, -18560, - -18176, -17792, -17408, -17024, -16640, -16256, -16000, -15616, - -15232, -14848, -14592, -14208, -13952, -13568, -13184, -12928, - -12672, -12288, -12032, -11648, -11392, -11136, -10880, -10496, - -10240, -9984, -9728, -9472, -9216, -8960, -8704, -8448, - -8192, -7936, -7680, -7424, -7168, -6912, -6656, -6400, - -6272, -6016, -5760, -5504, -5376, -5120, -4864, -4608, - -4480, -4224, -3968, -3840, -3584, -3456, -3200, -2944, - -2816, -2560, -2432, -2176, -2048, -1792, -1664, -1408, - -1280, -1024, -896, -768, -512, -384, -128, 0 - }; - - private static readonly sbyte[] _sinTable = new sbyte[33] - { - 000, 006, 012, 019, 025, 031, 037, 043, - 049, 054, 060, 065, 071, 076, 081, 085, - 090, 094, 098, 102, 106, 109, 112, 115, - 117, 120, 122, 123, 125, 126, 126, 127, - 127 - }; - public static int Sin(int index) - { - if (index < 0x20) - { - return _sinTable[index]; - } - else if (index < 0x40) - { - return _sinTable[0x20 - (index - 0x20)]; - } - else if (index < 0x60) - { - return -_sinTable[index - 0x40]; - } - else // < 0x80 - { - return -_sinTable[0x20 - (index - 0x60)]; - } - } - - private static readonly ushort[] _pitchTable = new ushort[768] - { - 0, 59, 118, 178, 237, 296, 356, 415, - 475, 535, 594, 654, 714, 773, 833, 893, - 953, 1013, 1073, 1134, 1194, 1254, 1314, 1375, - 1435, 1496, 1556, 1617, 1677, 1738, 1799, 1859, - 1920, 1981, 2042, 2103, 2164, 2225, 2287, 2348, - 2409, 2471, 2532, 2593, 2655, 2716, 2778, 2840, - 2902, 2963, 3025, 3087, 3149, 3211, 3273, 3335, - 3397, 3460, 3522, 3584, 3647, 3709, 3772, 3834, - 3897, 3960, 4022, 4085, 4148, 4211, 4274, 4337, - 4400, 4463, 4526, 4590, 4653, 4716, 4780, 4843, - 4907, 4971, 5034, 5098, 5162, 5226, 5289, 5353, - 5417, 5481, 5546, 5610, 5674, 5738, 5803, 5867, - 5932, 5996, 6061, 6125, 6190, 6255, 6320, 6384, - 6449, 6514, 6579, 6645, 6710, 6775, 6840, 6906, - 6971, 7037, 7102, 7168, 7233, 7299, 7365, 7431, - 7496, 7562, 7628, 7694, 7761, 7827, 7893, 7959, - 8026, 8092, 8159, 8225, 8292, 8358, 8425, 8492, - 8559, 8626, 8693, 8760, 8827, 8894, 8961, 9028, - 9096, 9163, 9230, 9298, 9366, 9433, 9501, 9569, - 9636, 9704, 9772, 9840, 9908, 9976, 10045, 10113, - 10181, 10250, 10318, 10386, 10455, 10524, 10592, 10661, - 10730, 10799, 10868, 10937, 11006, 11075, 11144, 11213, - 11283, 11352, 11421, 11491, 11560, 11630, 11700, 11769, - 11839, 11909, 11979, 12049, 12119, 12189, 12259, 12330, - 12400, 12470, 12541, 12611, 12682, 12752, 12823, 12894, - 12965, 13036, 13106, 13177, 13249, 13320, 13391, 13462, - 13533, 13605, 13676, 13748, 13819, 13891, 13963, 14035, - 14106, 14178, 14250, 14322, 14394, 14467, 14539, 14611, - 14684, 14756, 14829, 14901, 14974, 15046, 15119, 15192, - 15265, 15338, 15411, 15484, 15557, 15630, 15704, 15777, - 15850, 15924, 15997, 16071, 16145, 16218, 16292, 16366, - 16440, 16514, 16588, 16662, 16737, 16811, 16885, 16960, - 17034, 17109, 17183, 17258, 17333, 17408, 17483, 17557, - 17633, 17708, 17783, 17858, 17933, 18009, 18084, 18160, - 18235, 18311, 18387, 18462, 18538, 18614, 18690, 18766, - 18842, 18918, 18995, 19071, 19147, 19224, 19300, 19377, - 19454, 19530, 19607, 19684, 19761, 19838, 19915, 19992, - 20070, 20147, 20224, 20302, 20379, 20457, 20534, 20612, - 20690, 20768, 20846, 20924, 21002, 21080, 21158, 21236, - 21315, 21393, 21472, 21550, 21629, 21708, 21786, 21865, - 21944, 22023, 22102, 22181, 22260, 22340, 22419, 22498, - 22578, 22658, 22737, 22817, 22897, 22977, 23056, 23136, - 23216, 23297, 23377, 23457, 23537, 23618, 23698, 23779, - 23860, 23940, 24021, 24102, 24183, 24264, 24345, 24426, - 24507, 24589, 24670, 24752, 24833, 24915, 24996, 25078, - 25160, 25242, 25324, 25406, 25488, 25570, 25652, 25735, - 25817, 25900, 25982, 26065, 26148, 26230, 26313, 26396, - 26479, 26562, 26645, 26729, 26812, 26895, 26979, 27062, - 27146, 27230, 27313, 27397, 27481, 27565, 27649, 27733, - 27818, 27902, 27986, 28071, 28155, 28240, 28324, 28409, - 28494, 28579, 28664, 28749, 28834, 28919, 29005, 29090, - 29175, 29261, 29346, 29432, 29518, 29604, 29690, 29776, - 29862, 29948, 30034, 30120, 30207, 30293, 30380, 30466, - 30553, 30640, 30727, 30814, 30900, 30988, 31075, 31162, - 31249, 31337, 31424, 31512, 31599, 31687, 31775, 31863, - 31951, 32039, 32127, 32215, 32303, 32392, 32480, 32568, - 32657, 32746, 32834, 32923, 33012, 33101, 33190, 33279, - 33369, 33458, 33547, 33637, 33726, 33816, 33906, 33995, - 34085, 34175, 34265, 34355, 34446, 34536, 34626, 34717, - 34807, 34898, 34988, 35079, 35170, 35261, 35352, 35443, - 35534, 35626, 35717, 35808, 35900, 35991, 36083, 36175, - 36267, 36359, 36451, 36543, 36635, 36727, 36820, 36912, - 37004, 37097, 37190, 37282, 37375, 37468, 37561, 37654, - 37747, 37841, 37934, 38028, 38121, 38215, 38308, 38402, - 38496, 38590, 38684, 38778, 38872, 38966, 39061, 39155, - 39250, 39344, 39439, 39534, 39629, 39724, 39819, 39914, - 40009, 40104, 40200, 40295, 40391, 40486, 40582, 40678, - 40774, 40870, 40966, 41062, 41158, 41255, 41351, 41448, - 41544, 41641, 41738, 41835, 41932, 42029, 42126, 42223, - 42320, 42418, 42515, 42613, 42710, 42808, 42906, 43004, - 43102, 43200, 43298, 43396, 43495, 43593, 43692, 43790, - 43889, 43988, 44087, 44186, 44285, 44384, 44483, 44583, - 44682, 44781, 44881, 44981, 45081, 45180, 45280, 45381, - 45481, 45581, 45681, 45782, 45882, 45983, 46083, 46184, - 46285, 46386, 46487, 46588, 46690, 46791, 46892, 46994, - 47095, 47197, 47299, 47401, 47503, 47605, 47707, 47809, - 47912, 48014, 48117, 48219, 48322, 48425, 48528, 48631, - 48734, 48837, 48940, 49044, 49147, 49251, 49354, 49458, - 49562, 49666, 49770, 49874, 49978, 50082, 50187, 50291, - 50396, 50500, 50605, 50710, 50815, 50920, 51025, 51131, - 51236, 51341, 51447, 51552, 51658, 51764, 51870, 51976, - 52082, 52188, 52295, 52401, 52507, 52614, 52721, 52827, - 52934, 53041, 53148, 53256, 53363, 53470, 53578, 53685, - 53793, 53901, 54008, 54116, 54224, 54333, 54441, 54549, - 54658, 54766, 54875, 54983, 55092, 55201, 55310, 55419, - 55529, 55638, 55747, 55857, 55966, 56076, 56186, 56296, - 56406, 56516, 56626, 56736, 56847, 56957, 57068, 57179, - 57289, 57400, 57511, 57622, 57734, 57845, 57956, 58068, - 58179, 58291, 58403, 58515, 58627, 58739, 58851, 58964, - 59076, 59189, 59301, 59414, 59527, 59640, 59753, 59866, - 59979, 60092, 60206, 60319, 60433, 60547, 60661, 60774, - 60889, 61003, 61117, 61231, 61346, 61460, 61575, 61690, - 61805, 61920, 62035, 62150, 62265, 62381, 62496, 62612, - 62727, 62843, 62959, 63075, 63191, 63308, 63424, 63540, - 63657, 63774, 63890, 64007, 64124, 64241, 64358, 64476, - 64593, 64711, 64828, 64946, 65064, 65182, 65300, 65418 - }; - private static readonly byte[] _volumeTable = new byte[724] - { - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 4, 4, - 4, 4, 4, 4, 4, 4, 4, 4, - 4, 4, 4, 4, 4, 4, 4, 4, - 4, 5, 5, 5, 5, 5, 5, 5, - 5, 5, 5, 5, 5, 5, 5, 5, - 5, 6, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 6, 6, 6, 6, 7, - 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 10, 10, 10, - 10, 10, 10, 10, 10, 11, 11, 11, - 11, 11, 11, 11, 11, 12, 12, 12, - 12, 12, 12, 12, 13, 13, 13, 13, - 13, 13, 13, 14, 14, 14, 14, 14, - 14, 15, 15, 15, 15, 15, 16, 16, - 16, 16, 16, 16, 17, 17, 17, 17, - 17, 18, 18, 18, 18, 19, 19, 19, - 19, 19, 20, 20, 20, 20, 21, 21, - 21, 21, 22, 22, 22, 22, 23, 23, - 23, 23, 24, 24, 24, 25, 25, 25, - 25, 26, 26, 26, 27, 27, 27, 28, - 28, 28, 29, 29, 29, 30, 30, 30, - 31, 31, 31, 32, 32, 33, 33, 33, - 34, 34, 35, 35, 35, 36, 36, 37, - 37, 38, 38, 38, 39, 39, 40, 40, - 41, 41, 42, 42, 43, 43, 44, 44, - 45, 45, 46, 46, 47, 47, 48, 48, - 49, 50, 50, 51, 51, 52, 52, 53, - 54, 54, 55, 56, 56, 57, 58, 58, - 59, 60, 60, 61, 62, 62, 63, 64, - 65, 66, 66, 67, 68, 69, 70, 70, - 71, 72, 73, 74, 75, 75, 76, 77, - 78, 79, 80, 81, 82, 83, 84, 85, - 86, 87, 88, 89, 90, 91, 92, 93, - 94, 95, 96, 97, 98, 99, 101, 102, - 103, 104, 105, 106, 108, 109, 110, 111, - 113, 114, 115, 117, 118, 119, 121, 122, - 124, 125, 126, 127 - }; - - public static ushort GetChannelTimer(ushort baseTimer, int pitch) - { - int shift = 0; - pitch = -pitch; - - while (pitch < 0) - { - shift--; - pitch += 0x300; - } - - while (pitch >= 0x300) - { - shift++; - pitch -= 0x300; - } - - ulong timer = (_pitchTable[pitch] + 0x10000uL) * baseTimer; - shift -= 16; - if (shift <= 0) - { - timer >>= -shift; - } - else if (shift < 32) - { - if ((timer & (ulong.MaxValue << (32 - shift))) != 0) - { - return ushort.MaxValue; - } - timer <<= shift; - } - else - { - return ushort.MaxValue; - } - - if (timer < 0x10) - { - return 0x10; - } - if (timer > ushort.MaxValue) - { - timer = ushort.MaxValue; - } - return (ushort)timer; - } - public static byte GetChannelVolume(int vol) - { - int a = vol / 0x80; - if (a < -723) - { - a = -723; - } - else if (a > 0) - { - a = 0; - } - return _volumeTable[a + 723]; - } - } -} diff --git a/VG Music Studio.backup/Core/NDS/Utils.cs b/VG Music Studio.backup/Core/NDS/Utils.cs deleted file mode 100644 index ea2388b6..00000000 --- a/VG Music Studio.backup/Core/NDS/Utils.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Kermalis.VGMusicStudio.Core.NDS -{ - internal static class Utils - { - public const int ARM7_CLOCK = 16756991; // (33.513982 MHz / 2) == 16.756991 MHz == 16,756,991 Hz - } -} diff --git a/VG Music Studio.backup/Core/Player.cs b/VG Music Studio.backup/Core/Player.cs deleted file mode 100644 index 7a6e26f2..00000000 --- a/VG Music Studio.backup/Core/Player.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Core -{ - internal enum PlayerState : byte - { - Stopped = 0, - Playing, - Paused, - Recording, - ShutDown - } - - internal delegate void SongEndedEvent(); - - internal interface IPlayer : IDisposable - { - List[] Events { get; } - long MaxTicks { get; } - long ElapsedTicks { get; } - bool ShouldFadeOut { get; set; } - long NumLoops { get; set; } - - PlayerState State { get; } - event SongEndedEvent SongEnded; - - void LoadSong(long index); - void SetCurrentPosition(long ticks); - void Play(); - void Pause(); - void Stop(); - void Record(string fileName); - void GetSongState(UI.SongInfoControl.SongInfo info); - } -} diff --git a/VG Music Studio.backup/Core/SongEvent.cs b/VG Music Studio.backup/Core/SongEvent.cs deleted file mode 100644 index 7e518a3b..00000000 --- a/VG Music Studio.backup/Core/SongEvent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Core -{ - internal interface ICommand - { - Color Color { get; } - string Label { get; } - string Arguments { get; } - } - internal class SongEvent - { - public long Offset { get; } - public List Ticks { get; } = new List(); - public ICommand Command { get; } - - public SongEvent(long offset, ICommand command) - { - Offset = offset; - Command = command; - } - } -} diff --git a/VG Music Studio.backup/Core/VGMSDebug.cs b/VG Music Studio.backup/Core/VGMSDebug.cs deleted file mode 100644 index 91bc0919..00000000 --- a/VG Music Studio.backup/Core/VGMSDebug.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Kermalis.EndianBinaryIO; -using Sanford.Multimedia.Midi; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Kermalis.VGMusicStudio.Core -{ -#if DEBUG - internal static class VGMSDebug - { - public static void MIDIVolumeMerger(string f1, string f2) - { - var midi1 = new Sequence(f1); - var midi2 = new Sequence(f2); - var baby = new Sequence(midi1.Division); - - for (int i = 0; i < midi1.Count; i++) - { - Track midi1Track = midi1[i]; - Track midi2Track = midi2[i]; - var babyTrack = new Track(); - baby.Add(babyTrack); - - for (int j = 0; j < midi1Track.Count; j++) - { - MidiEvent e1 = midi1Track.GetMidiEvent(j); - if (e1.MidiMessage is ChannelMessage cm1 && cm1.Command == ChannelCommand.Controller && cm1.Data1 == (int)ControllerType.Volume) - { - MidiEvent e2 = midi2Track.GetMidiEvent(j); - var cm2 = (ChannelMessage)e2.MidiMessage; - babyTrack.Insert(e1.AbsoluteTicks, new ChannelMessage(ChannelCommand.Controller, cm1.MidiChannel, (int)ControllerType.Volume, Math.Max(cm1.Data2, cm2.Data2))); - } - else - { - babyTrack.Insert(e1.AbsoluteTicks, e1.MidiMessage); - } - } - } - - baby.Save(f1); - baby.Save(f2); - } - - public static void EventScan(List songs, bool showIndexes) - { - Console.WriteLine($"{nameof(EventScan)} started."); - var scans = new Dictionary>(); - foreach (Config.Song song in songs) - { - try - { - Engine.Instance.Player.LoadSong(song.Index); - } - catch (Exception ex) - { - Console.WriteLine("Exception loading {0} - {1}", showIndexes ? $"song {song.Index}" : $"\"{song.Name}\"", ex.Message); - continue; - } - if (Engine.Instance.Player.Events != null) - { - foreach (string cmd in Engine.Instance.Player.Events.Where(ev => ev != null).SelectMany(ev => ev).Select(ev => ev.Command.Label).Distinct()) - { - if (scans.ContainsKey(cmd)) - { - scans[cmd].Add(song); - } - else - { - scans.Add(cmd, new List() { song }); - } - } - } - } - foreach (KeyValuePair> kvp in scans.OrderBy(k => k.Key)) - { - Console.WriteLine("{0} ({1})", kvp.Key, showIndexes ? string.Join(", ", kvp.Value.Select(s => s.Index)) : string.Join(", ", kvp.Value.Select(s => s.Name))); - } - Console.WriteLine($"{nameof(EventScan)} ended."); - } - - public static void GBAGameCodeScan(string path) - { - Console.WriteLine($"{nameof(GBAGameCodeScan)} started."); - var scans = new List(); - foreach (string file in Directory.GetFiles(path, "*.gba", SearchOption.AllDirectories)) - { - try - { - using (var reader = new EndianBinaryReader(File.OpenRead(file))) - { - string gameCode = reader.ReadString(3, false, 0xAC); - char regionCode = reader.ReadChar(0xAF); - byte version = reader.ReadByte(0xBC); - scans.Add(string.Format("Code: {0}\tRegion: {1}\tVersion: {2}\tFile: {3}", gameCode, regionCode, version, file)); - } - } - catch (Exception ex) - { - Console.WriteLine("Exception loading \"{0}\" - {1}", file, ex.Message); - } - } - foreach (string s in scans.OrderBy(s => s)) - { - Console.WriteLine(s); - } - Console.WriteLine($"{nameof(GBAGameCodeScan)} ended."); - } - } -#endif -} diff --git a/VG Music Studio.backup/Dependencies/DLS2.dll b/VG Music Studio.backup/Dependencies/DLS2.dll deleted file mode 100644 index 1d3bc0c868d173339396a66bf1f3204055a04c67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36352 zcmeHwcYIW3_V>AWW^N{vLK^9iK?TMHVk#sCF&&~b2_RN7Bm+c3Cd^ELh-k2|sHnJh z#T5kGieg(UtFYF!yY{tVgWY9a%NBKY;r*WTJX1pAzJA`%`^WqFyuX3@-t#@rdD?mU zoja51CvPMh5!vxRc#!B}T=`Wi_}`%)!N~=WCes7)FO7QGnD)}BIZHZ&?w){ualpIG z-R|w~_J`aHeeOVSx4X03-8i$=z0BX?D^5ys9ifw+)kHMSu+hA-Q*I0AwucULCm1C} z>!7iecJ?-0-FWZ9nU3h@j_}joL0?Q~n>lYRW2pA>EmL-Gw(h;?UO~ zqD1IE+e(ywXxM+_ZX#z)xD$M83@;A(R)xS88H6k2sdWI3XKqdT)0o1hn(MB#0DrptYw)s;*{Aq0NL1e3Ye&M

D}<*8v!HSnC0eE@ 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 AZU6uP diff --git a/VG Music Studio.backup/Dependencies/Sanford.Multimedia.Midi.dll b/VG Music Studio.backup/Dependencies/Sanford.Multimedia.Midi.dll deleted file mode 100644 index 4e31c6b0b097ebdb81eaa3532dccbaa0a6a92cdf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180224 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 diff --git a/VG Music Studio.backup/MP2K.yaml b/VG Music Studio.backup/MP2K.yaml deleted file mode 100644 index 15660cec..00000000 --- a/VG Music Studio.backup/MP2K.yaml +++ /dev/null @@ -1,1443 +0,0 @@ -A2UJ_00: - Name: "Mother 1 + 2 (Japan)" - SongTableOffsets: 0x10B530 - SongTableSizes: 451 - SampleRate: 2 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -A3UJ_00: - Name: "Mother 3 (Japan)" - SongTableOffsets: 0x120E94 - SongTableSizes: 1968 - SampleRate: 4 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -A4AE_00: - Name: "The Simpson's Road Rage (USA)" - SongTableOffsets: 0x125FD8 - SongTableSizes: 112 - SampleRate: 2 - ReverbType: "Normal" - Reverb: 0 - Volume: 11 - HasGoldenSunSynths: False - HasPokemonCompression: False -A4AX_00: - Name: "The Simpson's Road Rage (Europe)" - SongTableOffsets: 0x1281D0 - SongTableSizes: 113 - Copy: "A4AE_00" -A7KE_00: - Name: "Kirby: Nightmare in Dream Land (USA)" - SongTableOffsets: 0x60B460 - SongTableSizes: 579 - SampleRate: 4 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False - Playlists: - Sound Test (Music): - 26: "0 - Title Screen" - 40: "1 - File Select" - 0: "2 - Vegetable Valley" - 21: "3 - Ice Cream Island" - 11: "4 - Butter Building" - 17: "5 - Grape Garden" - 22: "6 - Yogurt Yard" - 12: "7 - Orange Ocean" - 18: "8 - Rainbow Resort" - 15: "9 - Fortified Fronts" - 2: "10 - Sky Stages" - 37: "11 - Mini-Boss Fight" - 30: "12 - Boss Fight" - 1: "13 - Boss Defeated!" - 14: "14 - Victory Dance 1" - 13: "15 - Victory Dance 2" - 39: "16 - Arena" - 25: "17 - Museum" - 38: "18 - King Dedede's Theme" - 20: "19 - Green Greens Theme" - 27: "20 - Fountain of Dreams" - 31: "21 - Returning the Star Rod!" - 32: "22 - Nightmare's Power Orb" - 33: "23 - Nightmare's Pursuit" - 34: "24 - Fighting the Nightmare!" - 24: "25 - The End!" - 3: "26 - Down!" - 4: "27 - Level 1: Vegetable Valley" - 5: "28 - Level 2: Ice Cream Island" - 6: "29 - Level 3: Butter Building" - 7: "30 - Level 4: Grape Garden" - 8: "31 - Level 5: Yogurt Yard" - 9: "32 - Level 6: Orange Ocean" - 10: "33 - Level 7: Rainbow Resort" - 35: "34 - Minigame: Quick Draw" - 46: "35 - Minigame: Bomb Rally" - 42: "36 - Minigame: Kirby's Air Grind" - 43: "37 - Minigame: Kirby's Air Grind (Goal!)" - 44: "38 - Minigame: Kirby's Air Grind (Results)" - 23: "39 - Minigame Result: Bad!" - 28: "40 - Minigame Result: Good!" - 29: "41 - Minigame Result: Perfect!" - 19: "42 - Invincible Candy" - 16: "43 - Game Over" - Other Music: - 47: "Vegetable Valley (No Intro)" - 53: "Ice Cream Island (No Intro)" - 48: "Butter Building (No Intro)" - 50: "Grape Garden (No Intro)" - 54: "Yogurt Yard (No Intro)" - 49: "Orange Ocean (No Intro)" - 51: "Rainbow Resort (No Intro)" - 55: "Fortified Fronts (No Intro)" - 52: "Green Greens (No Intro)" - 41: "Building of Battles (Outside)" - 56: "Nightmare Appears!" -A7KJ_00: - Name: "Kirby: Nightmare in Dream Land (Japan)" - SongTableOffsets: 0x60F36C - Copy: "A7KE_00" -A7KP_00: - Name: "Kirby: Nightmare in Dream Land (Europe)" - SongTableOffsets: 0x843B50 - Copy: "A7KE_00" -A88E_00: - Name: "Mario & Luigi - Superstar Saga (USA)" - SongTableOffsets: 0xFB5DA4 - SongTableSizes: 323 - SampleRate: 2 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -A88J_00: - Name: "Mario & Luigi - Superstar Saga (Japan)" - SongTableOffsets: 0xFB5E00 - Copy: "A88E_00" -A88P_00: - Name: "Mario & Luigi - Superstar Saga (Europe)" - Copy: "A88E_00" -AE7E_00: - Name: "Fire Emblem: The Blazing Blade (USA)" - SongTableOffsets: 0x69D6D8 - SongTableSizes: 1001 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 13 - HasGoldenSunSynths: False - HasPokemonCompression: False -AE7J_00: - Name: "Fire Emblem: The Blazing Blade (Japan)" - SongTableOffsets: 0x6EA8C8 - Copy: "AE7E_00" -AE7X_00: - Name: "Fire Emblem: The Blazing Blade (Europe)" - SongTableOffsets: 0x6805F4 - Copy: "AE7E_00" -AE7Y_00: - SongTableOffsets: 0x67F1F4 - Copy: "AE7X_00" -AEFJ_00: - Name: "Fire Emblem: The Binding Blade (Japan)" - SongTableOffsets: 0x3994D8 - SongTableSizes: 621 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 13 - HasGoldenSunSynths: False - HasPokemonCompression: False -AFXE_00: - Name: "Final Fantasy Tactics Advance (USA)" - SongTableOffsets: 0x14BB48 - SongTableSizes: 640 - SampleRate: 4 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False - Playlists: - Music (OST Order): - 42: "Main Theme" - 24: "Snow Dancing in the Schoolyard" - 33: "Companions That Surpassed Their Tribe" - 20: "Magic Beast Farm" - 17: "Crystal" - 25: "Undeniable Anxiety" - 36: "Amber Valley" - 6: "Bell of Victory (Fanfare)" - 15: "At the Bar" - 12: "Different World Ivalice" - 10: "Engange (Fanfare)" - 0: "Gathering Allies" - 11: "Walking in Ivalice" - 4: "Wind of Hope (Fanfare)" - 14: "Teach Me, Mont Blanc" - 9: "Wounded Comrades (Fanfare)" - 39: "Undefeated Heart" - 2: "Gained Fruit (Fanfare)" - 28: "Marche" - 37: "Painful Battle" - 5: "Notice of Retreat (Fanfare)" - 7: "Sleep of Defeat (Fanfare)" - 21: "Prison" - 40: "Surpassing The Wall" - 3: "Exhausted Ones (Fanfare)" - 29: "Mewt" - 35: "Battle Of Hope" - 8: "Level Up (Fanfare)" - 22: "Law Card" - 30: "Ritz" - 19: "Mysterious Shop" - 34: "The Road We Both Aim For" - 16: "Wind of Liberation (Fanfare)" - 27: "Confusion" - 23: "Judge (Fanfare)" - 38: "Beyond The Wasteland" - 26: "The World Starting To Move" - 41: "Unavoidable Destiny" - 45: "Incarnation" - 31: "Vanishing World" - 32: "A Place We Should Return To" - 47: "Fulfilled Dream Segment" - Music (Bonus): - 13: "Main Theme (Beta Version)" - 48: "St Ivalice Turns Into Ivalice" - 49: "Save Menu Theme" -AFXJ_00: - Name: "Final Fantasy Tactics Advance (Japan)" - SongTableOffsets: 0x1401A8 - Copy: "AFXE_00" -AFXP_00: - Name: "Final Fantasy Tactics Advance (Europe)" - SongTableOffsets: 0x14F540 - Copy: "AFXE_00" -AGFD_00: - Name: "Golden Sun: The Lost Age (Germany)" - Copy: "AGFE_00" -AGFE_00: - Name: "Golden Sun: The Lost Age (USA)" - SongTableOffsets: 0x1C4530 - SongTableSizes: 800 - SampleRate: 8 - ReverbType: "Camelot2" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: True - HasPokemonCompression: False -AGFF_00: - Name: "Golden Sun: The Lost Age (France)" - Copy: "AGFE_00" -AGFI_00: - Name: "Golden Sun: The Lost Age (Italy)" - Copy: "AGFE_00" -AGFJ_00: - Name: "Golden Sun: The Lost Age (Japan)" - Copy: "AGFE_00" -AGFS_00: - Name: "Golden Sun: The Lost Age (Spain)" - Copy: "AGFE_00" -AGLJ_00: - Name: "Tomato Adventure (Japan)" - SongTableOffsets: 0x4B11A4 - SongTableSizes: 244 - SampleRate: 4 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -AGSD_00: - Name: "Golden Sun (Germany)" - SongTableOffsets: 0xFD484 - Copy: "AGSE_00" -AGSE_00: - Name: "Golden Sun (USA)" - SongTableOffsets: 0xFC684 - SongTableSizes: 312 - SampleRate: 6 - ReverbType: "Camelot1" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: True - HasPokemonCompression: False -AGSF_00: - Name: "Golden Sun (France)" - SongTableOffsets: 0xFEE84 - Copy: "AGSE_00" -AGSI_00: - Name: "Golden Sun (Italy)" - Copy: "AGSE_00" -AGSJ_00: - Name: "Golden Sun (Japan)" - SongTableOffsets: 0xF3684 - Copy: "AGSE_00" -AGSS_00: - Name: "Golden Sun (Spain)" - SongTableOffsets: 0xFEE84 - Copy: "AGSE_00" -AINJ_00: - Name: "Initial D: Another Stage (Japan)" - SongTableOffsets: 0x648B38 - SongTableSizes: 88 - SampleRate: 2 - ReverbType: "Normal" - Reverb: 0 - Volume: 13 - HasGoldenSunSynths: False - HasPokemonCompression: False -AMAC_00: - Name: "Super Mario Advance (China)" - SongTableOffsets: 0xF64C4 - Copy: "AMZE_00" -AMAE_00: - Name: "Super Mario Advance (Europe)" - SongTableOffsets: 0xF4368 - Copy: "AMZE_00" -AMAJ_00: - Name: "Super Mario Advance (Japan)" - SongTableOffsets: 0xF3F18 - Copy: "AMZE_00" -AMKE_00: - Name: "Mario Kart: Super Circuit (USA)" - SongTableOffsets: 0x102498 - SongTableSizes: 403 - SampleRate: 2 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False - Playlists: - Music: - 2: "Main Menu" - 3: "Race Intro (Grand Prix)" - 4: "Race Intro (Time Trial / Quick Race)" - 5: "1st Place" - 6: "2nd-4th Place" - 7: "5th Place and Under" - 10: "Starman" - 11: "Lightning" - 12: "Ghost" - 13: "Double Ghost" - 14: "1st Place Results" - 15: "2nd-4th Place Results" - 16: "5th Place and Under Results" - 17: "Quick Race Results" - 18: "Battle Mode" - 19: "Final Lap" - 20: "20" - 21: "Shy Guy Beach / Cheep-Cheep Island" - 22: "Bowser's Castles" - 23: "Cheese Land" - 24: "Riverside Park" - 25: "Mario/Peach/Luigi Circuit" - 26: "Sunset Wilds" - 27: "Boo Lake / Broken Pier" - 28: "Rainbow Road" - 29: "Yoshi Desert" - 30: "Ribbon Road" - 31: "Snow Land" - 32: "Lakeside Park" - 33: "Battle Mode Win" - 34: "Battle Mode Lose" - 35: "Sky Garden" - 36: "Rainbow Road (Intro)" - 37: "Ghost Data Trading" - 40: "Connection Screen" - 41: "SNES Mario Circuit" - 42: "SNES Donut Plains" - 43: "SNES Choco Island" - 44: "SNES Vanilla Lake" - 45: "SNES Ghost Valley" - 46: "SNES Rainbow Road" - 47: "SNES Bowser's Castle" - 48: "SNES Koopa Beach" - 51: "Night Opening" - 52: "Title Screen" - 53: "Opening" - 54: "Credits" - 55: "Award Ceremony (Intro)" - 56: "Award Ceremony" - 57: "Award Ceremony Lose" - 59: "59" - 60: "60" -AMKJ_00: - Name: "Mario Kart: Super Circuit (Japan)" - SongTableOffsets: 0x1245C8 - Copy: "AMKE_00" -AMKP_00: - Name: "Mario Kart: Super Circuit (Europe)" - Copy: "AMKE_00" -AMTC_00: - Name: "Metroid Fusion (China)" - SongTableOffsets: 0xAB0E4 - Copy: "AMTE_00" -AMTE_00: - Name: "Metroid Fusion (USA)" - SongTableOffsets: 0xA8D3C - SongTableSizes: 745 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -AMTJ_00: - Name: "Metroid Fusion (Japan)" - SongTableOffsets: 0xAB0A0 - Copy: "AMTE_00" -AMTP_00: - Name: "Metroid Fusion (Europe)" - SongTableOffsets: 0xA9398 - Copy: "AMTE_00" -AMZE_00: - Name: "Super Mario Advance (USA)" - SongTableOffsets: 0xF40D4 - SongTableSizes: 447 - SampleRate: 2 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -AWAC_00: - Name: "Wario Land 4 (China)" - SongTableOffsets: 0x98CB8 - Copy: "AWAE_00" -AWAE_00: - Name: "Wario Land 4 (USA)" - SongTableOffsets: 0x98028 - SongTableSizes: 819 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False - Playlists: - Music: - 635: "Intro" - 636: "Work It!" - 638: "Enter the Pyramid" - 639: "Map" - 672: "Hall of Hieroglyphs" - 651: "Palm Tree Paradise" - 652: "Palm Tree Paradise (Caves)" - 653: "Palm Tree Paradise (Back From Caves)" - 654: "Wildflower Fields" - 655: "Mystic Lake (Lakeside)" - 656: "Mystic Lake (Inside Jungle)" - 657: "Mystic Lake (Back to Lakeside)" - 658: "Monsoon Jungle" - 659: "The Curious Factory" - 660: "The Toxic Landfill" - 662: "40 Below Fridge" - 661: "Pinball Zone" - 663: "Toy Block Tower" - 664: "The Big Board" - 665: "Doodle Woods" - 666: "Domino Row" - 667: "Crescent Moon Village" - 668: "Arabian Night" - 670: "Fiery Cavern" - 669: "Hotel Horror" - 671: "Golden Passage" - 617: "Wario's Workout" - 673: "Puzzle Room" - 674: "Puzzle Room (2)" - 217: "Hurry Up Time Limit" - 681: "Hurry Up!!!" - 450: "Only a Few Seconds Left!!!" - 452: "Coin Loss..." - 642: "Inside the Void" - 342: "Four Pieces" - 594: "High Score" - 448: "Door to Next Stage Opens" - 457: "Door to Next Stage Opens (2)" - 421: "Stage Already Clear" - 447: "Stage Already Clear (2)" - 475: "Can't Enter Boss Room" - 425: "Boss Room Opens" - 473: "Enter Boss Room" - 474: "Track 37" - 640: "Boss Room" - 682: "Item Shop" - 493: "Boss's Presence" - 683: "Here Comes the Mystery Man" - 684: "Here Comes the Mystery Man (Shorter Version)" - 508: "Bugle" - 511: "Large Lips" - 512: "Large Lips (Longer Version)" - 513: "Random Damage Sound" - 124: "Boss is Ready" - 207: "Boss is Ready (Shorter Version)" - 687: "Boss" - 429: "Treasures (Part 1)" - 430: "Treasures (Part 2)" - 431: "Treasures (Part 3)" - 422: "Victory" - 432: "Victory (2)" - 701: "Mini Game Shop" - 709: "Wario's Homerun Derby" - 710: "Homerun!" - 715: "Cheerleaders (Part 1)" - 716: "Cheerleaders (Part 2)" - 702: "The Wario Hop Begins" - 707: "The Wario Hop Begins (No Loop)" - 703: "The Wario Hop 1" - 704: "The Wario Hop 2" - 705: "The Wario Hop 3" - 706: "The Wario Hop 4" - 708: "Wario's Roulette" - 711: "Failure 1" - 712: "Failure 2" - 713: "Failure 3" - 714: "Failure 4" - 593: "Mini Game High Score" - 191: "Golden Diva Appears" - 183: "Golden Diva is Ready" - 688: "Golden Diva" - 689: "Golden Diva's Lip is Left" - 489: "All of my Treasures!" - 800: "Escape from the Pyramid" - 801: "Track 68" - 802: "Thank You Wario" - 803: "Japanese Ending Song" - 806: "Japanese Ending Song (2)" - 809: "Japanese Ending Song (3)" - 812: "Japanese Ending Song (4)" - 815: "English Ending Song" - 816: "English Ending Song (2)" - 817: "English Ending Song (3)" - 818: "English Engind Song (4)" - 814: "The Big Board (Ending Mix)" - 813: "Mystic Lake (Ending Mix)" - 811: "Wildflower Fields (Ending Mix)" - 810: "Hall of Hieroglyphs (Ending Mix)" - 808: "Doodle Woods (Ending Mix)" - 807: "Crescent Moon Village (Ending Mix)" - 805: "Monsoon Jungle (Ending Mix)" - 804: "Toy Blick Tower (Ending Mix)" - 618: "Sound Room" - 619: "About That Shepherd" - 620: "Things That Never Change" - 621: "Tomorrow's Blood Pressure" - 622: "Beyond the Headrush" - 623: "Driftwood & The Island Dog" - 624: "The Judge's Feet" - 625: "The Moon's Lamppost" - 626: "Soft Shell" - 627: "So Sleepy" - 628: "The Short Futon" - 629: "Avocado Song" - 630: "Mr. Fly" - 631: "Yesterday's Words" - 632: "The Errand" - 633: "You and Your Shoes" - 634: "Mr. Ether and Planaria" - 727: "Medamayaki (Karaoke)" - 728: "Medamayaki (Karaoke) (2)" - 729: "Medamayaki (Voice)" -AWAJ_00: - Name: "Wario Land 4 (Japan)" - Copy: "AWAE_00" -AXPD_00: - Name: "Pokémon Sapphire Version (Germany)" - SongTableOffsets: 0x463394 - Copy: "AXVD_00" -AXPD_01: - Copy: "AXPD_00" -AXPE_00: - Name: "Pokémon Sapphire Version (USA)" - SongTableOffsets: 0x4554E8 - Copy: "AXVE_00" -AXPE_01: - SongTableOffsets: 0x455500 - Copy: "AXPE_00" -AXPE_02: - Copy: "AXPE_01" -AXPF_00: - Name: "Pokémon Sapphire Version (France)" - SongTableOffsets: 0x45E094 - Copy: "AXVF_00" -AXPF_01: - Copy: "AXPF_00" -AXPI_00: - Name: "Pokémon Sapphire Version (Italy)" - SongTableOffsets: 0x4574B4 - Copy: "AXVI_00" -AXPI_01: - Copy: "AXPI_00" -AXPJ_00: - Name: "Pokémon Sapphire Version (Japan)" - SongTableOffsets: 0x416ECC - Copy: "AXVJ_00" -AXPS_00: - Name: "Pokémon Sapphire Version (Spain)" - SongTableOffsets: 0x45A660 - Copy: "AXVS_00" -AXPS_01: - Copy: "AXPS_00" -AXVD_00: - Name: "Pokémon Ruby Version (Germany)" - SongTableOffsets: 0x463428 - Copy: "AXVE_00" -AXVD_01: - Copy: "AXVD_00" -AXVE_00: - Name: "Pokémon Ruby Version (USA)" - SongTableOffsets: 0x45548C - SongTableSizes: 468 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 12 - HasGoldenSunSynths: False - HasPokemonCompression: True - Playlists: - Disc 1: - 414: "Opening Movie: Setting out on a Journey in the Hoenn Region" - 442: "Opening Movie: Double Battles" - 413: "Title Screen" - 374: "Introductions" - 405: "Littleroot Town" - 383: "Birch Pokémon Lab" - 415: "May" - 410: "H-Help Me!" - 457: "Battle! (Wild Pokémon)" - 353: "Victory! (Wild Pokémon)" - 359: "Route 101" - 363: "Oldale Town" - 400: "Pokémon Center" - 368: "Pokémon Healed" - 380: "Trainers' Eyes Meet (Youngster)" - 407: "Trainers' Eyes Meet (Lass)" - 459: "Battle! (Trainer Battle)" - 412: "Victory! (Trainer Battle)" - 367: "Level Up!" - 362: "Petalburg City" - 420: "Hurry Along" - 401: "Route 104" - 366: "Petalburg Woods" - 441: "Team Magma Appears!" - 458: "Battle! (Team Aqua / Team Magma)" - 424: "Victory! (Team Aqua / Team Magma)" - 399: "Rustboro City" - 435: "Trainers' School" - 431: "Crossing the Sea" - 427: "Dewford Town" - 379: "Trainers' Eyes Meet (Tuber♀)" - 433: "Slateport City" - 375: "Oceanic Museum" - 360: "Route 110" - 403: "Cycling" - 426: "Game Corner" - 390: "Win" - 391: "Lose" - 392: "Reel Time" - 389: "Jackpot" - 398: "Verdanturf Town" - 418: "Route 113" - 449: "Twins" - 437: "Fallarbor Town" - 425: "Cable Car" - 406: "Mt. Chimney" - 451: "Trainers' Eyes Meet (Hiker)" - 409: "Route 111" - 364: "Pokémon Gym" - 460: "Battle! (Gym Leader)" - 354: "Victory! (Gym Leader)" - 369: "Obtained a Badge!" - 372: "Obtained a TM!" - 365: "Surf" - Disc 2: - 402: "Route 119" - 382: "Fortree City" - 361: "Route 120" - 453: "Interviewers" - 428: "Safari Zone" - 397: "Trainers' Eyes Meet (Gentleman)" - 408: "Lilycove City" - 373: "Museum" - 378: "Move Deleted" - 421: "Brendan" - 464: "Battle! (Brendan/May)" - 376: "Evolution (Intro)" - 377: "Evolution" - 371: "Congratulations! Your Pokémon Evolved!" - 404: "Poké Mart" - 432: "Mt. Pyre" - 416: "Trainers' Eyes Meet (Psychic)" - 423: "Trainers' Eyes Meet (Hex Maniac)" - 434: "Mt. Pyre Exterior" - 430: "Hideout" - 370: "Obtained an Item!" - 419: "Team Aqua Appears!" - 466: "Battle! (Team Aqua/Team Magma Leaders)" - 388: "The Super-Ancient Pokémon Awaken!" - 444: "Drought" - 443: "Heavy Rain" - 411: "Dive" - 445: "Sootopolis City" - 386: "Cave of Origin" - 463: "Battle! (Super-Ancient Pokémon)" - 385: "Trainers' Eyes Meet (Swimmer♀)" - 422: "Evergrande City" - 387: "Obtained a Berry!" - 452: "Contest Lobby" - 440: "Pokémon Contest!" - 446: "Results Announcement" - 439: "Contest Winner" - 438: "Sealed Chamber" - 462: "Battle! (Regirock/Regice/Registeel)" - 448: "The Trick House" - 381: "Abandoned Ship" - 384: "Battle Tower" - 429: "Victory Road" - 417: "Trainers' Eyes Meet (Cooltrainer)" - 450: "The Elite Four Appear!" - 465: "Battle! (Elite Four)" - 454: "Champion Steven" - 461: "Battle! (Steven)" - 355: "Victory! (Steven)" - 447: "Room of Glory" - 436: "The Hall of Fame" - 455: "Ending Theme" - 456: "The End" - Other Music: - 350: "Unused - TETSUJI" - 351: "Unused - Route 38" - 352: "Victory! (Wild Pokémon) (No Intro)" - 356: "Unused - Pokémon Center (2)" - 357: "Unused - Viridian City" - 358: "Unused - Battle! (Entei/Raikou/Suicune)" - 393: "Pokémon Contest! (Multiplayer - Player 1)" - 394: "Pokémon Contest! (Multiplayer - Player 2)" - 395: "Pokémon Contest! (Multiplayer - Player 3)" - 396: "Pokémon Contest! (Multiplayer - Player 4)" - 467: "Unused - Team Rocket Appears!" -AXVE_01: - SongTableOffsets: 0x4554A0 - Copy: "AXVE_00" -AXVE_02: - Copy: "AXVE_01" -AXVF_00: - Name: "Pokémon Ruby Version (France)" - SongTableOffsets: 0x45E564 - Copy: "AXVE_00" -AXVF_01: - Copy: "AXVF_00" -AXVI_00: - Name: "Pokémon Ruby Version (Italy)" - SongTableOffsets: 0x457810 - Copy: "AXVE_00" -AXVI_01: - Copy: "AXVI_00" -AXVJ_00: - Name: "Pokémon Ruby Version (Japan)" - SongTableOffsets: 0x416EE4 - Copy: "AXVE_00" -AXVS_00: - Name: "Pokémon Ruby Version (Spain)" - SongTableOffsets: 0x45A924 - Copy: "AXVE_00" -AXVS_01: - Copy: "AXVS_00" -AZLE_00: - Name: "The Legend of Zelda: A Link to the Past and Four Swords (USA)" - SongTableOffsets: 0x3C3BBC - SongTableSizes: 545 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -AZLJ_00: - Name: "The Legend of Zelda: A Link to the Past and Four Swords (Japan)" - SongTableOffsets: 0x3E7A54 - Copy: "AZLE_00" -AZLP_00: - Name: "The Legend of Zelda: A Link to the Past and Four Swords (Europe)" - SongTableOffsets: 0x43F8AC - Copy: "AZLE_00" -B24E_00: - Name: "Pokémon Mystery Dungeon: Red Rescue Team (USA)" - SongTableOffsets: 0x1E866BC - SongTableSizes: 940 - SampleRate: 5 - ReverbType: "Normal" - Reverb: 0 - Volume: 14 - HasGoldenSunSynths: False - HasPokemonCompression: False - Playlists: - Disc 1: - 40: "Intro" - 43: "Title" - 8: "File Select" - 4: "Personality Test" - 101: "Awakening" - 10: "There's Trouble!" - 125: "Tiny Woods" - 114: "At the End of the Road" - 103: "Happiness" - 1: "Rescue Team Base" - 46: "Insert Title" - 14: "Thunderwave Cave" - 12: "Dream" - 7: "Pokémon Plaza" - 107: "Friend Area ~ Field" - 105: "Friend Area ~ Steppe" - 121: "Friend Area ~ Forest" - 116: "Friend Area ~ Wilds" - 2: "Friend Area ~ Swamp" - 16: "Friend Area ~ Pond" - 120: "Mt. Steel" - 11: "Versus Boss" - 112: "Friend Area ~ Lab" - 110: "Makuhita Dojo" - 15: "Sinister Woods" - 9: "Danger" - 113: "Silent Chasm" - 111: "Mt. Thunder" - 126: "Mt. Thunder Peak" - 20: "Great Canyon" - 6: "The Legend of Ninetales" - 24: "Run Away" - Disc 2: - 102: "Lapis Cave" - 38: "The Mountain of Fire" - 25: "Mt. Blaze" - 17: "Kecleon Shop" - 18: "Stop! Thief!" - 33: "Mt. Blaze Peak" - 36: "Escape Through the Snow" - 104: "Frosty Forest" - 39: "Frosty Grotto" - 5: "Benevolent Spirit" - 115: "Mt. Freeze" - 123: "Mt. Freeze Peak" - 108: "Magma Cavern" - 128: "Monster House!" - 124: "Magma Cavern Pit" - 19: "World Calamity" - 29: "Dream Eater" - 22: "Sky Tower" - 23: "Sky Tower Summit" - 26: "Rayquaza's Domain" - 32: "Versus Legendary" - 37: "The Other Side" - 42: "Farewell" - 44: "Staff Roll" - 45: "Time of Reunion" - 106: "Friend Area ~ Oceanic" - 28: "Friend Area ~ Rainbow Peak" - 3: "Friend Area ~ Caves" - 35: "Friend Area ~ Cryptic Cave" - 118: "Friend Area ~ Southern Island" - 122: "Friend Area ~ Final Island" - 21: "Stormy Sea" - 13: "Buried Relic" - 117: "Friend Area ~ Legendary Island" - 30: "Friend Area ~ Deep-Sea Current" - 127: "Friend Area ~ Healing Forest" - 31: "Friend Area ~ Seafloor Cave" - 34: "Friend Area ~ Volcanic Pit" - 27: "Friend Area ~ Stratos Lookout" - 119: "Friend Area ~ Enclosed Island" - 100: "Unused 1" - 109: "Unused 2" - Other Music: - 41: "41" - 51: "Exploration Failed" - 52: "Exploration Complete!" - Dungeon Music: - 125: "Tiny Woods" - 14: "Thunderwave Cave" - 120: "Mt. Steel" - 110: "Makuhita Dojo" - 15: "Sinister Woods" - 113: "Silent Chasm" - 111: "Mt. Thunder" - 126: "Mt. Thunder Peak" - 20: "Great Canyon" - 102: "Lapis Cave" - 25: "Mt. Blaze" - 33: "Mt. Blaze Peak" - 104: "Frosty Forest" - 39: "Frosty Grotto" - 115: "Mt. Freeze" - 123: "Mt. Freeze Peak" - 108: "Magma Cavern" - 124: "Magma Cavern Pit" - 22: "Sky Tower" - 23: "Sky Tower Summit" - 21: "Stormy Sea" - 13: "Buried Relic" -B24J_00: - Name: "Pokémon Mystery Dungeon: Red Rescue Team (Japan)" - SongTableOffsets: 0x1DC66BC - Copy: "B24E_00" -B24P_00: - Name: "Pokémon Mystery Dungeon: Red Rescue Team (Europe)" - Copy: "B24E_00" -BE8E_00: - Name: "Fire Emblem: The Sacred Stones (USA)" - SongTableOffsets: 0x224470 - SongTableSizes: 1000 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 13 - HasGoldenSunSynths: False - HasPokemonCompression: False -BE8J_00: - Name: "Fire Emblem: The Sacred Stones (Japan)" - SongTableOffsets: 0x214120 - Copy: "BE8E_00" -BE8P_00: - Name: "Fire Emblem: The Sacred Stones (Europe)" - SongTableOffsets: 0x42FFB0 - Copy: "BE8E_00" -BMXC_00: - Name: "Metroid: Zero Mission (China)" - SongTableOffsets: 0xA8AAC - Copy: "BMXE_00" -BMXE_00: - Name: "Metroid: Zero Mission (USA)" - SongTableOffsets: 0x8F2C0 - SongTableSizes: 708 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -BMXJ_00: - Name: "Metroid: Zero Mission (Japan)" - SongTableOffsets: 0x8F31C - Copy: "BMXE_00" -BMXP_00: - Name: "Metroid: Zero Mission (Europe)" - SongTableOffsets: 0x8FF4C - Copy: "BMXE_00" -BPED_00: - Name: "Pokémon Emerald Version (Germany)" - SongTableOffsets: 0x6C5BDC - Copy: "BPEE_00" -BPEE_00: - Name: "Pokémon Emerald Version (USA)" - SongTableOffsets: 0x6B49F0 - SongTableSizes: 610 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 12 - HasGoldenSunSynths: False - HasPokemonCompression: True - Playlists: - Music: - 350: "Unused - TETSUJI" - 351: "Unused - Route 38" - 352: "Victory! (Wild Pokémon) (No Intro)" - 353: "Victory! (Wild Pokémon)" - 354: "Victory! (Gym Leader)" - 355: "Victory! (Wallace)" - 356: "Unused - Pokémon Center (2)" - 357: "Unused - Viridian City" - 358: "Unused - Battle! (Entei/Raikou/Suicune)" - 359: "Route 101" - 360: "Route 110" - 361: "Route 120" - 362: "Petalburg City" - 363: "Oldale Town" - 364: "Pokémon Gym" - 365: "Surf" - 366: "Petalburg Woods" - 367: "Level Up!" - 368: "Pokémon Healed" - 369: "Obtained a Badge!" - 370: "Obtained an Item!" - 371: "Congratulations! Your Pokémon Evolved!" - 372: "Obtained a TM!" - 373: "Museum" - 374: "Introductions" - 375: "Oceanic Museum" - 376: "Evolution (Intro)" - 377: "Evolution" - 378: "Move Deleted" - 379: "Trainers' Eyes Meet (Tuber♀)" - 380: "Trainers' Eyes Meet (Youngster)" - 381: "Abandoned Ship" - 382: "Fortree City" - 383: "Birch Pokémon Lab" - 384: "Battle Tower (RS)" - 385: "Trainers' Eyes Meet (Swimmer♀)" - 386: "Cave of Origin" - 387: "Obtained a Berry!" - 388: "The Super-Ancient Pokémon Awaken!" - 389: "Jackpot" - 390: "Win" - 391: "Lose" - 392: "Reel Time" - 393: "Pokémon Contest! (Multiplayer - Player 1)" - 394: "Pokémon Contest! (Multiplayer - Player 2)" - 395: "Pokémon Contest! (Multiplayer - Player 3)" - 396: "Pokémon Contest! (Multiplayer - Player 4)" - 397: "Trainers' Eyes Meet (Gentleman)" - 398: "Verdanturf Town" - 399: "Rustboro City" - 400: "Pokémon Center" - 401: "Route 104" - 402: "Route 119" - 403: "Cycling" - 404: "Poké Mart" - 405: "Littleroot Town" - 406: "Mt. Chimney" - 407: "Trainers' Eyes Meet (Lass)" - 408: "Lilycove City" - 409: "Route 111" - 410: "H-Help Me!" - 411: "Dive" - 412: "Victory! (Trainer Battle)" - 413: "Title Screen" - 414: "Opening Movie: Setting out on a Journey in the Hoenn Region" - 415: "May" - 416: "Trainers' Eyes Meet (Psychic)" - 417: "Trainers' Eyes Meet (Cooltrainer)" - 418: "Route 113" - 419: "Team Aqua Appears!" - 420: "Hurry Along" - 421: "Brendan" - 422: "Evergrande City" - 423: "Trainers' Eyes Meet (Hex Maniac)" - 424: "Victory! (Team Aqua / Team Magma)" - 425: "Cable Car" - 426: "Game Corner" - 427: "Dewford Town" - 428: "Safari Zone" - 429: "Victory Road" - 430: "Hideout" - 431: "Crossing the Sea" - 432: "Mt. Pyre" - 433: "Slateport City" - 434: "Mt. Pyre Exterior" - 435: "Trainers' School" - 436: "The Hall of Fame" - 437: "Fallarbor Town" - 438: "Sealed Chamber" - 439: "Contest Winner" - 440: "Pokémon Contest!" - 441: "Team Magma Appears!" - 442: "Opening Movie: Double Battles" - 443: "Heavy Rain" - 444: "The Drought" - 445: "Sootopolis City" - 446: "Results Announcement" - 447: "Room of Glory" - 448: "The Trick House" - 449: "Twins" - 450: "The Elite Four Appear!" - 451: "Trainers' Eyes Meet (Hiker)" - 452: "Contest Lobby" - 453: "Interviewers" - 454: "Champion Wallace" - 455: "Ending Theme" - 456: "The End" - 457: "Battle Frontier" - 458: "Battle Arena" - 459: "Obtained a Battle Point!" - 460: "Registered Trainer!" - 461: "Battle Pyramid" - 462: "Battle Pyramid Summit" - 463: "Battle Palace" - 464: "Rayquaza Enters!" - 465: "Battle Tower" - 466: "Obtained a Frontier Symbol!" - 467: "Battle Dome" - 468: "Battle Pike" - 469: "Battle Factory" - 470: "Battle! (Rayquaza)" - 471: "Battle! (Frontier Brain)" - 472: "Battle! (Mew)" - 473: "Battle Dome Lobby" - 474: "Battle! (Wild Pokémon)" - 475: "Battle! (Team Aqua / Team Magma)" - 476: "Battle! (Trainer Battle)" - 477: "Battle! (Gym Leader)" - 478: "Battle! (Champion Wallace)" - 479: "Battle! (Regirock/Regice/Registeel)" - 480: "Battle! (Groudon/Kyogre)" - 481: "Battle! (Brendan/May/Steven)" - 482: "Battle! (Elite Four)" - 483: "Battle! (Team Aqua/Team Magma Leaders)" - 484: "Guide (FRLG)" - 485: "Rocket Game Corner (FRLG)" - 486: "Rocket Hideout (FRLG)" - 487: "Pokémon Gym (FRLG)" - 488: "Jigglypuff's Song (FRLG)" - 489: "Opening Movie (FRLG)" - 490: "Title Screen (FRLG)" - 491: "Cinnabar Island Theme (FRLG)" - 492: "Lavender Town Theme (FRLG)" - 493: "Pokémon Healed (FRLG)" - 494: "Cycling (FRLG)" - 495: "A Trainer Appears (Bad Guy Version) (FRLG)" - 496: "A Trainer Appears (Girl Version) (FRLG)" - 497: "A Trainer Appears (Boy Version) (FRLG)" - 498: "Hall of Fame (FRLG)" - 499: "Viridian Forest (FRLG)" - 500: "Navel Rock" - 501: "Pokémon Mansion (FRLG)" - 502: "Ending Theme (FRLG)" - 503: "Road to Viridian City: Leaving Pallet Town (FRLG)" - 504: "Welcome to the World of Pokémon! (FRLG)" - 505: "Road to Cerulean City: Leaving Mt. Moon (FRLG)" - 506: "Road to Fuchsia City: Leaving Lavender Town (FRLG)" - 507: "The Final Road (FRLG)" - 508: "Battle! (Gym Leader Battle) (FRLG)" - 509: "Battle! (Trainer Battle) (FRLG)" - 510: "Battle! (Wild Pokémon) (FRLG)" - 511: "Final Battle! (Rival) (FRLG)" - 512: "Pallet Town Theme (FRLG)" - 513: "Professor Oak's Laboratory (FRLG)" - 514: "Professor Oak (FRLG)" - 515: "Pokémon Center (FRLG)" - 516: "The S.S. Anne (FRLG)" - 517: "The Sea (FRLG)" - 518: "Pokémon Tower (FRLG)" - 519: "Silph Co. (FRLG)" - 520: "Fuchsia City Theme (FRLG)" - 521: "Celadon City Theme (FRLG)" - 522: "Victory! (Trainer Battle) (FRLG)" - 523: "Victory! (Wild Pokémon) (FRLG)" - 524: "Victory! (Gym Leader Battle) (FRLG)" - 525: "Vermillion City Theme (FRLG)" - 526: "Pewter City Theme (FRLG)" - 527: "A Rival Appears (FRLG)" - 528: "A Rival Appears (No Intro) (FRLG)" - 529: "Fanfare: Professor Oak's Evaluation (FRLG)" - 530: "Fanfare: Pokémon Obtained (FRLG)" - 531: "Fanfare: Pokémon Caught" - 532: "Pokémon Printer (FRLG)" - 533: "Game Freak Logo (FRLG)" - 534: "Fanfare: Pokémon Caught (No Intro) (FRLG)" - 535: "Game Tutorial (1) (FRLG)" - 536: "Game Tutorial (2) (FRLG)" - 537: "Game Tutorial (3) (FRLG)" - 538: "Pokémon Jump (FRLG)" - 539: "The Union Room (FRLG)" - 540: "Pokémon Net Center (FRLG)" - 541: "Mystery Gift (FRLG)" - 542: "Dodrio Berry Picking (FRLG)" - 543: "Mt. Ember (FRLG)" - 544: "Teachy TV Lesson (FRLG)" - 545: "Sevii Islands (FRLG)" - 546: "Tanoby Chambers (FRLG)" - 547: "Sevii Islands: One, Two & Three Islands (FRLG)" - 548: "Sevii Islands: Four & Five Islands (FRLG)" - 549: "Sevii Islands: Six & Seven Islands (FRLG)" - 550: "The Poké Flute (FRLG)" - 551: "Battle! (Deoxys)" - 552: "Battle! (Mewtwo) (FRLG)" - 553: "Battle! (Ho-Oh/Lugia)" - 554: "Tense Battle! (FRLG)" - 555: "Deoxys Appears" - 556: "Trainer Tower (FRLG)" - 557: "Epilogue (FRLG)" - 558: "Teachy TV Menu (FRLG)" - Battle Music: - 358: "Unused - Battle! (Entei/Raikou/Suicune)" - 470: "Battle! (Rayquaza)" - 471: "Battle! (Frontier Brain)" - 472: "Battle! (Mew)" - 474: "Battle! (Wild Pokémon)" - 475: "Battle! (Team Aqua / Team Magma)" - 476: "Battle! (Trainer Battle)" - 477: "Battle! (Gym Leader)" - 478: "Battle! (Champion Wallace)" - 479: "Battle! (Regirock/Regice/Registeel)" - 481: "Battle! (Brendan/May/Steven)" - 482: "Battle! (Elite Four)" - 483: "Battle! (Team Aqua/Team Magma Leaders)" - 508: "Battle! (Gym Leader Battle) (FRLG)" - 509: "Battle! (Trainer Battle) (FRLG)" - 510: "Battle! (Wild Pokémon) (FRLG)" - 511: "Final Battle! (Rival) (FRLG)" - 551: "Battle! (Deoxys)" - 552: "Battle! (Mewtwo) (FRLG)" - 553: "Battle! (Ho-Oh/Lugia)" -BPEF_00: - Name: "Pokémon Emerald Version (France)" - SongTableOffsets: 0x6B890C - Copy: "BPEE_00" -BPEI_00: - Name: "Pokémon Emerald Version (Italy)" - SongTableOffsets: 0x6B1004 - Copy: "BPEE_00" -BPEJ_00: - Name: "Pokémon Emerald Version (Japan)" - SongTableOffsets: 0x63C2AC - Copy: "BPEE_00" -BPES_00: - Name: "Pokémon Emerald Version (Spain)" - SongTableOffsets: 0x6B6FA4 - Copy: "BPEE_00" -BPGD_00: - Name: "Pokémon Leaf Green Version (Germany)" - SongTableOffsets: 0x4A0A5C - Copy: "BPRD_00" -BPGE_00: - Name: "Pokémon Leaf Green Version (USA)" - SongTableOffsets: 0x4A2BA8 - Copy: "BPRE_00" -BPGE_01: - SongTableOffsets: 0x4A2C18 - Copy: "BPGE_00" -BPGF_00: - Name: "Pokémon Leaf Green Version (France)" - SongTableOffsets: 0x4980BC - Copy: "BPRF_00" -BPGI_00: - Name: "Pokémon Leaf Green Version (Italy)" - SongTableOffsets: 0x4964B8 - Copy: "BPRI_00" -BPGJ_00: - Name: "Pokémon Leaf Green Version (Japan)" - SongTableOffsets: 0x467D50 - Copy: "BPRJ_00" -BPGS_00: - Name: "Pokémon Leaf Green Version (Spain)" - SongTableOffsets: 0x4992C0 - Copy: "BPRS_00" -BPPE_00: - Name: "Pokémon Pinball: Ruby and Sapphire (USA)" - SongTableOffsets: 0x534E04 - SongTableSizes: 333 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 14 - HasGoldenSunSynths: False - HasPokemonCompression: False -BPPJ_00: - Name: "Pokémon Pinball: Ruby & Sapphire (Japan)" - SongTableOffsets: 0x51B9BC - Copy: "BPPE_00" -BPPJ_01: - SongTableOffsets: 0x51B708 - Copy: "BPPJ_00" -BPPP_00: - Name: "Pokémon Pinball: Ruby & Sapphire (Europe)" - SongTableOffsets: 0x7C67A4 - Copy: "BPPE_00" -BPRD_00: - Name: "Pokémon Fire Red Version (Germany)" - SongTableOffsets: 0x4A18F0 - Copy: "BPRE_00" -BPRE_00: - Name: "Pokémon Fire Red Version (USA)" - SongTableOffsets: 0x4A32CC - SongTableSizes: 347 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 12 - HasGoldenSunSynths: False - HasPokemonCompression: True - Playlists: - Disc 1: - 321: "Game Freak Logo" - 277: "Opening Movie" - 278: "Title Screen" - 323: "Game Tutorial (1)" - 324: "Game Tutorial (2)" - 325: "Game Tutorial (3)" - 292: "Welcome to the World of Pokémon!" - 300: "Pallet Town Theme" - 302: "Professor Oak" - 301: "Professor Oak's Laboratory" - 318: "Fanfare: Pokémon Obtained" - 315: "A Rival Appears" - 297: "Battle! (Trainer Battle)" - 310: "Victory! (Trainer Battle)" - 291: "Road to Viridian City: Leaving Pallet Town" - 257: "Fanfare: Item Obtained (Version 1)" - 314: "Pewter City Theme" - 346: "Teachy TV Menu" - 287: "Viridian Forest" - 298: "Battle! (Wild Pokémon)" - 311: "Victory! (Wild Pokémon)" - 285: "A Trainer Appears (Boy Version)" - 303: "Pokémon Center" - 276: "Jigglypuff's Song" - 256: "Pokémon Healed" - 272: "Guide" - 275: "Pokémon Gym" - 342: "Tense Battle!" - 296: "Battle! (Gym Leader Battle)" - 312: "Victory! (Gym Leader Battle)" - 260: "Fanfare: Badge Obtained" - 263: "Evolution (Intro)" - 264: "Evolution" - 259: "Fanfare: Evolution" - 293: "Road to Cerulean City: Leaving Mt. Moon" - 284: "A Trainer Appears (Girl Version)" - 288: "Caves of Mt. Moon" - 313: "Vermilion City Theme" - 304: "The S.S. Anne" - 282: "Cycling" - 280: "Lavender Town Theme" - 306: "Pokémon Tower" - 309: "Celadon City Theme" - 273: "Rocket Game Corner" - 268: "Fanfare: Winning" - 269: "Fanfare: Jackpot" - 320: "Pokémon Printer" - 274: "Rocket Hideout" - 283: "A Trainer Appears (Bad Guy Version)" - 307: "Silph Co." - 294: "Road to Fuchsia City: Leaving Lavender Town" - 338: "The Poké Flute" - 308: "Fuchsia City Theme" - 270: "Move Deleted" - 305: "The Sea" - 341: "Battle! (Legendary Pokémon)" - 319: "Fanfare: Pokémon Caught" - 279: "Cinnabar Island Theme" - 289: "Pokémon Mansion" - 328: "Pokémon Net Center" - 317: "Fanfare: Professor Oak's Evaluation" - 336: "Sevii Islands: Four & Five Islands" - 326: "Pokémon Jump" - 330: "Dodrio Berry Picking" - 271: "Too Bad..." - 333: "Sevii Islands" - 337: "Sevii Islands: Six & Seven Islands" - 327: "The Union Room" - 329: "Mystery Gift" - 258: "Fanfare: Item Obtained (Version 2)" - 340: "Battle! (Mewtwo)" - 295: "The Final Road" - 299: "Final Battle! (Rival)" - 345: "Epilogue" - 286: "Hall of Fame" - 290: "Ending Theme" - Disc 2: - 343: "Deoxys Appears" - 339: "Battle! (Deoxys)" - Other Music: - 261: "Unused - Obtained a TM! (RS)" - 262: "Unused - Obtained a Berry! (RS)" - 265: "Battle! (Link Battle) (1)" - 266: "Battle! (Link Battle) (2)" - 267: "Unused - Trainers' School (RS)" - 281: "Unused - Pokémon Healed (2)" - 316: "A Rival Appears (No Intro)" - 322: "Fanfare: Pokémon Caught (No Intro)" - 331: "Mt. Ember" - 332: "Teachy TV Lesson" - 334: "Tanoby Chambers" - 335: "Sevii Islands: One, Two & Three Islands" - 344: "Trainer Tower" -BPRE_01: - SongTableOffsets: 0x4A332C - Copy: "BPRE_00" -BPRF_00: - Name: "Pokémon Fire Red Version (France)" - SongTableOffsets: 0x499394 - Copy: "BPRE_00" -BPRI_00: - Name: "Pokémon Fire Red Version (Italy)" - SongTableOffsets: 0x496D20 - Copy: "BPRE_00" -BPRJ_00: - Name: "Pokémon Fire Red Version (Japan)" - SongTableOffsets: 0x467F0C - Copy: "BPRE_00" -BPRJ_01: - SongTableOffsets: 0x463708 - Copy: "BPRJ_00" -BPRS_00: - Name: "Pokémon Fire Red Version (Spain)" - SongTableOffsets: 0x499BC8 - Copy: "BPRE_00" -BZME_00: - Name: "The Legend of Zelda: The Minish Cap (USA)" - SongTableOffsets: 0xA11DBC - SongTableSizes: 546 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -BZMJ_00: - Name: "The Legend of Zelda: The Minish Cap (Japan)" - SongTableOffsets: 0x9F3D3C - Copy: "BZME_00" -BZMP_00: - Name: "The Legend of Zelda: The Minish Cap (Europe)" - SongTableOffsets: 0xB1D414 - Copy: "BZME_00" -U32E_00: - Name: "Boktai 2 - Solar Boy Django (USA)" - SongTableOffsets: 0x25EEA8 - SongTableSizes: 1000 - SampleRate: 6 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -U32P_00: - Name: "Boktai 2 - Solar Boy Django (Europe)" - SongTableOffsets: 0x264758 - Copy: "U32E_00" -U3IE_00: - Name: "Boktai - The Sun is in Your Hand (USA)" - SongTableOffsets: 0x1EC904 - SongTableSizes: 906 - SampleRate: 6 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -U3IP_00: - Name: "Boktai - The Sun is in Your Hand (Europe)" - SongTableOffsets: 0x1E7604 - Copy: "U3IE_00" -U33J_00: - Name: "Boktai 3 - Sabata's Counterattack (Japan)" - SongTableOffsets: 0x26456C - SongTableSizes: 1483 - SampleRate: 6 - ReverbType: "Normal" - Reverb: 0 - Volume: 15 - HasGoldenSunSynths: False - HasPokemonCompression: False -V49E_00: - Name: "Drill Dozer (USA)" - SongTableOffsets: 0x5079B8 - SongTableSizes: 387 - SampleRate: 3 - ReverbType: "Normal" - Reverb: 0 - Volume: 10 - HasGoldenSunSynths: False - HasPokemonCompression: False \ No newline at end of file diff --git a/VG Music Studio.backup/MPlayDef.s b/VG Music Studio.backup/MPlayDef.s deleted file mode 100644 index a014a0a4..00000000 --- a/VG Music Studio.backup/MPlayDef.s +++ /dev/null @@ -1,465 +0,0 @@ -@*** -@ -@ MusicPlayDef.s (MPlayDef.s) ver1.05 -@ -@ Copyright (C) 1999-2001 NINTENDO Co.,Ltd. -@**************************************************************@ - -@*** -@ MML (without running status) -@******************************************************@ - - .equ W00, 0x80 @ WAIT - .equ W01, W00+1 @ - .equ W02, W00+2 @ - .equ W03, W00+3 @ - .equ W04, W00+4 @ - .equ W05, W00+5 @ - .equ W06, W00+6 @ - .equ W07, W00+7 @ - .equ W08, W00+8 @ - .equ W09, W00+9 @ - .equ W10, W00+10 @ - .equ W11, W00+11 @ - .equ W12, W00+12 @ - .equ W13, W00+13 @ - .equ W14, W00+14 @ - .equ W15, W00+15 @ - .equ W16, W00+16 @ - .equ W17, W00+17 @ - .equ W18, W00+18 @ - .equ W19, W00+19 @ - .equ W20, W00+20 @ - .equ W21, W00+21 @ - .equ W22, W00+22 @ - .equ W23, W00+23 @ - .equ W24, W00+24 @ - .equ W28, W00+25 @ - .equ W30, W00+26 @ - .equ W32, W00+27 @ - .equ W36, W00+28 @ - .equ W40, W00+29 @ - .equ W42, W00+30 @ - .equ W44, W00+31 @ - .equ W48, W00+32 @ - .equ W52, W00+33 @ - .equ W54, W00+34 @ - .equ W56, W00+35 @ - .equ W60, W00+36 @ - .equ W64, W00+37 @ - .equ W66, W00+38 @ - .equ W68, W00+39 @ - .equ W72, W00+40 @ - .equ W76, W00+41 @ - .equ W78, W00+42 @ - .equ W80, W00+43 @ - .equ W84, W00+44 @ - .equ W88, W00+45 @ - .equ W90, W00+46 @ - .equ W92, W00+47 @ - .equ W96, W00+48 @ - - .equ FINE, 0xb1 @ fine - .equ GOTO, 0xb2 @ goto - .equ PATT, 0xb3 @ pattern play - .equ PEND, 0xb4 @ pattern end - .equ REPT, 0xb5 @ repeat - .equ MEMACC, 0xb9 @ memacc op adr dat ***lib - .equ PRIO, 0xba @ priority - .equ TEMPO, 0xbb @ tempo (BPM/2) - .equ KEYSH, 0xbc @ key shift - -@*** -@ MML (within running status) -@******************************************************@ - - .equ VOICE, 0xbd @ voice # - .equ VOL, 0xbe @ volume - .equ PAN, 0xbf @ panpot (c_v+??) - .equ BEND, 0xc0 @ pitch bend (c_v+??) - .equ BENDR, 0xc1 @ bend range - .equ LFOS, 0xc2 @ LFO speed - .equ LFODL, 0xc3 @ LFO delay - .equ MOD, 0xc4 @ modulation depth - .equ MODT, 0xc5 @ modulation type - .equ TUNE, 0xc8 @ micro tuning (c_v+??) - - .equ XCMD, 0xcd @ extend command ***lib - .equ xIECV, 0x08 @ imi.echo vol ***lib - .equ xIECL, 0x09 @ imi.echo len ***lib - - .equ EOT, 0xce @ End of Tie - .equ TIE, 0xcf @ - .equ N01, TIE+1 @ NOTE - .equ N02, N01+1 @ - .equ N03, N01+2 @ - .equ N04, N01+3 @ - .equ N05, N01+4 @ - .equ N06, N01+5 @ - .equ N07, N01+6 @ - .equ N08, N01+7 @ - .equ N09, N01+8 @ - .equ N10, N01+9 @ - .equ N11, N01+10 @ - .equ N12, N01+11 @ - .equ N13, N01+12 @ - .equ N14, N01+13 @ - .equ N15, N01+14 @ - .equ N16, N01+15 @ - .equ N17, N01+16 @ - .equ N18, N01+17 @ - .equ N19, N01+18 @ - .equ N20, N01+19 @ - .equ N21, N01+20 @ - .equ N22, N01+21 @ - .equ N23, N01+22 @ - .equ N24, N01+23 @ - .equ N28, N01+24 @ - .equ N30, N01+25 @ - .equ N32, N01+26 @ - .equ N36, N01+27 @ - .equ N40, N01+28 @ - .equ N42, N01+29 @ - .equ N44, N01+30 @ - .equ N48, N01+31 @ - .equ N52, N01+32 @ - .equ N54, N01+33 @ - .equ N56, N01+34 @ - .equ N60, N01+35 @ - .equ N64, N01+36 @ - .equ N66, N01+37 @ - .equ N68, N01+38 @ - .equ N72, N01+39 @ - .equ N76, N01+40 @ - .equ N78, N01+41 @ - .equ N80, N01+42 @ - .equ N84, N01+43 @ - .equ N88, N01+44 @ - .equ N90, N01+45 @ - .equ N92, N01+46 @ - .equ N96, N01+47 @ - -@*** -@ Max value of operators -@******************************************************@ - - .equ mxv, 0x7F @ - -@*** -@ center value of PAN, BEND, TUNE -@******************************************************@ - - .equ c_v, 0x40 @ -64 ~ +63 - -@*** -@ parameter of N??, TIE, EOT -@******************************************************@ - - .equ CnM2, 0 @ - .equ CsM2, 1 @ - .equ DnM2, 2 @ - .equ DsM2, 3 @ - .equ EnM2, 4 @ - .equ FnM2, 5 @ - .equ FsM2, 6 @ - .equ GnM2, 7 @ - .equ GsM2, 8 @ - .equ AnM2, 9 @ - .equ AsM2, 10 @ - .equ BnM2, 11 @ - .equ CnM1, 12 @ - .equ CsM1, 13 @ - .equ DnM1, 14 @ - .equ DsM1, 15 @ - .equ EnM1, 16 @ - .equ FnM1, 17 @ - .equ FsM1, 18 @ - .equ GnM1, 19 @ - .equ GsM1, 20 @ - .equ AnM1, 21 @ - .equ AsM1, 22 @ - .equ BnM1, 23 @ - .equ Cn0, 24 @ - .equ Cs0, 25 @ - .equ Dn0, 26 @ - .equ Ds0, 27 @ - .equ En0, 28 @ - .equ Fn0, 29 @ - .equ Fs0, 30 @ - .equ Gn0, 31 @ - .equ Gs0, 32 @ - .equ An0, 33 @ - .equ As0, 34 @ - .equ Bn0, 35 @ - .equ Cn1, 36 @ - .equ Cs1, 37 @ - .equ Dn1, 38 @ - .equ Ds1, 39 @ - .equ En1, 40 @ - .equ Fn1, 41 @ - .equ Fs1, 42 @ - .equ Gn1, 43 @ - .equ Gs1, 44 @ - .equ An1, 45 @ - .equ As1, 46 @ - .equ Bn1, 47 @ - .equ Cn2, 48 @ - .equ Cs2, 49 @ - .equ Dn2, 50 @ - .equ Ds2, 51 @ - .equ En2, 52 @ - .equ Fn2, 53 @ - .equ Fs2, 54 @ - .equ Gn2, 55 @ - .equ Gs2, 56 @ - .equ An2, 57 @ - .equ As2, 58 @ - .equ Bn2, 59 @ - .equ Cn3, 60 @ - .equ Cs3, 61 @ - .equ Dn3, 62 @ - .equ Ds3, 63 @ - .equ En3, 64 @ - .equ Fn3, 65 @ - .equ Fs3, 66 @ - .equ Gn3, 67 @ - .equ Gs3, 68 @ - .equ An3, 69 @ 440Hz - .equ As3, 70 @ - .equ Bn3, 71 @ - .equ Cn4, 72 @ - .equ Cs4, 73 @ - .equ Dn4, 74 @ - .equ Ds4, 75 @ - .equ En4, 76 @ - .equ Fn4, 77 @ - .equ Fs4, 78 @ - .equ Gn4, 79 @ - .equ Gs4, 80 @ - .equ An4, 81 @ - .equ As4, 82 @ - .equ Bn4, 83 @ - .equ Cn5, 84 @ - .equ Cs5, 85 @ - .equ Dn5, 86 @ - .equ Ds5, 87 @ - .equ En5, 88 @ - .equ Fn5, 89 @ - .equ Fs5, 90 @ - .equ Gn5, 91 @ - .equ Gs5, 92 @ - .equ An5, 93 @ - .equ As5, 94 @ - .equ Bn5, 95 @ - .equ Cn6, 96 @ - .equ Cs6, 97 @ - .equ Dn6, 98 @ - .equ Ds6, 99 @ - .equ En6, 100 @ - .equ Fn6, 101 @ - .equ Fs6, 102 @ - .equ Gn6, 103 @ - .equ Gs6, 104 @ - .equ An6, 105 @ - .equ As6, 106 @ - .equ Bn6, 107 @ - .equ Cn7, 108 @ - .equ Cs7, 109 @ - .equ Dn7, 110 @ - .equ Ds7, 111 @ - .equ En7, 112 @ - .equ Fn7, 113 @ - .equ Fs7, 114 @ - .equ Gn7, 115 @ - .equ Gs7, 116 @ - .equ An7, 117 @ - .equ As7, 118 @ - .equ Bn7, 119 @ - .equ Cn8, 120 @ - .equ Cs8, 121 @ - .equ Dn8, 122 @ - .equ Ds8, 123 @ - .equ En8, 124 @ - .equ Fn8, 125 @ - .equ Fs8, 126 @ - .equ Gn8, 127 @ - -@*** -@ parameter of velocity -@******************************************************@ - - .equ v000, 0 @ - .equ v001, 1 @ - .equ v002, 2 @ - .equ v003, 3 @ - .equ v004, 4 @ - .equ v005, 5 @ - .equ v006, 6 @ - .equ v007, 7 @ - .equ v008, 8 @ - .equ v009, 9 @ - .equ v010, 10 @ - .equ v011, 11 @ - .equ v012, 12 @ - .equ v013, 13 @ - .equ v014, 14 @ - .equ v015, 15 @ - .equ v016, 16 @ - .equ v017, 17 @ - .equ v018, 18 @ - .equ v019, 19 @ - .equ v020, 20 @ - .equ v021, 21 @ - .equ v022, 22 @ - .equ v023, 23 @ - .equ v024, 24 @ - .equ v025, 25 @ - .equ v026, 26 @ - .equ v027, 27 @ - .equ v028, 28 @ - .equ v029, 29 @ - .equ v030, 30 @ - .equ v031, 31 @ - .equ v032, 32 @ - .equ v033, 33 @ - .equ v034, 34 @ - .equ v035, 35 @ - .equ v036, 36 @ - .equ v037, 37 @ - .equ v038, 38 @ - .equ v039, 39 @ - .equ v040, 40 @ - .equ v041, 41 @ - .equ v042, 42 @ - .equ v043, 43 @ - .equ v044, 44 @ - .equ v045, 45 @ - .equ v046, 46 @ - .equ v047, 47 @ - .equ v048, 48 @ - .equ v049, 49 @ - .equ v050, 50 @ - .equ v051, 51 @ - .equ v052, 52 @ - .equ v053, 53 @ - .equ v054, 54 @ - .equ v055, 55 @ - .equ v056, 56 @ - .equ v057, 57 @ - .equ v058, 58 @ - .equ v059, 59 @ - .equ v060, 60 @ - .equ v061, 61 @ - .equ v062, 62 @ - .equ v063, 63 @ - .equ v064, 64 @ - .equ v065, 65 @ - .equ v066, 66 @ - .equ v067, 67 @ - .equ v068, 68 @ - .equ v069, 79 @ - .equ v070, 70 @ - .equ v071, 71 @ - .equ v072, 72 @ - .equ v073, 73 @ - .equ v074, 74 @ - .equ v075, 75 @ - .equ v076, 76 @ - .equ v077, 77 @ - .equ v078, 78 @ - .equ v079, 79 @ - .equ v080, 80 @ - .equ v081, 81 @ - .equ v082, 82 @ - .equ v083, 83 @ - .equ v084, 84 @ - .equ v085, 85 @ - .equ v086, 86 @ - .equ v087, 87 @ - .equ v088, 88 @ - .equ v089, 89 @ - .equ v090, 90 @ - .equ v091, 91 @ - .equ v092, 92 @ - .equ v093, 93 @ - .equ v094, 94 @ - .equ v095, 95 @ - .equ v096, 96 @ - .equ v097, 97 @ - .equ v098, 98 @ - .equ v099, 99 @ - .equ v100, 100 @ - .equ v101, 101 @ - .equ v102, 102 @ - .equ v103, 103 @ - .equ v104, 104 @ - .equ v105, 105 @ - .equ v106, 106 @ - .equ v107, 107 @ - .equ v108, 108 @ - .equ v109, 109 @ - .equ v110, 110 @ - .equ v111, 111 @ - .equ v112, 112 @ - .equ v113, 113 @ - .equ v114, 114 @ - .equ v115, 115 @ - .equ v116, 116 @ - .equ v117, 117 @ - .equ v118, 118 @ - .equ v119, 119 @ - .equ v120, 120 @ - .equ v121, 121 @ - .equ v122, 122 @ - .equ v123, 123 @ - .equ v124, 124 @ - .equ v125, 125 @ - .equ v126, 126 @ - .equ v127, 127 @ - -@*** -@ parameter of gate+ -@******************************************************@ - - .equ gtp1, 1 @ - .equ gtp2, 2 @ - .equ gtp3, 3 @ - -@*** -@ parameter of MODT, BRET -@******************************************************@ - - .equ mod_vib,0 @ vibrate - .equ mod_tre,1 @ tremolo - .equ mod_pan,2 @ auto-panpot - -@*** -@ parameter of MEMACC -@******************************************************@ - - .equ mem_set,0 @ - .equ mem_add,1 @ - .equ mem_sub,2 @ - .equ mem_mem_set,3 @ - .equ mem_mem_add,4 @ - .equ mem_mem_sub,5 @ - .equ mem_beq,6 @ - .equ mem_bne,7 @ - .equ mem_bhi,8 @ - .equ mem_bhs,9 @ - .equ mem_bls,10 @ - .equ mem_blo,11 @ - .equ mem_mem_beq,12 @ - .equ mem_mem_bne,13 @ - .equ mem_mem_bhi,14 @ - .equ mem_mem_bhs,15 @ - .equ mem_mem_bls,16 @ - .equ mem_mem_blo,17 @ - -@*** -@ etc. -@******************************************************@ - - .equ reverb_set,0x80 @ SOUND_MODE_REVERB_SET - .equ PAM, PAN @ - diff --git a/VG Music Studio.backup/Program.cs b/VG Music Studio.backup/Program.cs deleted file mode 100644 index e2e25293..00000000 --- a/VG Music Studio.backup/Program.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.UI; -using System; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio -{ - internal static class Program - { - [STAThread] - private static void Main() - { -#if DEBUG - //Debug.GBAGameCodeScan(@"C:\Users\Kermalis\Documents\Emulation\GBA\Games"); -#endif - try - { - GlobalConfig.Init(); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorGlobalConfig); - return; - } - Application.EnableVisualStyles(); - Application.Run(MainForm.Instance); - } - } -} diff --git a/VG Music Studio.backup/Properties/AssemblyInfo.cs b/VG Music Studio.backup/Properties/AssemblyInfo.cs deleted file mode 100644 index 90411a11..00000000 --- a/VG Music Studio.backup/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Resources; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("VG Music Studio")] -[assembly: AssemblyDescription("Listen to the music from popular video game formats.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Kermalis")] -[assembly: AssemblyProduct("VG Music Studio")] -[assembly: AssemblyCopyright("Copyright © Kermalis 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("97c8acf8-66a3-4321-91d6-3e94eaca577f")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.0.0.2")] -[assembly: AssemblyFileVersion("0.0.0.2")] -[assembly: NeutralResourcesLanguage("en-US")] - diff --git a/VG Music Studio.backup/Properties/Icon.ico b/VG Music Studio.backup/Properties/Icon.ico deleted file mode 100644 index f9a64706d4c81795fbb20a1228f606492531f002..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3238 zcmcImO=ufO6nR^y~dE4H?XLbYB@8$%9qQ&7ppSu+?K3?&C&)+E%#h+Z8BbSOld z_F&%vJrsJ24x+Gvf)2iTFLH2hhTd|GN-jOhmqrKI)9=lUeypgyW#H+}d-MKw=FK-E z67&(3%Q?-zpCwu+BF{7OUo%9%LsuwRSrGklg(#OZ@=eH3uM*vaPZ{(bh~xd=SXo}C z+h0{^<<VzAJp%kky2_}C?_{fOHO!yie5Nj z(sZ2nR*FZ(jnF|kk#V~kd^)ez$=sXOr|MjR(c#l~2U%AHM?HpA9o zax`*C)ym{!P&(ck*H>zlJnMZgY>nwtwOVCY>-}_`w1){6YPG%E&|zy_KM3TfTSH{&4Wc zqHwV9N_p&@tofW+SHwQf!7~Yc<3##?F4TIl4ZZ8YT%I~7QbK>2z*bc8!&)gfPVut_ zdp>7Tg@e)SMVX7)e2TuM_d!$-uJyt!IH&-6tu~&*sa69gr}Z4%+OseBxp3v84h}sB91d3o z)_hXG#jR|IErYDU z=iOTX=X7jj0hrx@?1fDoKop+I4S?=qnz0YC7r1ic_(U$w;k`qf?8W`G?a z!&TJBvW2}h{+|3gwCm4Dog}c5zF480fOkVLx*7C*}FM!IR58UTt|7M(Gd$4)pXzQ?Tmu|nHx%g8w3ch@A#`or8Jzv;+%pH55 z^@ZA(%Ri%A*}bs;xO2_tMnmoU?(q%ZOBy_P6!xo?8$H7yvmP4~)j)zgmoasAA@f*L{S>Kw zhSYfpR3~+zG@fxz5A;FL8MhHBh_1JqJV}*FL(jsBPkd+#kC**9oMhoi=cR{}5bp7b z*C1Tr4V;G9=D$Y(fdI!41j5BeIu5QfICdX@L^QYiF6^5=yjqCg#kO@;@otr1$%Hq8 dDf$G2MvuP6x~JbGv%>=WhIdQhO;c}({sqamkn{im diff --git a/VG Music Studio.backup/Properties/Icon16.png b/VG Music Studio.backup/Properties/Icon16.png deleted file mode 100644 index acacf012d3debe19507e5ec994bdd1a563f26455..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 359 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt3( zJ+r@loG*ZCZg{#lhFJLbPITmJG7xax$=4~pxHEl%@`MA&v@4kOlONgcDm!j_N1|+M z_@A>=wz*9@?sIz2eTLre|MYK_?VnO(e_Nx`hHtTct4g<2$K9RW7j7@espy`n%cbch z#yX|xhBlWNoBwg=7b~waaMtKtXjnD(BvZzk9j=T{ik=OkJKoHCZq^dF-!!k`$@_wc z&-c!LT08&pl-7Xj&;EUQ|5}$#p+Hglf$4eA1`fs(3I{ExpO|y+`}F&ahx2NAwY}EO y<$Ku1Gf4%+*qC_0E2Ps@a#CxX?zhUme7mPEsqN0y%WeVv$l&Sf=d#Wzp$P!<-HfXM diff --git a/VG Music Studio.backup/Properties/Icon24.png b/VG Music Studio.backup/Properties/Icon24.png deleted file mode 100644 index 449d8e39fc42179ba431ecef81ff8f1f3f25ec22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 476 zcmV<20VDp2P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y9E;jv63FrU-0ZmCnK~zXfrINv|!Y~j8(+<+VAdr3yPy!NM zXn_i728lj$=0+b}5I4gZUbJ!6jsunko3(dd61fQ>e9de&e@*_LaEmn{-)(ncEW|C= zpa6yBf(C^ekgZqi2mxAqLA}OWY=DOi88nYECKn2EB_(V?DeUKf|K%b|m{Yl6-<1HH z7S(LCYPk^hvSM99dl*>j>C>Jxh}u9dv6b5f&9+Y{M7_NLE4NY)&Xz)OXur4N$vm1@ z+wJjiL)?IzTqCU$aJgQ?X0y+RXx$ZrBpRQ?U7c@C0CL_U_a-9#!~>^uxF3#P2!pi( z5DR@G;7y7Fk}Cj^Fc1O5%8`~nk)`+au>E^qHE7#5qW@{Y*yn*5VC8xPkEu@&@ke52 zB%avHYo8vv1#+NSLQjlTH)v7-(4d=PuVE@=u93r_pk`3O6k&>66u*eygyspZ1Dv|o S#%M$U0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y9E;jv63FrU-0mVr~K~z{r#gsu#!!Q&?d!0&b5J;SX9-&gZ z;R>99%T(eXS+nas*r4u0$Lb`LpK)TFN?3Z>d?$aN-NL5t`@6E;K3*rk=di_A5Pm!y z`x*&bY~^1C5W0KaC4m|VTWo;>QDRNR7F$pON^rd-GApzY!*e)^$uF&x3V(;77J<@qT!#;jqEA^|+)(0$hwbE(8Elnns?o)x}G*o>#w9%~YK z#)jvDT?1XaULYrez1a&T!44-tauO4P7#<&9g4bUv!TvJ=ZCXQWbp1YmUoNW>JRqMN zHe6$c!|hSv>wI36SQWs`wZd_GX|l)brvTXR--ZAMH4O*DMk<~@6Ig2q#_-H_Xt)X> zn75F-iC7zpw{YG+YHh*L4XE z2P(j;ZxV>%hllaM*b)%X(?g~-93%l3j(JTWhI3sdfFBI34UeCF?did=U@&Gh2f1K9 zIm5Yb761qbY{oD}PYn=`P}ct>0MI~B1Y)Sx8Hl+y9M~cy__@YV?zIF)Y!UE8D#K6` i-)X@XBliv+={LWVau*j&XqjOE0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y9E;jv63FrU-0@q1IK~!i%)s;(b)IbzPvz@=Ulgwz8Z$%O}M^(|9s26EyQAP zkiWmXA6kTn#oqka0g%4=d@}}U5h50QQUe?f5sN)Z0gi--#a>8WcmT$GLd0S(Y5)(R zF>|&AiN#)|0N??{+90vmgE{$2TnZA4J=hyS%!N2B^x&wxNFgSJUbG4THN@ngUZem= zLV1X@!&;L>KYjc#N^q~eHN1HM3A6|i4_$(Vl{!bL?+5GY`IH2)~CMqceHLx5ayq4Bxr6CuSm#i_4P?;-I^fGo84ru zxWhhmT+fSi=}UYmK;>nh zlarvHm){xy=6e5{L0-^zdEi1ya3yh(p1xio z`xGZ~Yq%)yAzX;-XaHc}rU8;X90}m^HNX*hm;^}Tq6UD6hldlj=cK3aftmzJ(&Fyj z-#^qOcZsE^PyL8Ii~*9gK%RSx04@(l^y>$x0H6@uB>*m%&^(a5gsHt+06<;=gbOBoE+paB z@(_!l1C!hwF3q|s%)O;N#G+6L2O-HhwI=b_!j)KbJcOiN6kJ{bdy9)$tmsJsa!4j& wr|!{{EjABN%T4^6cbP8a*t5dZ)H07*qoM6N<$g3Q5&p8x;= diff --git a/VG Music Studio.backup/Properties/Icon528.png b/VG Music Studio.backup/Properties/Icon528.png deleted file mode 100644 index 75601de2ef2e68898be2f817784b750d79ffbeb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10133 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 PydL+1rG@ihln?(8zw=kQ diff --git a/VG Music Studio.backup/Properties/Next.png b/VG Music Studio.backup/Properties/Next.png deleted file mode 100644 index dc839b50f18c9d65802cc276b8eaa1c7eeabce56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4026 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%0D$qRH; zyXtyZUEit5-_fJ&^J>Zk+;?M@Ps28yCFwT*4$p8uFn$-gRj>RKK}M5_qTQppzwt`b z9N0fuD%6E11`d4!NrL7hyD!T#fu^dJ^oKGfnFLdH(4~cTpQv7b+197fUcTZq*S13w z{uG@;u^0{`sMJFl!}NjZ&}TWsoe6RF0kWn|CFsC^`M}UPlCjMLT|@Av73bi9Piv5! Mwd1L(lYUqK2WetO7XSbN diff --git a/VG Music Studio.backup/Properties/Pause.ico b/VG Music Studio.backup/Properties/Pause.ico deleted file mode 100644 index 30b143620803667ca703b20759d56c50743d8b20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118062 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 diff --git a/VG Music Studio.backup/Properties/Pause.png b/VG Music Studio.backup/Properties/Pause.png deleted file mode 100644 index 9a736deda0138a1282a6d9c36f6f0b9190b9839c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1450 zcmeAS@N?(olHy`uVBq!ia0vp^b3mAb4M=wFIOGeY7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0h7I;J!GcfQS24TkI`72U@f)XXJ5hcO-X(i=}MX3x0iJ5sNdU>fO3MP7n zdWOkbbG#WCSfxE(978JN-d=KCC!8+ens{PDYRg`OQ(ZHZZ!n*laW%EgMIk8em;3dg zov&YL=GmXJwNS0yq!E&PXHK}kzyB}$e?MMVmMmtQ&Lk4)YMxb~=`b;LsBu@ya`jwT zyY4}dwTSq_=-7ix%QW79Qg*j(7?&a-#1@!_X4 zQ}5;4<=H*?95>(Wy?MS_cHHyN&jVso%%`WXNne+~{{4dY`>sAUUtiwv{>7Ps%CwpK zv%j`a|5mp7+4h=$GF{zoo_*^1>ho(|^;)=!^Rwk==l|Su{CiE^-9P&xKTkTi7HHD- z_q&cGn`3YOn*aZk&vO5teSUv-Jy)LX?z-!?diyK)GS|4Lx1BaVEivcd*%P!P?!NdW z?^^%8r{|z7@4HX$m+qHtI4#ItUvcaK&pS8lbe`L>2Yqbc6L#2a?l~xV2dH(35WRZ; z1HY7?KOu4F;sd?^aldvm?vN?`(4Nk7?m=D&b~-JE=UqktKh2}z?1`Fte{R&Aer~(( z`RCWq!dbV^i{1ZPZu6oEZlKI zx$WM?nGb`k1;iKO%OP8x3k7#w6^-EvND#fd;^0yS*RYGdQb2CX5aX83i~GyIsW5h$ Uqzzvruqb8lboFyt=akR{092E?-2eap diff --git a/VG Music Studio.backup/Properties/Play.ico b/VG Music Studio.backup/Properties/Play.ico deleted file mode 100644 index e8be3d8757c7a4f9b418a3650f1cd6c69278d3db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140062 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 F;r}DLY(@Y8 diff --git a/VG Music Studio.backup/Properties/Play.png b/VG Music Studio.backup/Properties/Play.png deleted file mode 100644 index f386fd325480bec804b628fe9fb3016a349159db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2298 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 diff --git a/VG Music Studio.backup/Properties/Previous.ico b/VG Music Studio.backup/Properties/Previous.ico deleted file mode 100644 index fe2ef73e77e4f7b2ae6c6c6f1701a2b5ffdaa4bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140062 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 diff --git a/VG Music Studio.backup/Properties/Previous.png b/VG Music Studio.backup/Properties/Previous.png deleted file mode 100644 index 63496267cc5cabedf50440f1a3b095a96bef2eb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3708 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

-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 y2S{H_$3dd9qdC6QL6S$6aNP{V$@my diff --git a/VG Music Studio.backup/Properties/Resources.Designer.cs b/VG Music Studio.backup/Properties/Resources.Designer.cs deleted file mode 100644 index 1aa1551a..00000000 --- a/VG Music Studio.backup/Properties/Resources.Designer.cs +++ /dev/null @@ -1,133 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Kermalis.VGMusicStudio.Properties { - using System; - - - ///

- /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon Icon { - get { - object obj = ResourceManager.GetObject("Icon", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon IconNext { - get { - object obj = ResourceManager.GetObject("IconNext", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon IconPause { - get { - object obj = ResourceManager.GetObject("IconPause", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon IconPlay { - get { - object obj = ResourceManager.GetObject("IconPlay", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap IconPlaylist { - get { - object obj = ResourceManager.GetObject("IconPlaylist", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon IconPrevious { - get { - object obj = ResourceManager.GetObject("IconPrevious", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap IconSong { - get { - object obj = ResourceManager.GetObject("IconSong", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - } -} diff --git a/VG Music Studio.backup/Properties/Resources.resx b/VG Music Studio.backup/Properties/Resources.resx deleted file mode 100644 index ad871cdc..00000000 --- a/VG Music Studio.backup/Properties/Resources.resx +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - ..\Properties\Icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - Next.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - Pause.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - Play.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Properties\Playlist.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - Previous.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Properties\Song.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - \ No newline at end of file diff --git a/VG Music Studio.backup/Properties/Settings.Designer.cs b/VG Music Studio.backup/Properties/Settings.Designer.cs deleted file mode 100644 index 875ce0f2..00000000 --- a/VG Music Studio.backup/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Kermalis.VGMusicStudio.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/VG Music Studio.backup/Properties/Settings.settings b/VG Music Studio.backup/Properties/Settings.settings deleted file mode 100644 index 39645652..00000000 --- a/VG Music Studio.backup/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/VG Music Studio.backup/Properties/Song.png b/VG Music Studio.backup/Properties/Song.png deleted file mode 100644 index 62a132e5d49752cc1cccbe330aeb2d0148cbbbe5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2328 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fo{p?&#~tz_78O`%fY(0|PTd zfKP}kP<#}OhQNpofz1YAIzSnvB*-uL|HuXm!Qa_cfeC@Lz$3Dlfr0N32s4Umcr^e8 zB}-f*N`mv#O3D+9QW**oGxJLH@={9_O!N%&j0`_2WKIPZFtwg8jv*CsZzo2UNgML8 z)c*Ijo)pUWc+W*0mSvt&vG%eDopq-6pMMm=5Gls3a5{-GB~qtBr<-*`>PCi5I${pf zjxwFtM3!V<(vSKBCs^(FJ^U`f#`pY}dWf4rp4s;O3(Oi47w`Ndy{alr&>)XLh&SDQ z#p#U<*y)*W2kJlE{Ch_00r$SWQ&+OuJp6roM%)4Bd3O$TG3AABXlsxbU8D5CjP*6g zj?BxgEO!S!pGKNV$N@@~(PhBHgA zi%i(Dm32av#S-aTM?elJ|KdE~;565Xs5?v^rZIvIZh_ZCCv4dXQliANHYtK(Wt(); z1??7)fFj3PC6EAj(*>PFx0r;sZ3h~_-m);(el<78`+T5{*Yb13&VRW2PCftBY#-Hx z)pZ>F(pUVgX80ap=34$xGN8sObv`F+OxS_d`Wkx|Y&$)LX&3K@-_omgpV3@!nPLAw zr^@M`tTCn^YR=pLYmD0s41!&ayct_hZ($PRxsWlxlC_2R!n1}>RuQl2$xr&hXKx+F2TK_3_8pUAIw$eq^lBZY2LX)nvFVbGyV-s$ zX)k5j6MkU+?+DKW+nD}cIUU6$_u(gZmf$qu3B90j+3gtHM?Lyy!43W&mzlYEUlqNR zJQFy3&(BAp*NP7(=-ohPwmj#pOOgVT*ped}#h^^R(r|P7q#2Hq{47KA- zC+aK|xb8k_Vc%RYaqA>72Z&$mceb|_bNF`XSK$kFb&DBqg%>Q8;a0Ft{LPY~0o34k z!;Vqv6i}t`HvfjUBAtdeEhtLT#TdU_PG(Hee9j7m!dhW5<%;T#*D5wrKJTRFl(EI}sMiX -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Kermalis.VGMusicStudio.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to {0} key. - /// - internal static string ConfigKeySubkey { - get { - return ResourceManager.GetString("ConfigKeySubkey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Would you like to stop playing the current playlist?. - /// - internal static string EndPlaylistBody { - get { - return ResourceManager.GetString("EndPlaylistBody", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid command in track {0} at 0x{1:X}: 0x{2:X}. - /// - internal static string ErrorAlphaDreamDSEMP2KSDATInvalidCommand { - get { - return ResourceManager.GetString("ErrorAlphaDreamDSEMP2KSDATInvalidCommand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot copy invalid game code "{0}". - /// - internal static string ErrorAlphaDreamMP2KCopyInvalidGameCode { - get { - return ResourceManager.GetString("ErrorAlphaDreamMP2KCopyInvalidGameCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Game code "{0}" is missing.. - /// - internal static string ErrorAlphaDreamMP2KMissingGameCode { - get { - return ResourceManager.GetString("ErrorAlphaDreamMP2KMissingGameCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error parsing game code "{0}" in "{1}"{2}. - /// - internal static string ErrorAlphaDreamMP2KParseGameCode { - get { - return ResourceManager.GetString("ErrorAlphaDreamMP2KParseGameCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Playlist "{0}" has song {1} defined more than once between decimal and hexadecimal.. - /// - internal static string ErrorAlphaDreamMP2KSongRepeated { - get { - return ResourceManager.GetString("ErrorAlphaDreamMP2KSongRepeated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" count must be the same as "{1}" count.. - /// - internal static string ErrorAlphaDreamMP2KSongTableCounts { - get { - return ResourceManager.GetString("ErrorAlphaDreamMP2KSongTableCounts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" must be True or False.. - /// - internal static string ErrorBoolParse { - get { - return ResourceManager.GetString("ErrorBoolParse", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Color {0} has an invalid key.. - /// - internal static string ErrorConfigColorInvalidKey { - get { - return ResourceManager.GetString("ErrorConfigColorInvalidKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Color {0} is not defined.. - /// - internal static string ErrorConfigColorMissing { - get { - return ResourceManager.GetString("ErrorConfigColorMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Color {0} is defined more than once between decimal and hexadecimal.. - /// - internal static string ErrorConfigColorRepeated { - get { - return ResourceManager.GetString("ErrorConfigColorRepeated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" is invalid.. - /// - internal static string ErrorConfigKeyInvalid { - get { - return ResourceManager.GetString("ErrorConfigKeyInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" is missing.. - /// - internal static string ErrorConfigKeyMissing { - get { - return ResourceManager.GetString("ErrorConfigKeyMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" must have at least one entry.. - /// - internal static string ErrorConfigKeyNoEntries { - get { - return ResourceManager.GetString("ErrorConfigKeyNoEntries", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown header version: 0x{0:X}. - /// - internal static string ErrorDSEInvalidHeaderVersion { - get { - return ResourceManager.GetString("ErrorDSEInvalidHeaderVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid key in track {0} at 0x{1:X}: {2}. - /// - internal static string ErrorDSEInvalidKey { - get { - return ResourceManager.GetString("ErrorDSEInvalidKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are no "bgm(NNNN).smd" files.. - /// - internal static string ErrorDSENoSequences { - get { - return ResourceManager.GetString("ErrorDSENoSequences", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading Global Config. - /// - internal static string ErrorGlobalConfig { - get { - return ResourceManager.GetString("ErrorGlobalConfig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading Song {0}. - /// - internal static string ErrorLoadSong { - get { - return ResourceManager.GetString("ErrorLoadSong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid running status command in track {0} at 0x{1:X}: 0x{2:X}. - /// - internal static string ErrorMP2KInvalidRunningStatusCommand { - get { - return ResourceManager.GetString("ErrorMP2KInvalidRunningStatusCommand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Too many nested call events in track {0}. - /// - internal static string ErrorMP2KSDATNestedCalls { - get { - return ResourceManager.GetString("ErrorMP2KSDATNestedCalls", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading GBA ROM (AlphaDream). - /// - internal static string ErrorOpenAlphaDream { - get { - return ResourceManager.GetString("ErrorOpenAlphaDream", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading DSE Folder. - /// - internal static string ErrorOpenDSE { - get { - return ResourceManager.GetString("ErrorOpenDSE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading GBA ROM (MP2K). - /// - internal static string ErrorOpenMP2K { - get { - return ResourceManager.GetString("ErrorOpenMP2K", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading SDAT File. - /// - internal static string ErrorOpenSDAT { - get { - return ResourceManager.GetString("ErrorOpenSDAT", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error parsing "{0}"{1}. - /// - internal static string ErrorParseConfig { - get { - return ResourceManager.GetString("ErrorParseConfig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Exporting DLS. - /// - internal static string ErrorSaveDLS { - get { - return ResourceManager.GetString("ErrorSaveDLS", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Exporting MIDI. - /// - internal static string ErrorSaveMIDI { - get { - return ResourceManager.GetString("ErrorSaveMIDI", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Exporting SF2. - /// - internal static string ErrorSaveSF2 { - get { - return ResourceManager.GetString("ErrorSaveSF2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Exporting WAV. - /// - internal static string ErrorSaveWAV { - get { - return ResourceManager.GetString("ErrorSaveWAV", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This SDAT archive has no sequences.. - /// - internal static string ErrorSDATNoSequences { - get { - return ResourceManager.GetString("ErrorSDATNoSequences", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" is not an integer value.. - /// - internal static string ErrorValueParse { - get { - return ResourceManager.GetString("ErrorValueParse", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" must be between {1} and {2}.. - /// - internal static string ErrorValueParseRanged { - get { - return ResourceManager.GetString("ErrorValueParseRanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to GBA Files. - /// - internal static string FilterOpenGBA { - get { - return ResourceManager.GetString("FilterOpenGBA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SDAT Files. - /// - internal static string FilterOpenSDAT { - get { - return ResourceManager.GetString("FilterOpenSDAT", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DLS Files. - /// - internal static string FilterSaveDLS { - get { - return ResourceManager.GetString("FilterSaveDLS", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MIDI Files. - /// - internal static string FilterSaveMIDI { - get { - return ResourceManager.GetString("FilterSaveMIDI", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SF2 Files. - /// - internal static string FilterSaveSF2 { - get { - return ResourceManager.GetString("FilterSaveSF2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WAV Files. - /// - internal static string FilterSaveWAV { - get { - return ResourceManager.GetString("FilterSaveWAV", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Data. - /// - internal static string MenuData { - get { - return ResourceManager.GetString("MenuData", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to End Current Playlist. - /// - internal static string MenuEndPlaylist { - get { - return ResourceManager.GetString("MenuEndPlaylist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to File. - /// - internal static string MenuFile { - get { - return ResourceManager.GetString("MenuFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open GBA ROM (AlphaDream). - /// - internal static string MenuOpenAlphaDream { - get { - return ResourceManager.GetString("MenuOpenAlphaDream", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open DSE Folder. - /// - internal static string MenuOpenDSE { - get { - return ResourceManager.GetString("MenuOpenDSE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open GBA ROM (MP2K). - /// - internal static string MenuOpenMP2K { - get { - return ResourceManager.GetString("MenuOpenMP2K", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open SDAT File. - /// - internal static string MenuOpenSDAT { - get { - return ResourceManager.GetString("MenuOpenSDAT", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Playlist. - /// - internal static string MenuPlaylist { - get { - return ResourceManager.GetString("MenuPlaylist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export VoiceTable as DLS. - /// - internal static string MenuSaveDLS { - get { - return ResourceManager.GetString("MenuSaveDLS", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export Song as MIDI. - /// - internal static string MenuSaveMIDI { - get { - return ResourceManager.GetString("MenuSaveMIDI", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export VoiceTable as SF2. - /// - internal static string MenuSaveSF2 { - get { - return ResourceManager.GetString("MenuSaveSF2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export Song as WAV. - /// - internal static string MenuSaveWAV { - get { - return ResourceManager.GetString("MenuSaveWAV", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to C;C#;D;D#;E;F;F#;G;G#;A;A#;B. - /// - internal static string Notes { - get { - return ResourceManager.GetString("Notes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Next Song. - /// - internal static string PlayerNextSong { - get { - return ResourceManager.GetString("PlayerNextSong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Notes. - /// - internal static string PlayerNotes { - get { - return ResourceManager.GetString("PlayerNotes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Pause. - /// - internal static string PlayerPause { - get { - return ResourceManager.GetString("PlayerPause", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Play. - /// - internal static string PlayerPlay { - get { - return ResourceManager.GetString("PlayerPlay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Position. - /// - internal static string PlayerPosition { - get { - return ResourceManager.GetString("PlayerPosition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Previous Song. - /// - internal static string PlayerPreviousSong { - get { - return ResourceManager.GetString("PlayerPreviousSong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Rest. - /// - internal static string PlayerRest { - get { - return ResourceManager.GetString("PlayerRest", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Stop. - /// - internal static string PlayerStop { - get { - return ResourceManager.GetString("PlayerStop", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tempo. - /// - internal static string PlayerTempo { - get { - return ResourceManager.GetString("PlayerTempo", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Type. - /// - internal static string PlayerType { - get { - return ResourceManager.GetString("PlayerType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unpause. - /// - internal static string PlayerUnpause { - get { - return ResourceManager.GetString("PlayerUnpause", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Music. - /// - internal static string PlaylistMusic { - get { - return ResourceManager.GetString("PlaylistMusic", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Would you like to play the following playlist?{0}. - /// - internal static string PlayPlaylistBody { - get { - return ResourceManager.GetString("PlayPlaylistBody", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to VoiceTable saved to {0}.. - /// - internal static string SuccessSaveDLS { - get { - return ResourceManager.GetString("SuccessSaveDLS", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MIDI saved to {0}.. - /// - internal static string SuccessSaveMIDI { - get { - return ResourceManager.GetString("SuccessSaveMIDI", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to VoiceTable saved to {0}.. - /// - internal static string SuccessSaveSF2 { - get { - return ResourceManager.GetString("SuccessSaveSF2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WAV saved to {0}.. - /// - internal static string SuccessSaveWAV { - get { - return ResourceManager.GetString("SuccessSaveWAV", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Arguments. - /// - internal static string TrackViewerArguments { - get { - return ResourceManager.GetString("TrackViewerArguments", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event. - /// - internal static string TrackViewerEvent { - get { - return ResourceManager.GetString("TrackViewerEvent", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Offset. - /// - internal static string TrackViewerOffset { - get { - return ResourceManager.GetString("TrackViewerOffset", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ticks. - /// - internal static string TrackViewerTicks { - get { - return ResourceManager.GetString("TrackViewerTicks", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Track Viewer. - /// - internal static string TrackViewerTitle { - get { - return ResourceManager.GetString("TrackViewerTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Track {0}. - /// - internal static string TrackViewerTrackX { - get { - return ResourceManager.GetString("TrackViewerTrackX", resourceCulture); - } - } - } -} diff --git a/VG Music Studio.backup/Properties/Strings.es.resx b/VG Music Studio.backup/Properties/Strings.es.resx deleted file mode 100644 index d98bb266..00000000 --- a/VG Music Studio.backup/Properties/Strings.es.resx +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Quisiera detener la Lista de Reproducción actual? - - - Error al Cargar Canción {0} - - - Error al Abrir Carpeta DSE - - - Error al Abrir GBA ROM (AlphaDream) - - - Error al Abrir GBA ROM (MP2K) - - - Error al Abrir Archivo SDAT - - - Error al Exportar MIDI - - - Archivo GBA - - - Archivo SDAT - - - Archivos MIDI - - - Datos - - - Detener Lista de Reproducción Actual - - - Archivo - - - Abrir Carpeta DSE - - - Abrir GBA ROM (AlphaDream) - - - Abrir GBA ROM (MP2K) - - - Abrir Archivo SDAT - - - Playlist - - - Exportar Canción como MIDI - - - Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si - - - Siguiente Canción - - - Notas - - - Pausar - - - Reproducir - - - Posición - - - Canción Anterior - - - Retraso - - - Detener - - - Tempo - - - Tipo - - - Resumir - - - Música - - - Quisiera reproducir la siguiente Lista de Reproducción? {0} - - - MIDI guardado en {0}. - - - Argumento - - - Evento - - - Offset - - - Ticks - - - Visor de Eventos - - - Pista {0} - - - {0} clave - - - "{0}" debe ser Verdadero o Falso. - - - El color {0} tiene una clave inválida. - - - El color {0} no está definido. - - - El color {0} está definido más de una vez entre decimal y hexadecimal. - - - "{0}" es inválido. - - - "{0}" no se encuentra. - - - "{0}" debe tener al menos una entrada. - - - Versión del encabezado desconocida: 0x{0:X} - - - Clave inválida en la pista {0} en 0x{1:X}: {2} - - - Comando inválido en la pista {0} en 0x{1:X}: 0x{2:X} - - - No hay ningún archivo "bgm(NNNN).smd". - - - Error al Cargar Configuración Global - - - No se puede copiar, el código del juego "{0}" es inválido. - - - No se encuentra el código del juego "{0}". - - - Error analizando el código del juego "{0}" en "{1}"{2} - - - La Lista de Reproducción "{0}" tiene a la canción {1} definida más de una vez entre decimal y hexadecimal. - - - El número de "{0}" debe ser igual al de "{1}". - - - Comando de estado en ejecución inválido en la pista {0} en 0x{1:X}: 0x{2:X} - - - Demasiadas llamadas a eventos anidadas en la pista {0} - - - Error analizando "{0}"{1} - - - Este archivo SDAT no tiene secuencias. - - - "{0}" no es un valor entero. - - - "{0}" debe estar entre {1} y {2}. - - - Error al Exportar WAV - - - Archivos WAV - - - Exportar Canción como WAV - - - WAV guardado en {0}. - - - Error al Exportar SF2 - - - Archivos SF2 - - - Exportar VoiceTable como SF2 - - - SF2 guardado en {0}. - - - Error al Exportar DLS - - - Archivos DLS - - - Exportar VoiceTable como DLS - - - DLS guardado en {0}. - - \ No newline at end of file diff --git a/VG Music Studio.backup/Properties/Strings.it.resx b/VG Music Studio.backup/Properties/Strings.it.resx deleted file mode 100644 index 935f17e8..00000000 --- a/VG Music Studio.backup/Properties/Strings.it.resx +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Errore durante il caricamento del Brano {0} - - - Errore durante il caricamento della ROM GBA (AlphaDream) - - - Errore Durante L'Esportazione in MIDI - - - File GBA - - - File MIDI - - - Dati - - - File - - - Apri ROM GBA (MP2K) - - - Esporta Brano in MIDI - - - Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si - - - Pausa - - - Note - - - Pausa - - - Play - - - Posizione - - - Stop - - - Tempo - - - Tipo - - - Riprendi - - - MIDI salvato in {0}. - - - Desideri riprodurre la seguente playlist?{0} - - - Brano Sucessivo - - - Brano Precedente - - - Desideri interrompere la riproduzione della playlist attuale? - - - Errore durante il caricamento della Cartella DSE - - - Errore durante il caricamento della ROM GBA (MP2K) - - - Errore durante il caricamento del File SDAT - - - File SDAT - - - Termina la Playlist Attuale - - - Apri Cartella DSE - - - Apri ROM GBA (AlphaDream) - - - Apri File SDAT - - - Playlist - - - Musica - - - Argomenti - - - Evento - - - Posizione - - - Tick - - - Visualizzatore Traccia - - - Traccia {0} - - - La Chiave {0} - - - "{0}" deve essere Vero o Falso. - - - Il colore {0} non ha una chiave valida. - - - Il colore {0} non è definito. - - - Il colore {0} è definito più di una volta tra decimale ed esadecimale. - - - "{0}" non è valido. - - - "{0}" è mancante. - - - "{0}" deve possedere almeno una voce. - - - Versione dell'header sconosciuta: 0x{0:X} - - - Tasto del pianoforte non valido nella traccia {0} a 0x{1:X}: {2} - - - Comando non valido nella traccia {0} a 0x{1:X}: 0x{2:X} - - - Non sono presenti file "bgm(NNNN).smd". - - - Errore nel Caricamento della Configurazione Globale - - - Non è possibile copiare il Codice Gioco invalido "{0}" - - - Il Codice Gioco "{0}" non è presente. - - - Errore nell'analisi del Codice Gioco "{0}" in "{1}"{2} - - - La Playlist "{0}" presenta il brano {1} più volte definito tra decimale e esadecimale. - - - Il numero di "{0}" deve essere uguale a quello di "{1}". - - - Comando di stato in esecuzione non valido nella traccia {0} a 0x{1:X}: 0x{2:X} - - - Troppi eventi di chiamata nidificati nella traccia {0} - - - Errore nell'analisi di "{0}"{1} - - - Questo archivio SDAT non ha sequenze. - - - "{0}" non è un valore intero. - - - "{0}" deve essere compreso tra {1} e {2}. - - - Errore Durante L'Esportazione in WAV - - - File WAV - - - Esporta Brano in WAV - - - WAV salvato in {0}. - - - Errore Durante L'Esportazione in SF2 - - - File SF2 - - - Esporta VoiceTable In SF2 - - - VoiceTable salvata in {0}. - - - Errore Durante L'Esportazione in DLS - - - File DLS - - - Esporta VoiceTable In DLS - - - VoiceTable salvata in {0}. - - \ No newline at end of file diff --git a/VG Music Studio.backup/Properties/Strings.resx b/VG Music Studio.backup/Properties/Strings.resx deleted file mode 100644 index 8e4ae4a1..00000000 --- a/VG Music Studio.backup/Properties/Strings.resx +++ /dev/null @@ -1,376 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Error Loading Song {0} - {0} is the song number. - - - Error Loading GBA ROM (AlphaDream) - - - Error Exporting MIDI - - - GBA Files - - - MIDI Files - - - Data - - - File - - - Open GBA ROM (MP2K) - - - Export Song as MIDI - - - C;C#;D;D#;E;F;F#;G;G#;A;A#;B - - - Rest - - - Notes - - - Pause - - - Play - - - Position - - - Stop - - - Tempo - - - Type - - - Unpause - - - MIDI saved to {0}. - {0} is the file name. - - - Would you like to play the following playlist?{0} - {0} is a newline character followed by the playlist name. - - - Next Song - - - Previous Song - - - Would you like to stop playing the current playlist? - - - Error Loading DSE Folder - - - Error Loading GBA ROM (MP2K) - - - Error Loading SDAT File - - - SDAT Files - - - End Current Playlist - - - Open DSE Folder - - - Open GBA ROM (AlphaDream) - - - Open SDAT File - - - Playlist - - - Music - - - Arguments - - - Event - - - Offset - - - Ticks - - - Track Viewer - - - Track {0} - {0} is the track number. - - - {0} key - {0} is the parent key name. - - - "{0}" must be True or False. - {0} is the value name. - - - Color {0} has an invalid key. - {0} is the color number. - - - Color {0} is not defined. - {0} is the color number. - - - Color {0} is defined more than once between decimal and hexadecimal. - {0} is the color number. - - - "{0}" is invalid. - {0} is the invalid key. - - - "{0}" is missing. - {0} is the missing key. - - - "{0}" must have at least one entry. - {0} is the key. - - - Unknown header version: 0x{0:X} - {0} is the header version. - - - Invalid key in track {0} at 0x{1:X}: {2} - {0} is the track number, {1} is the offset, {2} is the key. - - - Invalid command in track {0} at 0x{1:X}: 0x{2:X} - {0} is the track number, {1} is the offset, {2} is the command. - - - There are no "bgm(NNNN).smd" files. - - - Error Loading Global Config - - - Cannot copy invalid game code "{0}" - {0} is the invalid game code. - - - Game code "{0}" is missing. - {0} is the game code. - - - Error parsing game code "{0}" in "{1}"{2} - {0} is the game code, {1} is the config filename, {2} is a newline character followed by the error message. - - - Playlist "{0}" has song {1} defined more than once between decimal and hexadecimal. - {0} is the playlist name, {1} is the song index. - - - "{0}" count must be the same as "{1}" count. - {0} is key 1, {1} is key 2. - - - Invalid running status command in track {0} at 0x{1:X}: 0x{2:X} - {0} is the track number, {1} is the offset, {2} is the command. - - - Too many nested call events in track {0} - {0} is the track number. - - - Error parsing "{0}"{1} - {0} is the config filename, {1} is a newline character followed by the error message. - - - This SDAT archive has no sequences. - - - "{0}" is not an integer value. - {0} is the value name. - - - "{0}" must be between {1} and {2}. - {0} is the value name, {1} is the minimum allowed value, {2} is the maximum allowed value. - - - Error Exporting WAV - - - WAV Files - - - Export Song as WAV - - - WAV saved to {0}. - {0} is the file name. - - - Error Exporting SF2 - - - SF2 Files - - - Export VoiceTable as SF2 - - - VoiceTable saved to {0}. - {0} is the file name. - - - Error Exporting DLS - - - DLS Files - - - Export VoiceTable as DLS - - - VoiceTable saved to {0}. - {0} is the file name. - - \ No newline at end of file diff --git a/VG Music Studio.backup/UI/ColorSlider.cs b/VG Music Studio.backup/UI/ColorSlider.cs deleted file mode 100644 index b23df9ce..00000000 --- a/VG Music Studio.backup/UI/ColorSlider.cs +++ /dev/null @@ -1,485 +0,0 @@ -#region License - -/* Copyright (c) 2017 Fabrice Lacharme - * This code is inspired from Michal Brylka - * https://www.codeproject.com/Articles/17395/Owner-drawn-trackbar-slider - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#endregion - - -using System; -using System.ComponentModel; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory(""), ToolboxBitmap(typeof(TrackBar))] - internal class ColorSlider : Control - { - private const int thumbSize = 14; - private Rectangle thumbRect; - - private long _value = 0L; - public long Value - { - get => _value; - set - { - if (value >= _minimum && value <= _maximum) - { - _value = value; - ValueChanged?.Invoke(this, new EventArgs()); - Invalidate(); - } - else - { - throw new ArgumentOutOfRangeException(nameof(Value), $"{nameof(Value)} must be between {nameof(Minimum)} and {nameof(Maximum)}."); - } - } - } - private long _minimum = 0L; - public long Minimum - { - get => _minimum; - set - { - if (value <= _maximum) - { - _minimum = value; - if (_value < _minimum) - { - _value = _minimum; - ValueChanged?.Invoke(this, new EventArgs()); - } - Invalidate(); - } - else - { - throw new ArgumentOutOfRangeException(nameof(Minimum), $"{nameof(Minimum)} cannot be higher than {nameof(Maximum)}."); - } - } - } - private long _maximum = 10L; - public long Maximum - { - get => _maximum; - set - { - if (value >= _minimum) - { - _maximum = value; - if (_value > _maximum) - { - _value = _maximum; - ValueChanged?.Invoke(this, new EventArgs()); - } - Invalidate(); - } - else - { - throw new ArgumentOutOfRangeException(nameof(Maximum), $"{nameof(Maximum)} cannot be lower than {nameof(Minimum)}."); - } - } - } - private long _smallChange = 1L; - public long SmallChange - { - get => _smallChange; - set - { - if (value >= 0) - { - _smallChange = value; - } - else - { - throw new ArgumentOutOfRangeException(nameof(SmallChange), $"{nameof(SmallChange)} must be greater than or equal to 0."); - } - } - } - private long _largeChange = 5L; - public long LargeChange - { - get => _largeChange; - set - { - if (value >= 0) - { - _largeChange = value; - } - else - { - throw new ArgumentOutOfRangeException(nameof(LargeChange), $"{nameof(LargeChange)} must be greater than or equal to 0."); - } - } - } - private bool _acceptKeys = true; - public bool AcceptKeys - { - get => _acceptKeys; - set - { - _acceptKeys = value; - SetStyle(ControlStyles.Selectable, value); - } - } - - public event EventHandler ValueChanged; - - private readonly Color _thumbOuterColor = Color.White; - private readonly Color _thumbInnerColor = Color.White; - private readonly Color _thumbPenColor = Color.FromArgb(125, 125, 125); - private readonly Color _barInnerColor = Theme.BackColorMouseOver; - private readonly Color _elapsedPenColorTop = Theme.ForeColor; - private readonly Color _elapsedPenColorBottom = Theme.ForeColor; - private readonly Color _barPenColorTop = Color.FromArgb(85, 90, 104); - private readonly Color _barPenColorBottom = Color.FromArgb(117, 124, 140); - private readonly Color _elapsedInnerColor = Theme.BorderColor; - private readonly Color _tickColor = Color.White; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - pen.Dispose(); - } - base.Dispose(disposing); - } - public ColorSlider() - { - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | - ControlStyles.ResizeRedraw | ControlStyles.Selectable | - ControlStyles.SupportsTransparentBackColor | ControlStyles.UserMouse | - ControlStyles.UserPaint, true); - Size = new Size(200, 48); - } - - protected override void OnPaint(PaintEventArgs e) - { - if (!Enabled) - { - Color[] c = DesaturateColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, - _barInnerColor, - _elapsedPenColorTop, _elapsedPenColorBottom, - _barPenColorTop, _barPenColorBottom, - _elapsedInnerColor); - Draw(e, - c[0], c[1], c[2], - c[3], - c[4], c[5], - c[6], c[7], - c[8]); - } - else - { - if (mouseInRegion) - { - Color[] c = LightenColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, - _barInnerColor, - _elapsedPenColorTop, _elapsedPenColorBottom, - _barPenColorTop, _barPenColorBottom, - _elapsedInnerColor); - Draw(e, - c[0], c[1], c[2], - c[3], - c[4], c[5], - c[6], c[7], - c[8]); - } - else - { - Draw(e, - _thumbOuterColor, _thumbInnerColor, _thumbPenColor, - _barInnerColor, - _elapsedPenColorTop, _elapsedPenColorBottom, - _barPenColorTop, _barPenColorBottom, - _elapsedInnerColor); - } - } - } - private readonly Pen pen = new Pen(Color.Transparent); - private void Draw(PaintEventArgs e, - Color thumbOuterColorPaint, Color thumbInnerColorPaint, Color thumbPenColorPaint, - Color barInnerColorPaint, - Color elapsedTopPenColorPaint, Color elapsedBottomPenColorPaint, - Color barTopPenColorPaint, Color barBottomPenColorPaint, - Color elapsedInnerColorPaint) - { - if (Focused) - { - ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Color.FromArgb(50, elapsedTopPenColorPaint), ButtonBorderStyle.Dashed); - } - - long a = _maximum - _minimum; - long x = a == 0 ? 0 : (_value - _minimum) * (ClientRectangle.Width - thumbSize) / a; - thumbRect = new Rectangle((int)x, ClientRectangle.Y + (ClientRectangle.Height / 2) - (thumbSize / 2), thumbSize, thumbSize); - Rectangle barRect = ClientRectangle; - barRect.Inflate(-1, -barRect.Height / 3); - Rectangle elapsedRect = barRect; - elapsedRect.Width = thumbRect.Left + (thumbSize / 2); - - pen.Color = barInnerColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + (barRect.Height / 2)); - pen.Color = elapsedInnerColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + (barRect.Height / 2)); - pen.Color = elapsedTopPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2)); - pen.Color = elapsedBottomPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y + 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2)); - pen.Color = barTopPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2)); - pen.Color = barBottomPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); - pen.Color = barTopPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X, barRect.Y + (barRect.Height / 2) + 1); - pen.Color = barBottomPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); - - e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; - Color newthumbOuterColorPaint = thumbOuterColorPaint, - newthumbInnerColorPaint = thumbInnerColorPaint; - if (busyMouse) - { - newthumbOuterColorPaint = Color.FromArgb(175, thumbOuterColorPaint); - newthumbInnerColorPaint = Color.FromArgb(175, thumbInnerColorPaint); - } - using (GraphicsPath thumbPath = CreateRoundRectPath(thumbRect, thumbSize)) - { - using (var lgbThumb = new LinearGradientBrush(thumbRect, newthumbOuterColorPaint, newthumbInnerColorPaint, LinearGradientMode.Vertical) { WrapMode = WrapMode.TileFlipXY }) - { - e.Graphics.FillPath(lgbThumb, thumbPath); - } - Color newThumbPenColor = thumbPenColorPaint; - if (busyMouse || mouseInThumbRegion) - { - newThumbPenColor = ControlPaint.Dark(newThumbPenColor); - } - pen.Color = newThumbPenColor; - e.Graphics.DrawPath(pen, thumbPath); - } - - const int numTicks = 1 + (10 * (5 + 1)); - int interval = 0; - int start = thumbRect.Width / 2; - int w = barRect.Width - thumbRect.Width; - int idx = 0; - pen.Color = _tickColor; - for (int i = 0; i <= 10; i++) - { - e.Graphics.DrawLine(pen, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height - 5); - if (i < 10) - { - for (int j = 0; j <= 5; j++) - { - idx++; - interval = idx * w / (numTicks - 1); - } - } - } - } - - private bool mouseInRegion = false; - private bool mouseInThumbRegion = false; - private bool busyMouse = false; - private void SetValueFromPoint(Point p) - { - int x = p.X; - int margin = thumbSize / 2; - x -= margin; - _value = (long)((x * ((_maximum - _minimum) / (ClientSize.Width - (2f * margin)))) + _minimum); - if (_value < _minimum) - { - _value = _minimum; - } - else if (_value > _maximum) - { - _value = _maximum; - } - ValueChanged?.Invoke(this, new EventArgs()); - } - protected override void OnEnabledChanged(EventArgs e) - { - base.OnEnabledChanged(e); - Invalidate(); - } - protected override void OnMouseEnter(EventArgs e) - { - base.OnMouseEnter(e); - mouseInRegion = true; - Invalidate(); - } - protected override void OnMouseLeave(EventArgs e) - { - base.OnMouseLeave(e); - mouseInRegion = false; - mouseInThumbRegion = false; - Invalidate(); - } - protected override void OnMouseDown(MouseEventArgs e) - { - base.OnMouseDown(e); - mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); - busyMouse = (MouseButtons & MouseButtons.Left) != MouseButtons.None; - if (busyMouse) - { - SetValueFromPoint(e.Location); - } - Invalidate(); - } - protected override void OnMouseMove(MouseEventArgs e) - { - base.OnMouseMove(e); - mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); - if (busyMouse) - { - SetValueFromPoint(e.Location); - } - Invalidate(); - } - protected override void OnMouseUp(MouseEventArgs e) - { - base.OnMouseUp(e); - mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); - bool old = busyMouse; - busyMouse = old && e.Button == MouseButtons.Left ? false : old; - Invalidate(); - } - protected override void OnGotFocus(EventArgs e) - { - base.OnGotFocus(e); - Invalidate(); - } - protected override void OnLostFocus(EventArgs e) - { - base.OnLostFocus(e); - Invalidate(); - } - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (_acceptKeys && !busyMouse) - { - switch (e.KeyCode) - { - case Keys.Down: - case Keys.Left: - { - long newVal = _value - _smallChange; - if (newVal < _minimum) - { - newVal = _minimum; - } - Value = newVal; - break; - } - case Keys.Up: - case Keys.Right: - { - long newVal = _value + _smallChange; - if (newVal > _maximum) - { - newVal = _maximum; - } - Value = newVal; - break; - } - case Keys.Home: - { - Value = _minimum; - break; - } - case Keys.End: - { - Value = _maximum; - break; - } - case Keys.PageDown: - { - long newVal = _value - _largeChange; - if (newVal < _minimum) - { - newVal = _minimum; - } - Value = newVal; - break; - } - case Keys.PageUp: - { - long newVal = _value + _largeChange; - if (newVal > _maximum) - { - newVal = _maximum; - } - Value = newVal; - break; - } - } - } - } - protected override bool ProcessDialogKey(Keys keyData) - { - return !_acceptKeys || keyData == Keys.Tab || ModifierKeys == Keys.Shift ? base.ProcessDialogKey(keyData) : false; - } - - private static GraphicsPath CreateRoundRectPath(Rectangle rect, int size) - { - var gp = new GraphicsPath(); - gp.AddLine(rect.Left + (size / 2), rect.Top, rect.Right - (size / 2), rect.Top); - gp.AddArc(rect.Right - size, rect.Top, size, size, 270, 90); - - gp.AddLine(rect.Right, rect.Top + (size / 2), rect.Right, rect.Bottom - (size / 2)); - gp.AddArc(rect.Right - size, rect.Bottom - size, size, size, 0, 90); - - gp.AddLine(rect.Right - (size / 2), rect.Bottom, rect.Left + (size / 2), rect.Bottom); - gp.AddArc(rect.Left, rect.Bottom - size, size, size, 90, 90); - - gp.AddLine(rect.Left, rect.Bottom - (size / 2), rect.Left, rect.Top + (size / 2)); - gp.AddArc(rect.Left, rect.Top, size, size, 180, 90); - return gp; - } - private static Color[] DesaturateColors(params Color[] colors) - { - var ret = new Color[colors.Length]; - for (int i = 0; i < colors.Length; i++) - { - int gray = (int)((colors[i].R * 0.3) + (colors[i].G * 0.6) + (colors[i].B * 0.1)); - ret[i] = Color.FromArgb((-0x010101 * (255 - gray)) - 1); - } - return ret; - } - private static Color[] LightenColors(params Color[] colors) - { - var ret = new Color[colors.Length]; - for (int i = 0; i < colors.Length; i++) - { - ret[i] = ControlPaint.Light(colors[i]); - } - return ret; - } - private static bool IsPointInRect(Point p, Rectangle rect) - { - return p.X > rect.Left & p.X < rect.Right & p.Y > rect.Top & p.Y < rect.Bottom; - } - } -} diff --git a/VG Music Studio.backup/UI/FlexibleMessageBox.cs b/VG Music Studio.backup/UI/FlexibleMessageBox.cs deleted file mode 100644 index 30d29ad1..00000000 --- a/VG Music Studio.backup/UI/FlexibleMessageBox.cs +++ /dev/null @@ -1,697 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.Drawing; -using System.Globalization; -using System.Linq; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - /* FlexibleMessageBox – A flexible replacement for the .NET MessageBox - * - * Author: Jörg Reichert (public@jreichert.de) - * Contributors: Thanks to: David Hall, Roink - * Version: 1.3 - * Published at: http://www.codeproject.com/Articles/601900/FlexibleMessageBox - * - ************************************************************************************************************ - * Features: - * - It can be simply used instead of MessageBox since all important static "Show"-Functions are supported - * - It is small, only one source file, which could be added easily to each solution - * - It can be resized and the content is correctly word-wrapped - * - It tries to auto-size the width to show the longest text row - * - It never exceeds the current desktop working area - * - It displays a vertical scrollbar when needed - * - It does support hyperlinks in text - * - * Because the interface is identical to MessageBox, you can add this single source file to your project - * and use the FlexibleMessageBox almost everywhere you use a standard MessageBox. - * The goal was NOT to produce as many features as possible but to provide a simple replacement to fit my - * own needs. Feel free to add additional features on your own, but please left my credits in this class. - * - ************************************************************************************************************ - * Usage examples: - * - * FlexibleMessageBox.Show("Just a text"); - * - * FlexibleMessageBox.Show("A text", - * "A caption"); - * - * FlexibleMessageBox.Show("Some text with a link: www.google.com", - * "Some caption", - * MessageBoxButtons.AbortRetryIgnore, - * MessageBoxIcon.Information, - * MessageBoxDefaultButton.Button2); - * - * var dialogResult = FlexibleMessageBox.Show("Do you know the answer to life the universe and everything?", - * "One short question", - * MessageBoxButtons.YesNo); - * - ************************************************************************************************************ - * THE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS", WITHOUT WARRANTY - * OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHOR BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THIS - * SOFTWARE. - * - ************************************************************************************************************ - * History: - * Version 1.3 - 19.Dezember 2014 - * - Added refactoring function GetButtonText() - * - Used CurrentUICulture instead of InstalledUICulture - * - Added more button localizations. Supported languages are now: ENGLISH, GERMAN, SPANISH, ITALIAN - * - Added standard MessageBox handling for "copy to clipboard" with + and + - * - Tab handling is now corrected (only tabbing over the visible buttons) - * - Added standard MessageBox handling for ALT-Keyboard shortcuts - * - SetDialogSizes: Refactored completely: Corrected sizing and added caption driven sizing - * - * Version 1.2 - 10.August 2013 - * - Do not ShowInTaskbar anymore (original MessageBox is also hidden in taskbar) - * - Added handling for Escape-Button - * - Adapted top right close button (red X) to behave like MessageBox (but hidden instead of deactivated) - * - * Version 1.1 - 14.June 2013 - * - Some Refactoring - * - Added internal form class - * - Added missing code comments, etc. - * - * Version 1.0 - 15.April 2013 - * - Initial Version - */ - - internal class FlexibleMessageBox - { - #region Public statics - - /// - /// Defines the maximum width for all FlexibleMessageBox instances in percent of the working area. - /// - /// Allowed values are 0.2 - 1.0 where: - /// 0.2 means: The FlexibleMessageBox can be at most half as wide as the working area. - /// 1.0 means: The FlexibleMessageBox can be as wide as the working area. - /// - /// Default is: 70% of the working area width. - /// - public static double MAX_WIDTH_FACTOR = 0.7; - - /// - /// Defines the maximum height for all FlexibleMessageBox instances in percent of the working area. - /// - /// Allowed values are 0.2 - 1.0 where: - /// 0.2 means: The FlexibleMessageBox can be at most half as high as the working area. - /// 1.0 means: The FlexibleMessageBox can be as high as the working area. - /// - /// Default is: 90% of the working area height. - /// - public static double MAX_HEIGHT_FACTOR = 0.9; - - /// - /// Defines the font for all FlexibleMessageBox instances. - /// - /// Default is: Theme.Font - /// - public static Font FONT = Theme.Font; - - #endregion - - #region Public show functions - - public static DialogResult Show(string text) - { - return FlexibleMessageBoxForm.Show(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text) - { - return FlexibleMessageBoxForm.Show(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption) - { - return FlexibleMessageBoxForm.Show(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(Exception ex, string caption) - { - return FlexibleMessageBoxForm.Show(null, string.Format("Error Details:{1}{1}{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace), caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text, string caption) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption, MessageBoxButtons buttons) - { - return FlexibleMessageBoxForm.Show(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) - { - return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) - { - return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, defaultButton); - } - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, defaultButton); - } - - #endregion - - #region Internal form class - - class FlexibleMessageBoxForm : ThemedForm - { - IContainer components = null; - - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - void InitializeComponent() - { - components = new Container(); - button1 = new ThemedButton(); - richTextBoxMessage = new ThemedRichTextBox(); - FlexibleMessageBoxFormBindingSource = new BindingSource(components); - panel1 = new ThemedPanel(); - pictureBoxForIcon = new PictureBox(); - button2 = new ThemedButton(); - button3 = new ThemedButton(); - ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).BeginInit(); - panel1.SuspendLayout(); - ((ISupportInitialize)(pictureBoxForIcon)).BeginInit(); - SuspendLayout(); - // - // button1 - // - button1.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; - button1.AutoSize = true; - button1.DialogResult = DialogResult.OK; - button1.Location = new Point(11, 67); - button1.MinimumSize = new Size(0, 24); - button1.Name = "button1"; - button1.Size = new Size(75, 24); - button1.TabIndex = 2; - button1.Text = "OK"; - button1.UseVisualStyleBackColor = true; - button1.Visible = false; - // - // richTextBoxMessage - // - richTextBoxMessage.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) - | AnchorStyles.Left) - | AnchorStyles.Right); - richTextBoxMessage.BorderStyle = BorderStyle.None; - richTextBoxMessage.DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "MessageText", true, DataSourceUpdateMode.OnPropertyChanged)); - richTextBoxMessage.Font = new Font(Theme.Font.FontFamily, 9); - richTextBoxMessage.Location = new Point(50, 26); - richTextBoxMessage.Margin = new Padding(0); - richTextBoxMessage.Name = "richTextBoxMessage"; - richTextBoxMessage.ReadOnly = true; - richTextBoxMessage.ScrollBars = RichTextBoxScrollBars.Vertical; - richTextBoxMessage.Size = new Size(200, 20); - richTextBoxMessage.TabIndex = 0; - richTextBoxMessage.TabStop = false; - richTextBoxMessage.Text = ""; - richTextBoxMessage.LinkClicked += new LinkClickedEventHandler(LinkClicked); - // - // panel1 - // - panel1.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) - | AnchorStyles.Left) - | AnchorStyles.Right); - panel1.Controls.Add(pictureBoxForIcon); - panel1.Controls.Add(richTextBoxMessage); - panel1.Location = new Point(-3, -4); - panel1.Name = "panel1"; - panel1.Size = new Size(268, 59); - panel1.TabIndex = 1; - // - // pictureBoxForIcon - // - pictureBoxForIcon.BackColor = Color.Transparent; - pictureBoxForIcon.Location = new Point(15, 19); - pictureBoxForIcon.Name = "pictureBoxForIcon"; - pictureBoxForIcon.Size = new Size(32, 32); - pictureBoxForIcon.TabIndex = 8; - pictureBoxForIcon.TabStop = false; - // - // button2 - // - button2.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); - button2.DialogResult = DialogResult.OK; - button2.Location = new Point(92, 67); - button2.MinimumSize = new Size(0, 24); - button2.Name = "button2"; - button2.Size = new Size(75, 24); - button2.TabIndex = 3; - button2.Text = "OK"; - button2.UseVisualStyleBackColor = true; - button2.Visible = false; - // - // button3 - // - button3.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); - button3.AutoSize = true; - button3.DialogResult = DialogResult.OK; - button3.Location = new Point(173, 67); - button3.MinimumSize = new Size(0, 24); - button3.Name = "button3"; - button3.Size = new Size(75, 24); - button3.TabIndex = 0; - button3.Text = "OK"; - button3.UseVisualStyleBackColor = true; - button3.Visible = false; - // - // FlexibleMessageBoxForm - // - AutoScaleDimensions = new SizeF(6F, 13F); - AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(260, 102); - Controls.Add(button3); - Controls.Add(button2); - Controls.Add(panel1); - Controls.Add(button1); - DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "CaptionText", true)); - Icon = Properties.Resources.Icon; - MaximizeBox = false; - MinimizeBox = false; - MinimumSize = new Size(276, 140); - Name = "FlexibleMessageBoxForm"; - SizeGripStyle = SizeGripStyle.Show; - StartPosition = FormStartPosition.CenterParent; - Text = ""; - Shown += new EventHandler(FlexibleMessageBoxForm_Shown); - ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).EndInit(); - panel1.ResumeLayout(false); - ((ISupportInitialize)(pictureBoxForIcon)).EndInit(); - ResumeLayout(false); - PerformLayout(); - } - - ThemedButton button1, button2, button3; - private BindingSource FlexibleMessageBoxFormBindingSource; - ThemedRichTextBox richTextBoxMessage; - ThemedPanel panel1; - private PictureBox pictureBoxForIcon; - - #region Private constants - - //These separators are used for the "copy to clipboard" standard operation, triggered by Ctrl + C (behavior and clipboard format is like in a standard MessageBox) - static readonly String STANDARD_MESSAGEBOX_SEPARATOR_LINES = "---------------------------\n"; - static readonly String STANDARD_MESSAGEBOX_SEPARATOR_SPACES = " "; - - //These are the possible buttons (in a standard MessageBox) - private enum ButtonID { OK = 0, CANCEL, YES, NO, ABORT, RETRY, IGNORE }; - - //These are the buttons texts for different languages. - //If you want to add a new language, add it here and in the GetButtonText-Function - private enum TwoLetterISOLanguageID { en, de, es, it }; - static readonly String[] BUTTON_TEXTS_ENGLISH_EN = { "OK", "Cancel", "&Yes", "&No", "&Abort", "&Retry", "&Ignore" }; //Note: This is also the fallback language - static readonly String[] BUTTON_TEXTS_GERMAN_DE = { "OK", "Abbrechen", "&Ja", "&Nein", "&Abbrechen", "&Wiederholen", "&Ignorieren" }; - static readonly String[] BUTTON_TEXTS_SPANISH_ES = { "Aceptar", "Cancelar", "&Sí", "&No", "&Abortar", "&Reintentar", "&Ignorar" }; - static readonly String[] BUTTON_TEXTS_ITALIAN_IT = { "OK", "Annulla", "&Sì", "&No", "&Interrompi", "&Riprova", "&Ignora" }; - - #endregion - - #region Private members - - MessageBoxDefaultButton defaultButton; - int visibleButtonsCount; - readonly TwoLetterISOLanguageID languageID = TwoLetterISOLanguageID.en; - - #endregion - - #region Private constructor - - private FlexibleMessageBoxForm() - { - InitializeComponent(); - - //Try to evaluate the language. If this fails, the fallback language English will be used - Enum.TryParse(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, out languageID); - - KeyPreview = true; - KeyUp += FlexibleMessageBoxForm_KeyUp; - } - - #endregion - - #region Private helper functions - - static string[] GetStringRows(string message) - { - if (string.IsNullOrEmpty(message)) - { - return null; - } - - var messageRows = message.Split(new char[] { '\n' }, StringSplitOptions.None); - return messageRows; - } - - string GetButtonText(ButtonID buttonID) - { - var buttonTextArrayIndex = Convert.ToInt32(buttonID); - - switch (languageID) - { - case TwoLetterISOLanguageID.de: return BUTTON_TEXTS_GERMAN_DE[buttonTextArrayIndex]; - case TwoLetterISOLanguageID.es: return BUTTON_TEXTS_SPANISH_ES[buttonTextArrayIndex]; - case TwoLetterISOLanguageID.it: return BUTTON_TEXTS_ITALIAN_IT[buttonTextArrayIndex]; - - default: return BUTTON_TEXTS_ENGLISH_EN[buttonTextArrayIndex]; - } - } - - static double GetCorrectedWorkingAreaFactor(double workingAreaFactor) - { - const double MIN_FACTOR = 0.2; - const double MAX_FACTOR = 1.0; - - if (workingAreaFactor < MIN_FACTOR) - { - return MIN_FACTOR; - } - - if (workingAreaFactor > MAX_FACTOR) - { - return MAX_FACTOR; - } - - return workingAreaFactor; - } - - static void SetDialogStartPosition(FlexibleMessageBoxForm flexibleMessageBoxForm, IWin32Window owner) - { - //If no owner given: Center on current screen - if (owner == null) - { - var screen = Screen.FromPoint(Cursor.Position); - flexibleMessageBoxForm.StartPosition = FormStartPosition.Manual; - flexibleMessageBoxForm.Left = screen.Bounds.Left + screen.Bounds.Width / 2 - flexibleMessageBoxForm.Width / 2; - flexibleMessageBoxForm.Top = screen.Bounds.Top + screen.Bounds.Height / 2 - flexibleMessageBoxForm.Height / 2; - } - } - - static void SetDialogSizes(FlexibleMessageBoxForm flexibleMessageBoxForm, string text, string caption) - { - //First set the bounds for the maximum dialog size - flexibleMessageBoxForm.MaximumSize = new Size(Convert.ToInt32(SystemInformation.WorkingArea.Width * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_WIDTH_FACTOR)), - Convert.ToInt32(SystemInformation.WorkingArea.Height * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_HEIGHT_FACTOR))); - - //Get rows. Exit if there are no rows to render... - var stringRows = GetStringRows(text); - if (stringRows == null) - { - return; - } - - //Calculate whole text height - var textHeight = TextRenderer.MeasureText(text, FONT).Height; - - //Calculate width for longest text line - const int SCROLLBAR_WIDTH_OFFSET = 15; - var longestTextRowWidth = stringRows.Max(textForRow => TextRenderer.MeasureText(textForRow, FONT).Width); - var captionWidth = TextRenderer.MeasureText(caption, SystemFonts.CaptionFont).Width; - var textWidth = Math.Max(longestTextRowWidth + SCROLLBAR_WIDTH_OFFSET, captionWidth); - - //Calculate margins - var marginWidth = flexibleMessageBoxForm.Width - flexibleMessageBoxForm.richTextBoxMessage.Width; - var marginHeight = flexibleMessageBoxForm.Height - flexibleMessageBoxForm.richTextBoxMessage.Height; - - //Set calculated dialog size (if the calculated values exceed the maximums, they were cut by windows forms automatically) - flexibleMessageBoxForm.Size = new Size(textWidth + marginWidth, - textHeight + marginHeight); - } - - static void SetDialogIcon(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxIcon icon) - { - switch (icon) - { - case MessageBoxIcon.Information: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Information.ToBitmap(); - break; - case MessageBoxIcon.Warning: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Warning.ToBitmap(); - break; - case MessageBoxIcon.Error: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Error.ToBitmap(); - break; - case MessageBoxIcon.Question: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Question.ToBitmap(); - break; - default: - //When no icon is used: Correct placement and width of rich text box. - flexibleMessageBoxForm.pictureBoxForIcon.Visible = false; - flexibleMessageBoxForm.richTextBoxMessage.Left -= flexibleMessageBoxForm.pictureBoxForIcon.Width; - flexibleMessageBoxForm.richTextBoxMessage.Width += flexibleMessageBoxForm.pictureBoxForIcon.Width; - break; - } - } - - static void SetDialogButtons(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxButtons buttons, MessageBoxDefaultButton defaultButton) - { - //Set the buttons visibilities and texts - switch (buttons) - { - case MessageBoxButtons.AbortRetryIgnore: - flexibleMessageBoxForm.visibleButtonsCount = 3; - - flexibleMessageBoxForm.button1.Visible = true; - flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.ABORT); - flexibleMessageBoxForm.button1.DialogResult = DialogResult.Abort; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.IGNORE); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Ignore; - - flexibleMessageBoxForm.ControlBox = false; - break; - - case MessageBoxButtons.OKCancel: - flexibleMessageBoxForm.visibleButtonsCount = 2; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.OK; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - - case MessageBoxButtons.RetryCancel: - flexibleMessageBoxForm.visibleButtonsCount = 2; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - - case MessageBoxButtons.YesNo: - flexibleMessageBoxForm.visibleButtonsCount = 2; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.Yes; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.No; - - flexibleMessageBoxForm.ControlBox = false; - break; - - case MessageBoxButtons.YesNoCancel: - flexibleMessageBoxForm.visibleButtonsCount = 3; - - flexibleMessageBoxForm.button1.Visible = true; - flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); - flexibleMessageBoxForm.button1.DialogResult = DialogResult.Yes; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.No; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - - case MessageBoxButtons.OK: - default: - flexibleMessageBoxForm.visibleButtonsCount = 1; - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.OK; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - } - - //Set default button (used in FlexibleMessageBoxForm_Shown) - flexibleMessageBoxForm.defaultButton = defaultButton; - } - - #endregion - - #region Private event handlers - - void FlexibleMessageBoxForm_Shown(object sender, EventArgs e) - { - int buttonIndexToFocus = 1; - Button buttonToFocus; - - //Set the default button... - switch (defaultButton) - { - case MessageBoxDefaultButton.Button1: - default: - buttonIndexToFocus = 1; - break; - case MessageBoxDefaultButton.Button2: - buttonIndexToFocus = 2; - break; - case MessageBoxDefaultButton.Button3: - buttonIndexToFocus = 3; - break; - } - - if (buttonIndexToFocus > visibleButtonsCount) - { - buttonIndexToFocus = visibleButtonsCount; - } - - if (buttonIndexToFocus == 3) - { - buttonToFocus = button3; - } - else if (buttonIndexToFocus == 2) - { - buttonToFocus = button2; - } - else - { - buttonToFocus = button1; - } - - buttonToFocus.Focus(); - } - - void LinkClicked(object sender, LinkClickedEventArgs e) - { - try - { - Cursor.Current = Cursors.WaitCursor; - Process.Start(e.LinkText); - } - catch (Exception) - { - //Let the caller of FlexibleMessageBoxForm decide what to do with this exception... - throw; - } - finally - { - Cursor.Current = Cursors.Default; - } - } - - void FlexibleMessageBoxForm_KeyUp(object sender, KeyEventArgs e) - { - //Handle standard key strikes for clipboard copy: "Ctrl + C" and "Ctrl + Insert" - if (e.Control && (e.KeyCode == Keys.C || e.KeyCode == Keys.Insert)) - { - var buttonsTextLine = (button1.Visible ? button1.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) - + (button2.Visible ? button2.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) - + (button3.Visible ? button3.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty); - - //Build same clipboard text like the standard .Net MessageBox - var textForClipboard = STANDARD_MESSAGEBOX_SEPARATOR_LINES - + Text + Environment.NewLine - + STANDARD_MESSAGEBOX_SEPARATOR_LINES - + richTextBoxMessage.Text + Environment.NewLine - + STANDARD_MESSAGEBOX_SEPARATOR_LINES - + buttonsTextLine.Replace("&", string.Empty) + Environment.NewLine - + STANDARD_MESSAGEBOX_SEPARATOR_LINES; - - //Set text in clipboard - Clipboard.SetText(textForClipboard); - } - } - - #endregion - - #region Properties (only used for binding) - - public string CaptionText { get; set; } - public string MessageText { get; set; } - - #endregion - - #region Public show function - - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) - { - //Create a new instance of the FlexibleMessageBox form - var flexibleMessageBoxForm = new FlexibleMessageBoxForm - { - ShowInTaskbar = false, - - //Bind the caption and the message text - CaptionText = caption, - MessageText = text - }; - flexibleMessageBoxForm.FlexibleMessageBoxFormBindingSource.DataSource = flexibleMessageBoxForm; - - //Set the buttons visibilities and texts. Also set a default button. - SetDialogButtons(flexibleMessageBoxForm, buttons, defaultButton); - - //Set the dialogs icon. When no icon is used: Correct placement and width of rich text box. - SetDialogIcon(flexibleMessageBoxForm, icon); - - //Set the font for all controls - flexibleMessageBoxForm.Font = FONT; - flexibleMessageBoxForm.richTextBoxMessage.Font = FONT; - - //Calculate the dialogs start size (Try to auto-size width to show longest text row). Also set the maximum dialog size. - SetDialogSizes(flexibleMessageBoxForm, text, caption); - - //Set the dialogs start position when given. Otherwise center the dialog on the current screen. - SetDialogStartPosition(flexibleMessageBoxForm, owner); - - //Show the dialog - return flexibleMessageBoxForm.ShowDialog(owner); - } - - #endregion - } //class FlexibleMessageBoxForm - - #endregion - } -} diff --git a/VG Music Studio.backup/UI/ImageComboBox.cs b/VG Music Studio.backup/UI/ImageComboBox.cs deleted file mode 100644 index c928af92..00000000 --- a/VG Music Studio.backup/UI/ImageComboBox.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Drawing; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - internal class ImageComboBox : ComboBox - { - private const int _imgSize = 15; - private bool _open = false; - - public ImageComboBox() - { - DrawMode = DrawMode.OwnerDrawFixed; - DropDownStyle = ComboBoxStyle.DropDown; - } - - protected override void OnDrawItem(DrawItemEventArgs e) - { - e.DrawBackground(); - e.DrawFocusRectangle(); - - if (e.Index >= 0) - { - ImageComboBoxItem item = Items[e.Index] as ImageComboBoxItem ?? throw new InvalidCastException($"Item was not of type \"{nameof(ImageComboBoxItem)}\""); - int indent = _open ? item.IndentLevel : 0; - e.Graphics.DrawImage(item.Image, e.Bounds.Left + (indent * _imgSize), e.Bounds.Top, _imgSize, _imgSize); - e.Graphics.DrawString(item.ToString(), e.Font, new SolidBrush(e.ForeColor), e.Bounds.Left + (indent * _imgSize) + _imgSize, e.Bounds.Top); - } - - base.OnDrawItem(e); - } - protected override void OnDropDown(EventArgs e) - { - _open = true; - base.OnDropDown(e); - } - protected override void OnDropDownClosed(EventArgs e) - { - _open = false; - base.OnDropDownClosed(e); - } - } - internal class ImageComboBoxItem - { - public object Item { get; } - public Image Image { get; } - public int IndentLevel { get; } - - public ImageComboBoxItem(object item, Image image, int indentLevel) - { - Item = item; - Image = image; - IndentLevel = indentLevel; - } - - public override string ToString() - { - return Item.ToString(); - } - } -} diff --git a/VG Music Studio.backup/UI/MainForm.cs b/VG Music Studio.backup/UI/MainForm.cs deleted file mode 100644 index 761394fa..00000000 --- a/VG Music Studio.backup/UI/MainForm.cs +++ /dev/null @@ -1,836 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using Microsoft.WindowsAPICodePack.Dialogs; -using Microsoft.WindowsAPICodePack.Taskbar; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class MainForm : ThemedForm - { - private const int _intendedWidth = 675; - private const int _intendedHeight = 675 + 1 + 125 + 24; - - public static MainForm Instance { get; } = new MainForm(); - - public readonly bool[] PianoTracks = new bool[SongInfoControl.SongInfo.MaxTracks]; - - private bool _playlistPlaying; - private Config.Playlist _curPlaylist; - private long _curSong = -1; - private readonly List _playedSongs = new List(); - private readonly List _remainingSongs = new List(); - - private TrackViewer _trackViewer; - - #region Controls - - private readonly MenuStrip _mainMenu; - private readonly ToolStripMenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, - _dataItem, _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem, - _playlistItem, _endPlaylistItem; - private readonly Timer _timer; - private readonly ThemedNumeric _songNumerical; - private readonly ThemedButton _playButton, _pauseButton, _stopButton; - private readonly SplitContainer _splitContainer; - private readonly PianoControl _piano; - private readonly ColorSlider _volumeBar, _positionBar; - private readonly SongInfoControl _songInfo; - private readonly ImageComboBox _songsComboBox; - private readonly ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; - - #endregion - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _timer.Dispose(); - } - base.Dispose(disposing); - } - private MainForm() - { - for (int i = 0; i < PianoTracks.Length; i++) - { - PianoTracks[i] = true; - } - - // File Menu - _openDSEItem = new ToolStripMenuItem { Text = Strings.MenuOpenDSE }; - _openDSEItem.Click += OpenDSE; - _openAlphaDreamItem = new ToolStripMenuItem { Text = Strings.MenuOpenAlphaDream }; - _openAlphaDreamItem.Click += OpenAlphaDream; - _openMP2KItem = new ToolStripMenuItem { Text = Strings.MenuOpenMP2K }; - _openMP2KItem.Click += OpenMP2K; - _openSDATItem = new ToolStripMenuItem { Text = Strings.MenuOpenSDAT }; - _openSDATItem.Click += OpenSDAT; - _fileItem = new ToolStripMenuItem { Text = Strings.MenuFile }; - _fileItem.DropDownItems.AddRange(new ToolStripItem[] { _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem }); - - // Data Menu - _trackViewerItem = new ToolStripMenuItem { ShortcutKeys = Keys.Control | Keys.T, Text = Strings.TrackViewerTitle }; - _trackViewerItem.Click += OpenTrackViewer; - _exportDLSItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveDLS }; - _exportDLSItem.Click += ExportDLS; - _exportMIDIItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveMIDI }; - _exportMIDIItem.Click += ExportMIDI; - _exportSF2Item = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveSF2 }; - _exportSF2Item.Click += ExportSF2; - _exportWAVItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveWAV }; - _exportWAVItem.Click += ExportWAV; - _dataItem = new ToolStripMenuItem { Text = Strings.MenuData }; - _dataItem.DropDownItems.AddRange(new ToolStripItem[] { _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem }); - - // Playlist Menu - _endPlaylistItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuEndPlaylist }; - _endPlaylistItem.Click += EndCurrentPlaylist; - _playlistItem = new ToolStripMenuItem { Text = Strings.MenuPlaylist }; - _playlistItem.DropDownItems.AddRange(new ToolStripItem[] { _endPlaylistItem }); - - // Main Menu - _mainMenu = new MenuStrip { Size = new Size(_intendedWidth, 24) }; - _mainMenu.Items.AddRange(new ToolStripItem[] { _fileItem, _dataItem, _playlistItem }); - - // Buttons - _playButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumSpringGreen, Text = Strings.PlayerPlay }; - _playButton.Click += (o, e) => Play(); - _pauseButton = new ThemedButton { Enabled = false, ForeColor = Color.DeepSkyBlue, Text = Strings.PlayerPause }; - _pauseButton.Click += (o, e) => Pause(); - _stopButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumVioletRed, Text = Strings.PlayerStop }; - _stopButton.Click += (o, e) => Stop(); - - // Numerical - _songNumerical = new ThemedNumeric { Enabled = false, Minimum = 0, Visible = false }; - _songNumerical.ValueChanged += SongNumerical_ValueChanged; - - // Timer - _timer = new Timer(); - _timer.Tick += UpdateUI; - - // Piano - _piano = new PianoControl(); - - // Volume bar - _volumeBar = new ColorSlider { Enabled = false, LargeChange = 20, Maximum = 100, SmallChange = 5 }; - _volumeBar.ValueChanged += VolumeBar_ValueChanged; - - // Position bar - _positionBar = new ColorSlider { AcceptKeys = false, Enabled = false, Maximum = 0 }; - _positionBar.MouseUp += PositionBar_MouseUp; - _positionBar.MouseDown += PositionBar_MouseDown; - - // Playlist box - _songsComboBox = new ImageComboBox { Enabled = false }; - _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; - - // Track info - _songInfo = new SongInfoControl { Dock = DockStyle.Fill }; - - // Split container - _splitContainer = new SplitContainer { BackColor = Theme.TitleBar, Dock = DockStyle.Fill, IsSplitterFixed = true, Orientation = Orientation.Horizontal, SplitterWidth = 1 }; - _splitContainer.Panel1.Controls.AddRange(new Control[] { _playButton, _pauseButton, _stopButton, _songNumerical, _songsComboBox, _piano, _volumeBar, _positionBar }); - _splitContainer.Panel2.Controls.Add(_songInfo); - - // MainForm - ClientSize = new Size(_intendedWidth, _intendedHeight); - Controls.AddRange(new Control[] { _splitContainer, _mainMenu }); - MainMenuStrip = _mainMenu; - MinimumSize = new Size(_intendedWidth + (Width - _intendedWidth), _intendedHeight + (Height - _intendedHeight)); // Borders - Resize += OnResize; - Text = Utils.ProgramName; - - // Taskbar Buttons - if (TaskbarManager.IsPlatformSupported) - { - _prevTButton = new ThumbnailToolBarButton(Resources.IconPrevious, Strings.PlayerPreviousSong); - _prevTButton.Click += PlayPreviousSong; - _toggleTButton = new ThumbnailToolBarButton(Resources.IconPlay, Strings.PlayerPlay); - _toggleTButton.Click += TogglePlayback; - _nextTButton = new ThumbnailToolBarButton(Resources.IconNext, Strings.PlayerNextSong); - _nextTButton.Click += PlayNextSong; - _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = false; - TaskbarManager.Instance.ThumbnailToolBars.AddButtons(Handle, _prevTButton, _toggleTButton, _nextTButton); - } - - OnResize(null, null); - } - - private void VolumeBar_ValueChanged(object sender, EventArgs e) - { - Engine.Instance.Mixer.SetVolume(_volumeBar.Value / (float)_volumeBar.Maximum); - } - public void SetVolumeBarValue(float volume) - { - _volumeBar.ValueChanged -= VolumeBar_ValueChanged; - _volumeBar.Value = (int)(volume * _volumeBar.Maximum); - _volumeBar.ValueChanged += VolumeBar_ValueChanged; - } - private bool _positionBarFree = true; - private void PositionBar_MouseUp(object sender, MouseEventArgs e) - { - if (e.Button == MouseButtons.Left) - { - Engine.Instance.Player.SetCurrentPosition(_positionBar.Value); - _positionBarFree = true; - LetUIKnowPlayerIsPlaying(); - } - } - private void PositionBar_MouseDown(object sender, MouseEventArgs e) - { - if (e.Button == MouseButtons.Left) - { - _positionBarFree = false; - } - } - - private bool _autoplay = false; - private void SongNumerical_ValueChanged(object sender, EventArgs e) - { - _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; - - long index = (long)_songNumerical.Value; - Stop(); - Text = Utils.ProgramName; - _songsComboBox.SelectedIndex = 0; - _songInfo.DeleteData(); - bool success; - try - { - Engine.Instance.Player.LoadSong(index); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, string.Format(Strings.ErrorLoadSong, Engine.Instance.Config.GetSongName(index))); - success = false; - } - - _trackViewer?.UpdateTracks(); - if (success) - { - Config config = Engine.Instance.Config; - List songs = config.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 - Config.Song song = songs.SingleOrDefault(s => s.Index == index); - if (song != null) - { - Text = $"{Utils.ProgramName} - {song.Name}"; - _songsComboBox.SelectedIndex = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox - } - _positionBar.Maximum = Engine.Instance.Player.MaxTicks; - _positionBar.LargeChange = _positionBar.Maximum / 10; - _positionBar.SmallChange = _positionBar.LargeChange / 4; - _songInfo.SetNumTracks(Engine.Instance.Player.Events.Length); - if (_autoplay) - { - Play(); - } - } - else - { - _songInfo.SetNumTracks(0); - } - int numTracks = (Engine.Instance.Player.Events?.Length).GetValueOrDefault(); - _positionBar.Enabled = _exportWAVItem.Enabled = success && numTracks > 0; - _exportMIDIItem.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_MP2K && numTracks > 0; - _exportDLSItem.Enabled = _exportSF2Item.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_AlphaDream; - - _autoplay = true; - _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; - } - private void SongsComboBox_SelectedIndexChanged(object sender, EventArgs e) - { - var item = (ImageComboBoxItem)_songsComboBox.SelectedItem; - if (item.Item is Config.Song song) - { - SetAndLoadSong(song.Index); - } - else if (item.Item is Config.Playlist playlist) - { - if (playlist.Songs.Count > 0 - && FlexibleMessageBox.Show(string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist), Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) - { - ResetPlaylistStuff(false); - _curPlaylist = playlist; - Engine.Instance.Player.ShouldFadeOut = _playlistPlaying = true; - Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; - _endPlaylistItem.Enabled = true; - SetAndLoadNextPlaylistSong(); - } - } - } - private void SetAndLoadSong(long index) - { - _curSong = index; - if (_songNumerical.Value == index) - { - SongNumerical_ValueChanged(null, null); - } - else - { - _songNumerical.Value = index; - } - } - private void SetAndLoadNextPlaylistSong() - { - if (_remainingSongs.Count == 0) - { - _remainingSongs.AddRange(_curPlaylist.Songs.Select(s => s.Index)); - if (GlobalConfig.Instance.PlaylistMode == PlaylistMode.Random) - { - _remainingSongs.Shuffle(); - } - } - long nextSong = _remainingSongs[0]; - _remainingSongs.RemoveAt(0); - SetAndLoadSong(nextSong); - } - private void ResetPlaylistStuff(bool enableds) - { - if (Engine.Instance != null) - { - Engine.Instance.Player.ShouldFadeOut = false; - } - _playlistPlaying = false; - _curPlaylist = null; - _curSong = -1; - _remainingSongs.Clear(); - _playedSongs.Clear(); - _endPlaylistItem.Enabled = false; - _songNumerical.Enabled = _songsComboBox.Enabled = enableds; - } - private void EndCurrentPlaylist(object sender, EventArgs e) - { - if (FlexibleMessageBox.Show(Strings.EndPlaylistBody, Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) - { - ResetPlaylistStuff(true); - } - } - - private void OpenDSE(object sender, EventArgs e) - { - var d = new CommonOpenFileDialog - { - Title = Strings.MenuOpenDSE, - IsFolderPicker = true - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.NDS_DSE, d.FileName); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenDSE); - success = false; - } - if (success) - { - var config = (Core.NDS.DSE.Config)Engine.Instance.Config; - FinishLoading(config.BGMFiles.Length); - _songNumerical.Visible = false; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = false; - _exportSF2Item.Visible = false; - } - } - } - private void OpenAlphaDream(object sender, EventArgs e) - { - var d = new CommonOpenFileDialog - { - Title = Strings.MenuOpenAlphaDream, - Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.GBA_AlphaDream, File.ReadAllBytes(d.FileName)); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenAlphaDream); - success = false; - } - if (success) - { - var config = (Core.GBA.AlphaDream.Config)Engine.Instance.Config; - FinishLoading(config.SongTableSizes[0]); - _songNumerical.Visible = true; - _exportDLSItem.Visible = true; - _exportMIDIItem.Visible = false; - _exportSF2Item.Visible = true; - } - } - } - private void OpenMP2K(object sender, EventArgs e) - { - var d = new CommonOpenFileDialog - { - Title = Strings.MenuOpenMP2K, - Filters = { new CommonFileDialogFilter(Strings.FilterOpenGBA, ".gba") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.GBA_MP2K, File.ReadAllBytes(d.FileName)); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenMP2K); - success = false; - } - if (success) - { - var config = (Core.GBA.MP2K.Config)Engine.Instance.Config; - FinishLoading(config.SongTableSizes[0]); - _songNumerical.Visible = true; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = true; - _exportSF2Item.Visible = false; - } - } - } - private void OpenSDAT(object sender, EventArgs e) - { - var d = new CommonOpenFileDialog - { - Title = Strings.MenuOpenSDAT, - Filters = { new CommonFileDialogFilter(Strings.FilterOpenSDAT, ".sdat") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.NDS_SDAT, new Core.NDS.SDAT.SDAT(File.ReadAllBytes(d.FileName))); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenSDAT); - success = false; - } - if (success) - { - var config = (Core.NDS.SDAT.Config)Engine.Instance.Config; - FinishLoading(config.SDAT.INFOBlock.SequenceInfos.NumEntries); - _songNumerical.Visible = true; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = false; - _exportSF2Item.Visible = false; - } - } - } - - private void ExportDLS(object sender, EventArgs e) - { - var d = new CommonSaveFileDialog - { - DefaultFileName = Engine.Instance.Config.GetGameName(), - DefaultExtension = ".dls", - EnsureValidNames = true, - Title = Strings.MenuSaveDLS, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveDLS, ".dls") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - try - { - Core.GBA.AlphaDream.SoundFontSaver_DLS.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveDLS, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveDLS); - } - } - } - private void ExportMIDI(object sender, EventArgs e) - { - var d = new CommonSaveFileDialog - { - DefaultFileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), - DefaultExtension = ".mid", - EnsureValidNames = true, - Title = Strings.MenuSaveMIDI, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveMIDI, ".mid;.midi") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - var p = (Core.GBA.MP2K.Player)Engine.Instance.Player; - var args = new Core.GBA.MP2K.Player.MIDISaveArgs - { - SaveCommandsBeforeTranspose = true, - ReverseVolume = false, - TimeSignatures = new List<(int AbsoluteTick, (byte Numerator, byte Denominator))> - { - (0, (4, 4)) - } - }; - try - { - p.SaveAsMIDI(d.FileName, args); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveMIDI, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveMIDI); - } - } - } - private void ExportSF2(object sender, EventArgs e) - { - var d = new CommonSaveFileDialog - { - DefaultFileName = Engine.Instance.Config.GetGameName(), - DefaultExtension = ".sf2", - EnsureValidNames = true, - Title = Strings.MenuSaveSF2, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveSF2, ".sf2") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - try - { - Core.GBA.AlphaDream.SoundFontSaver_SF2.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveSF2, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveSF2); - } - } - } - private void ExportWAV(object sender, EventArgs e) - { - var d = new CommonSaveFileDialog - { - DefaultFileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), - DefaultExtension = ".wav", - EnsureValidNames = true, - Title = Strings.MenuSaveWAV, - Filters = { new CommonFileDialogFilter(Strings.FilterSaveWAV, ".wav") } - }; - if (d.ShowDialog() == CommonFileDialogResult.Ok) - { - Stop(); - bool oldFade = Engine.Instance.Player.ShouldFadeOut; - long oldLoops = Engine.Instance.Player.NumLoops; - Engine.Instance.Player.ShouldFadeOut = true; - Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; - try - { - Engine.Instance.Player.Record(d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveWAV); - } - Engine.Instance.Player.ShouldFadeOut = oldFade; - Engine.Instance.Player.NumLoops = oldLoops; - _stopUI = false; - } - } - - public void LetUIKnowPlayerIsPlaying() - { - if (!_timer.Enabled) - { - _pauseButton.Enabled = _stopButton.Enabled = true; - _pauseButton.Text = Strings.PlayerPause; - _timer.Interval = (int)(1_000d / GlobalConfig.Instance.RefreshRate); - _timer.Start(); - UpdateTaskbarState(); - UpdateTaskbarButtons(); - } - } - private void Play() - { - Engine.Instance.Player.Play(); - LetUIKnowPlayerIsPlaying(); - } - private void Pause() - { - Engine.Instance.Player.Pause(); - if (Engine.Instance.Player.State == PlayerState.Paused) - { - _pauseButton.Text = Strings.PlayerUnpause; - _timer.Stop(); - } - else - { - _pauseButton.Text = Strings.PlayerPause; - _timer.Start(); - } - UpdateTaskbarState(); - UpdateTaskbarButtons(); - } - private void Stop() - { - Engine.Instance.Player.Stop(); - _pauseButton.Enabled = _stopButton.Enabled = false; - _pauseButton.Text = Strings.PlayerPause; - _timer.Stop(); - _songInfo.DeleteData(); - _piano.UpdateKeys(_songInfo.Info, PianoTracks); - UpdatePositionIndicators(0L); - UpdateTaskbarState(); - UpdateTaskbarButtons(); - } - private void TogglePlayback(object sender, EventArgs e) - { - if (Engine.Instance.Player.State == PlayerState.Stopped) - { - Play(); - } - else if (Engine.Instance.Player.State == PlayerState.Paused || Engine.Instance.Player.State == PlayerState.Playing) - { - Pause(); - } - } - private void PlayPreviousSong(object sender, EventArgs e) - { - long prevSong; - if (_playlistPlaying) - { - int index = _playedSongs.Count - 1; - prevSong = _playedSongs[index]; - _playedSongs.RemoveAt(index); - _remainingSongs.Insert(0, _curSong); - } - else - { - prevSong = (long)_songNumerical.Value - 1; - } - SetAndLoadSong(prevSong); - } - private void PlayNextSong(object sender, EventArgs e) - { - if (_playlistPlaying) - { - _playedSongs.Add(_curSong); - SetAndLoadNextPlaylistSong(); - } - else - { - SetAndLoadSong((long)_songNumerical.Value + 1); - } - } - - private void FinishLoading(long numSongs) - { - Engine.Instance.Player.SongEnded += SongEnded; - foreach (Config.Playlist playlist in Engine.Instance.Config.Playlists) - { - _songsComboBox.Items.Add(new ImageComboBoxItem(playlist, Resources.IconPlaylist, 0)); - _songsComboBox.Items.AddRange(playlist.Songs.Select(s => new ImageComboBoxItem(s, Resources.IconSong, 1)).ToArray()); - } - _songNumerical.Maximum = numSongs - 1; -#if DEBUG - //VGMSDebug.EventScan(Engine.Instance.Config.Playlists[0].Songs, numericalVisible); -#endif - _autoplay = false; - SetAndLoadSong(Engine.Instance.Config.Playlists[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[0].Songs[0].Index); - _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = true; - UpdateTaskbarButtons(); - } - private void DisposeEngine() - { - if (Engine.Instance != null) - { - Stop(); - Engine.Instance.Dispose(); - } - _trackViewer?.UpdateTracks(); - _prevTButton.Enabled = _toggleTButton.Enabled = _nextTButton.Enabled = _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = _positionBar.Enabled = false; - Text = Utils.ProgramName; - _songInfo.SetNumTracks(0); - _songInfo.ResetMutes(); - ResetPlaylistStuff(false); - UpdatePositionIndicators(0L); - UpdateTaskbarState(); - _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; - _songNumerical.ValueChanged -= SongNumerical_ValueChanged; - _songNumerical.Visible = false; - _songNumerical.Value = _songNumerical.Maximum = 0; - _songsComboBox.SelectedItem = null; - _songsComboBox.Items.Clear(); - _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; - _songNumerical.ValueChanged += SongNumerical_ValueChanged; - } - private bool _stopUI = false; - private void UpdateUI(object sender, EventArgs e) - { - if (_stopUI) - { - _stopUI = false; - if (_playlistPlaying) - { - _playedSongs.Add(_curSong); - SetAndLoadNextPlaylistSong(); - } - else - { - Stop(); - } - } - else - { - if (WindowState != FormWindowState.Minimized) - { - SongInfoControl.SongInfo info = _songInfo.Info; - Engine.Instance.Player.GetSongState(info); - _piano.UpdateKeys(info, PianoTracks); - _songInfo.Invalidate(); - } - UpdatePositionIndicators(Engine.Instance.Player.ElapsedTicks); - } - } - private void SongEnded() - { - _stopUI = true; - } - private void UpdatePositionIndicators(long ticks) - { - if (_positionBarFree) - { - _positionBar.Value = ticks; - } - if (GlobalConfig.Instance.TaskbarProgress && TaskbarManager.IsPlatformSupported) - { - TaskbarManager.Instance.SetProgressValue((int)ticks, (int)_positionBar.Maximum); - } - } - private void UpdateTaskbarState() - { - if (GlobalConfig.Instance.TaskbarProgress && TaskbarManager.IsPlatformSupported) - { - TaskbarProgressBarState state; - switch (Engine.Instance?.Player.State) - { - case PlayerState.Playing: state = TaskbarProgressBarState.Normal; break; - case PlayerState.Paused: state = TaskbarProgressBarState.Paused; break; - default: state = TaskbarProgressBarState.NoProgress; break; - } - TaskbarManager.Instance.SetProgressState(state); - } - } - private void UpdateTaskbarButtons() - { - if (TaskbarManager.IsPlatformSupported) - { - if (_playlistPlaying) - { - _prevTButton.Enabled = _playedSongs.Count > 0; - _nextTButton.Enabled = true; - } - else - { - _prevTButton.Enabled = _curSong > 0; - _nextTButton.Enabled = _curSong < _songNumerical.Maximum; - } - switch (Engine.Instance.Player.State) - { - case PlayerState.Stopped: _toggleTButton.Icon = Resources.IconPlay; _toggleTButton.Tooltip = Strings.PlayerPlay; break; - case PlayerState.Playing: _toggleTButton.Icon = Resources.IconPause; _toggleTButton.Tooltip = Strings.PlayerPause; break; - case PlayerState.Paused: _toggleTButton.Icon = Resources.IconPlay; _toggleTButton.Tooltip = Strings.PlayerUnpause; break; - } - _toggleTButton.Enabled = true; - } - } - - private void OpenTrackViewer(object sender, EventArgs e) - { - if (_trackViewer != null) - { - _trackViewer.Focus(); - return; - } - _trackViewer = new TrackViewer { Owner = this }; - _trackViewer.FormClosed += (o, s) => _trackViewer = null; - _trackViewer.Show(); - } - - protected override void OnFormClosing(FormClosingEventArgs e) - { - DisposeEngine(); - base.OnFormClosing(e); - } - private void OnResize(object sender, EventArgs e) - { - if (WindowState != FormWindowState.Minimized) - { - _splitContainer.SplitterDistance = (int)(ClientSize.Height / 5.5) - 25; // -25 for menustrip (24) and itself (1) - - int w1 = (int)(_splitContainer.Panel1.Width / 2.35); - int h1 = (int)(_splitContainer.Panel1.Height / 5.0); - - int xoff = _splitContainer.Panel1.Width / 83; - int yoff = _splitContainer.Panel1.Height / 25; - int a, b, c, d; - - // Buttons - a = (w1 / 3) - xoff; - b = (xoff / 2) + 1; - _playButton.Location = new Point(xoff + b, yoff); - _pauseButton.Location = new Point((xoff * 2) + a + b, yoff); - _stopButton.Location = new Point((xoff * 3) + (a * 2) + b, yoff); - _playButton.Size = _pauseButton.Size = _stopButton.Size = new Size(a, h1); - c = yoff + ((h1 - 21) / 2); - _songNumerical.Location = new Point((xoff * 4) + (a * 3) + b, c); - _songNumerical.Size = new Size((int)(a / 1.175), 21); - // Song combobox - d = _splitContainer.Panel1.Width - w1 - xoff; - _songsComboBox.Location = new Point(d, c); - _songsComboBox.Size = new Size(w1, 21); - - // Volume bar - c = (int)(_splitContainer.Panel1.Height / 3.5); - _volumeBar.Location = new Point(xoff, c); - _volumeBar.Size = new Size(w1, h1); - // Position bar - _positionBar.Location = new Point(d, c); - _positionBar.Size = new Size(w1, h1); - - // Piano - _piano.Size = new Size(_splitContainer.Panel1.Width, (int)(_splitContainer.Panel1.Height / 2.5)); // Force it to initialize piano keys again - _piano.Location = new Point((_splitContainer.Panel1.Width - (_piano.WhiteKeyWidth * PianoControl.WhiteKeyCount)) / 2, _splitContainer.Panel1.Height - _piano.Height - 1); - } - } - protected override bool ProcessCmdKey(ref Message msg, Keys keyData) - { - if (keyData == Keys.Space && _playButton.Enabled && !_songsComboBox.Focused) - { - TogglePlayback(null, null); - return true; - } - else - { - return base.ProcessCmdKey(ref msg, keyData); - } - } - } -} diff --git a/VG Music Studio.backup/UI/PianoControl.cs b/VG Music Studio.backup/UI/PianoControl.cs deleted file mode 100644 index 102a4b0a..00000000 --- a/VG Music Studio.backup/UI/PianoControl.cs +++ /dev/null @@ -1,183 +0,0 @@ -#region License - -/* Copyright (c) 2006 Leslie Sanford - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#endregion - -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Util; -using System; -using System.ComponentModel; -using System.Drawing; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class PianoControl : Control - { - private enum KeyType : byte - { - Black, - White - } - private static readonly KeyType[] KeyTypeTable = new KeyType[12] - { - KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White - }; - private const double blackKeyScale = 2.0 / 3.0; - - public class PianoKey : Control - { - public bool Dirty; - public bool Pressed; - - public readonly SolidBrush OnBrush = new SolidBrush(Color.Transparent); - private readonly SolidBrush _offBrush; - - public PianoKey(byte k) - { - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); - SetStyle(ControlStyles.Selectable, false); - _offBrush = new SolidBrush(new HSLColor(160.0, 0.0, KeyTypeTable[k % 12] == KeyType.White ? k / 12 % 2 == 0 ? 240.0 : 120.0 : 0.0)); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - OnBrush.Dispose(); - _offBrush.Dispose(); - } - base.Dispose(disposing); - } - protected override void OnPaint(PaintEventArgs e) - { - e.Graphics.FillRectangle(Pressed ? OnBrush : _offBrush, 1, 1, Width - 2, Height - 2); - e.Graphics.DrawRectangle(Pens.Black, 0, 0, Width - 1, Height - 1); - base.OnPaint(e); - } - } - - private readonly PianoKey[] _keys = new PianoKey[0x80]; - public const int WhiteKeyCount = 75; - public int WhiteKeyWidth; - - public PianoControl() - { - SetStyle(ControlStyles.Selectable, false); - for (byte k = 0; k <= 0x7F; k++) - { - var key = new PianoKey(k); - _keys[k] = key; - if (KeyTypeTable[k % 12] == KeyType.Black) - { - key.BringToFront(); - } - Controls.Add(key); - } - SetKeySizes(); - } - private void SetKeySizes() - { - WhiteKeyWidth = Width / WhiteKeyCount; - int blackKeyWidth = (int)(WhiteKeyWidth * blackKeyScale); - int blackKeyHeight = (int)(Height * blackKeyScale); - int offset = WhiteKeyWidth - (blackKeyWidth / 2); - int w = 0; - for (int k = 0; k <= 0x7F; k++) - { - PianoKey key = _keys[k]; - if (KeyTypeTable[k % 12] == KeyType.White) - { - key.Height = Height; - key.Width = WhiteKeyWidth; - key.Location = new Point(w * WhiteKeyWidth, 0); - w++; - } - else - { - key.Height = blackKeyHeight; - key.Width = blackKeyWidth; - key.Location = new Point(offset + ((w - 1) * WhiteKeyWidth)); - key.BringToFront(); - } - } - } - - public void UpdateKeys(SongInfoControl.SongInfo info, bool[] enabledTracks) - { - for (int k = 0; k <= 0x7F; k++) - { - PianoKey key = _keys[k]; - key.Dirty = key.Pressed; - key.Pressed = false; - } - for (int i = SongInfoControl.SongInfo.MaxTracks - 1; i >= 0; i--) - { - if (enabledTracks[i]) - { - SongInfoControl.SongInfo.Track tin = info.Tracks[i]; - for (int nk = 0; nk < SongInfoControl.SongInfo.MaxKeys; nk++) - { - byte k = tin.Keys[nk]; - if (k == byte.MaxValue) - { - break; - } - else - { - PianoKey key = _keys[k]; - key.OnBrush.Color = GlobalConfig.Instance.Colors[tin.Voice]; - key.Pressed = key.Dirty = true; - } - } - } - } - for (int k = 0; k <= 0x7F; k++) - { - PianoKey key = _keys[k]; - if (key.Dirty) - { - key.Invalidate(); - } - } - } - - protected override void OnResize(EventArgs e) - { - SetKeySizes(); - base.OnResize(e); - } - protected override void Dispose(bool disposing) - { - if (disposing) - { - for (int k = 0; k < 0x80; k++) - { - _keys[k].Dispose(); - } - } - base.Dispose(disposing); - } - } -} diff --git a/VG Music Studio.backup/UI/SongInfoControl.cs b/VG Music Studio.backup/UI/SongInfoControl.cs deleted file mode 100644 index ce91a71d..00000000 --- a/VG Music Studio.backup/UI/SongInfoControl.cs +++ /dev/null @@ -1,354 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.ComponentModel; -using System.Drawing; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class SongInfoControl : Control - { - public class SongInfo - { - public class Track - { - public long Position; - public byte Voice; - public byte Volume; - public int LFO; - public long Rest; - public sbyte Panpot; - public float LeftVolume; - public float RightVolume; - public int PitchBend; - public byte Extra; - public string Type; - public byte[] Keys = new byte[MaxKeys]; - - public int PreviousKeysTime; - public string PreviousKeys; - - public Track() - { - for (int i = 0; i < MaxKeys; i++) - { - Keys[i] = byte.MaxValue; - } - } - } - public const int MaxKeys = 32 + 1; // DSE is currently set to use 32 channels - public const int MaxTracks = 18; // PMD2 has a few songs with 18 tracks - - public ushort Tempo; - public Track[] Tracks = new Track[MaxTracks]; - - public SongInfo() - { - for (int i = 0; i < MaxTracks; i++) - { - Tracks[i] = new Track(); - } - } - } - - private const int _checkboxSize = 15; - - private readonly CheckBox[] _mutes; - private readonly CheckBox[] _pianos; - private readonly SolidBrush _solidBrush = new SolidBrush(Theme.PlayerColor); - private readonly Pen _pen = new Pen(Color.Transparent); - - public SongInfo Info; - private int _numTracksToDraw; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _solidBrush.Dispose(); - _pen.Dispose(); - } - base.Dispose(disposing); - } - public SongInfoControl() - { - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); - SetStyle(ControlStyles.Selectable, false); - Font = new Font("Segoe UI", 10.5f, FontStyle.Regular, GraphicsUnit.Point); - Size = new Size(675, 675); - - _pianos = new CheckBox[SongInfo.MaxTracks + 1]; - _mutes = new CheckBox[SongInfo.MaxTracks + 1]; - for (int i = 0; i < SongInfo.MaxTracks + 1; i++) - { - _pianos[i] = new CheckBox - { - BackColor = Color.Transparent, - Checked = true, - Size = new Size(_checkboxSize, _checkboxSize), - TabStop = false - }; - _pianos[i].CheckStateChanged += TogglePiano; - _mutes[i] = new CheckBox - { - BackColor = Color.Transparent, - Checked = true, - Size = new Size(_checkboxSize, _checkboxSize), - TabStop = false - }; - _mutes[i].CheckStateChanged += ToggleMute; - } - Controls.AddRange(_pianos); - Controls.AddRange(_mutes); - - SetNumTracks(0); - DeleteData(); - } - - private void TogglePiano(object sender, EventArgs e) - { - var check = (CheckBox)sender; - CheckBox master = _pianos[SongInfo.MaxTracks]; - if (check == master) - { - bool b = check.CheckState != CheckState.Unchecked; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - _pianos[i].Checked = b; - } - } - else - { - int numChecked = 0; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - if (_pianos[i] == check) - { - MainForm.Instance.PianoTracks[i] = _pianos[i].Checked; - } - if (_pianos[i].Checked) - { - numChecked++; - } - } - master.CheckStateChanged -= TogglePiano; - master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); - master.CheckStateChanged += TogglePiano; - } - } - private void ToggleMute(object sender, EventArgs e) - { - var check = (CheckBox)sender; - CheckBox master = _mutes[SongInfo.MaxTracks]; - if (check == master) - { - bool b = check.CheckState != CheckState.Unchecked; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - _mutes[i].Checked = b; - } - } - else - { - int numChecked = 0; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - if (_mutes[i] == check) - { - Engine.Instance.Mixer.Mutes[i] = !check.Checked; - } - if (_mutes[i].Checked) - { - numChecked++; - } - } - master.CheckStateChanged -= ToggleMute; - master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); - master.CheckStateChanged += ToggleMute; - } - } - - public void DeleteData() - { - Info = new SongInfo(); - Invalidate(); - } - public void SetNumTracks(int num) - { - _numTracksToDraw = num; - _pianos[SongInfo.MaxTracks].Enabled = _mutes[SongInfo.MaxTracks].Enabled = num > 0; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - _pianos[i].Visible = _mutes[i].Visible = i < num; - } - OnResize(EventArgs.Empty); - } - public void ResetMutes() - { - for (int i = 0; i < SongInfo.MaxTracks + 1; i++) - { - CheckBox mute = _mutes[i]; - mute.CheckStateChanged -= ToggleMute; - mute.CheckState = CheckState.Checked; - mute.CheckStateChanged += ToggleMute; - } - } - - private float _infoHeight, _infoY, _positionX, _keysX, _delayX, _typeEndX, _typeX, _voicesX, _row2ElementAdditionX, _yMargin, _trackHeight, _row2Offset, _tempoX; - private int _barHeight, _barStartX, _barWidth, _bwd, _barRightBoundX, _barCenterX; - protected override void OnResize(EventArgs e) - { - _infoHeight = Height / 30f; - _infoY = _infoHeight - (TextRenderer.MeasureText("A", Font).Height * 1.125f); - _positionX = (_checkboxSize * 2) + 2; - int fWidth = Width - (int)_positionX; // Width between checkboxes' edges and the window edge - _keysX = _positionX + (fWidth / 4.4f); - _delayX = _positionX + (fWidth / 7.5f); - _typeEndX = _positionX + fWidth - (fWidth / 100f); - _typeX = _typeEndX - TextRenderer.MeasureText(Strings.PlayerType, Font).Width; - _voicesX = _positionX + (fWidth / 25f); - _row2ElementAdditionX = fWidth / 15f; - - _yMargin = Height / 200f; - _trackHeight = (Height - _yMargin) / ((_numTracksToDraw < 16 ? 16 : _numTracksToDraw) * 1.04f); - _row2Offset = _trackHeight / 2.5f; - _barHeight = (int)(Height / 30.3f); - _barStartX = (int)(_positionX + (fWidth / 2.35f)); - _barWidth = (int)(fWidth / 2.95f); - _bwd = _barWidth % 2; // Add/Subtract by 1 if the bar width is odd - _barRightBoundX = _barStartX + _barWidth - _bwd; - _barCenterX = _barStartX + (_barWidth / 2); - - _tempoX = _barCenterX - (TextRenderer.MeasureText(string.Format("{0} - 999", Strings.PlayerTempo), Font).Width / 2); - - if (_mutes != null) - { - int x1 = 3; - int x2 = _checkboxSize + 4; - int y = (int)_infoY + 3; - _mutes[SongInfo.MaxTracks].Location = new Point(x1, y); - _pianos[SongInfo.MaxTracks].Location = new Point(x2, y); - for (int i = 0; i < _numTracksToDraw; i++) - { - float r1y = _infoHeight + _yMargin + (i * _trackHeight); - y = (int)r1y + 4; - _mutes[i].Location = new Point(x1, y); - _pianos[i].Location = new Point(x2, y); - } - } - - base.OnResize(e); - } - protected override void OnPaint(PaintEventArgs e) - { - _solidBrush.Color = Theme.PlayerColor; - e.Graphics.FillRectangle(_solidBrush, e.ClipRectangle); - - e.Graphics.DrawString(Strings.PlayerPosition, Font, Brushes.Lime, _positionX, _infoY); - e.Graphics.DrawString(Strings.PlayerRest, Font, Brushes.Crimson, _delayX, _infoY); - e.Graphics.DrawString(Strings.PlayerNotes, Font, Brushes.Turquoise, _keysX, _infoY); - e.Graphics.DrawString("L", Font, Brushes.GreenYellow, _barStartX - 5, _infoY); - e.Graphics.DrawString(string.Format("{0} - ", Strings.PlayerTempo) + Info.Tempo, Font, Brushes.Cyan, _tempoX, _infoY); - e.Graphics.DrawString("R", Font, Brushes.GreenYellow, _barRightBoundX - 5, _infoY); - e.Graphics.DrawString(Strings.PlayerType, Font, Brushes.DeepPink, _typeX, _infoY); - e.Graphics.DrawLine(Pens.Gold, 0, _infoHeight, Width, _infoHeight); - - for (int i = 0; i < _numTracksToDraw; i++) - { - SongInfo.Track track = Info.Tracks[i]; - float r1y = _infoHeight + _yMargin + (i * _trackHeight); // Row 1 y - e.Graphics.DrawString(string.Format("0x{0:X}", track.Position), Font, Brushes.Lime, _positionX, r1y); - e.Graphics.DrawString(track.Rest.ToString(), Font, Brushes.Crimson, _delayX, r1y); - - float r2y = r1y + _row2Offset; // Row 2 y - e.Graphics.DrawString(track.Panpot.ToString(), Font, Brushes.OrangeRed, _voicesX + _row2ElementAdditionX, r2y); - e.Graphics.DrawString(track.Volume.ToString(), Font, Brushes.LightSeaGreen, _voicesX + (_row2ElementAdditionX * 2), r2y); - e.Graphics.DrawString(track.LFO.ToString(), Font, Brushes.SkyBlue, _voicesX + (_row2ElementAdditionX * 3), r2y); - e.Graphics.DrawString(track.PitchBend.ToString(), Font, Brushes.Purple, _voicesX + (_row2ElementAdditionX * 4), r2y); - e.Graphics.DrawString(track.Extra.ToString(), Font, Brushes.HotPink, _voicesX + (_row2ElementAdditionX * 5), r2y); - - int by = (int)(r1y + _yMargin); // Bar y - int byh = by + _barHeight; - e.Graphics.DrawString(track.Type, Font, Brushes.DeepPink, _typeEndX - e.Graphics.MeasureString(track.Type, Font).Width, by + (_row2Offset / (Font.Size / 2.5f))); - e.Graphics.DrawLine(Pens.GreenYellow, _barStartX, by, _barStartX, byh); // Left bar bound line - e.Graphics.DrawLine(Pens.GreenYellow, _barRightBoundX, by, _barRightBoundX, byh); // Right bar bound line - if (GlobalConfig.Instance.PanpotIndicators) - { - int pax = (int)(_barStartX + (_barWidth / 2) + (_barWidth / 2 * (track.Panpot / (float)0x40))); // Pan line x - e.Graphics.DrawLine(Pens.OrangeRed, pax, by, pax, byh); // Pan line - } - - { - Color color = GlobalConfig.Instance.Colors[track.Voice]; - _solidBrush.Color = color; - _pen.Color = color; - e.Graphics.DrawString(track.Voice.ToString(), Font, _solidBrush, _voicesX, r2y); - var rect = new Rectangle((int)(_barStartX + (_barWidth / 2) - (track.LeftVolume * _barWidth / 2)) + _bwd, - by, - (int)((track.LeftVolume + track.RightVolume) * _barWidth / 2), - _barHeight); - if (!rect.IsEmpty) - { - float velocity = (track.LeftVolume + track.RightVolume) * 2f; - if (velocity > 1f) - { - velocity = 1f; - } - _solidBrush.Color = Color.FromArgb((byte)(velocity * byte.MaxValue), color); - e.Graphics.FillRectangle(_solidBrush, rect); - e.Graphics.DrawRectangle(_pen, rect); - } - if (GlobalConfig.Instance.CenterIndicators) - { - e.Graphics.DrawLine(_pen, _barCenterX, by, _barCenterX, byh); // Center line - } - } - { - string keysString; - if (track.Keys[0] == byte.MaxValue) - { - if (track.PreviousKeysTime != 0) - { - track.PreviousKeysTime--; - keysString = track.PreviousKeys; - } - else - { - keysString = string.Empty; - } - } - else - { - keysString = string.Empty; - for (int nk = 0; nk < SongInfo.MaxKeys; nk++) - { - byte k = track.Keys[nk]; - if (k == byte.MaxValue) - { - break; - } - else - { - if (nk != 0) - { - keysString += ' '; - } - keysString += Utils.GetNoteName(k); - } - } - track.PreviousKeysTime = GlobalConfig.Instance.RefreshRate << 2; - track.PreviousKeys = keysString; - } - if (keysString != string.Empty) - { - e.Graphics.DrawString(keysString, Font, Brushes.Turquoise, _keysX, r1y); - } - } - } - base.OnPaint(e); - } - } -} diff --git a/VG Music Studio.backup/UI/Theme.cs b/VG Music Studio.backup/UI/Theme.cs deleted file mode 100644 index 0973e2a7..00000000 --- a/VG Music Studio.backup/UI/Theme.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - internal static class Theme - { - public static readonly Font Font = new Font("Segoe UI", 8f, FontStyle.Bold); - public static readonly Color - BackColor = Color.FromArgb(33, 33, 39), - BackColorDisabled = Color.FromArgb(35, 42, 47), - BackColorMouseOver = Color.FromArgb(32, 37, 47), - BorderColor = Color.FromArgb(25, 120, 186), - BorderColorDisabled = Color.FromArgb(47, 55, 60), - ForeColor = Color.FromArgb(94, 159, 230), - PlayerColor = Color.FromArgb(8, 8, 8), - SelectionColor = Color.FromArgb(7, 51, 141), - TitleBar = Color.FromArgb(16, 40, 63); - - public static HSLColor DrainColor(Color c) - { - var drained = new HSLColor(c); - drained.Saturation /= 2.5; - return drained; - } - } - - internal class ThemedButton : Button - { - public ThemedButton() : base() - { - FlatAppearance.MouseOverBackColor = Theme.BackColorMouseOver; - FlatStyle = FlatStyle.Flat; - Font = Theme.Font; - ForeColor = Theme.ForeColor; - } - protected override void OnEnabledChanged(EventArgs e) - { - base.OnEnabledChanged(e); - BackColor = Enabled ? Theme.BackColor : Theme.BackColorDisabled; - FlatAppearance.BorderColor = Enabled ? Theme.BorderColor : Theme.BorderColorDisabled; - } - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - if (!Enabled) - { - TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, Theme.DrainColor(ForeColor), BackColor); - } - } - protected override bool ShowFocusCues => false; - } - internal class ThemedLabel : Label - { - public ThemedLabel() : base() - { - Font = Theme.Font; - ForeColor = Theme.ForeColor; - } - } - internal class ThemedForm : Form - { - public ThemedForm() : base() - { - BackColor = Theme.BackColor; - Icon = Resources.Icon; - } - } - internal class ThemedPanel : Panel - { - public ThemedPanel() : base() - { - SetStyle(ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); - } - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - using (var b = new SolidBrush(BackColor)) - { - e.Graphics.FillRectangle(b, e.ClipRectangle); - } - using (var b = new SolidBrush(Theme.BorderColor)) - using (var p = new Pen(b, 2)) - { - e.Graphics.DrawRectangle(p, e.ClipRectangle); - } - } - private const int WM_PAINT = 0xF; - protected override void WndProc(ref Message m) - { - if (m.Msg == WM_PAINT) - { - Invalidate(); - } - base.WndProc(ref m); - } - } - internal class ThemedTextBox : TextBox - { - public ThemedTextBox() : base() - { - BackColor = Theme.BackColor; - Font = Theme.Font; - ForeColor = Theme.ForeColor; - } - [DllImport("user32.dll")] - private static extern IntPtr GetWindowDC(IntPtr hWnd); - [DllImport("user32.dll")] - private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); - [DllImport("user32.dll")] - private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprc, IntPtr hrgn, uint flags); - private const int WM_NCPAINT = 0x85; - private const uint RDW_INVALIDATE = 0x1; - private const uint RDW_IUPDATENOW = 0x100; - private const uint RDW_FRAME = 0x400; - protected override void WndProc(ref Message m) - { - base.WndProc(ref m); - if (m.Msg == WM_NCPAINT && BorderStyle == BorderStyle.Fixed3D) - { - IntPtr hdc = GetWindowDC(Handle); - using (var g = Graphics.FromHdcInternal(hdc)) - using (var p = new Pen(Theme.BorderColor)) - { - g.DrawRectangle(p, new Rectangle(0, 0, Width - 1, Height - 1)); - } - ReleaseDC(Handle, hdc); - } - } - protected override void OnSizeChanged(EventArgs e) - { - base.OnSizeChanged(e); - RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero, RDW_FRAME | RDW_IUPDATENOW | RDW_INVALIDATE); - } - } - internal class ThemedRichTextBox : RichTextBox - { - public ThemedRichTextBox() : base() - { - BackColor = Theme.BackColor; - Font = Theme.Font; - ForeColor = Theme.ForeColor; - SelectionColor = Theme.SelectionColor; - } - } - internal class ThemedNumeric : NumericUpDown - { - public ThemedNumeric() : base() - { - BackColor = Theme.BackColor; - Font = new Font(Theme.Font.FontFamily, 7.5f, Theme.Font.Style); - ForeColor = Theme.ForeColor; - TextAlign = HorizontalAlignment.Center; - } - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Enabled ? Theme.BorderColor : Theme.BorderColorDisabled, ButtonBorderStyle.Solid); - } - } -} diff --git a/VG Music Studio.backup/UI/TrackViewer.cs b/VG Music Studio.backup/UI/TrackViewer.cs deleted file mode 100644 index 7aa83b97..00000000 --- a/VG Music Studio.backup/UI/TrackViewer.cs +++ /dev/null @@ -1,113 +0,0 @@ -using BrightIdeasSoftware; -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class TrackViewer : ThemedForm - { - private List _events; - private readonly ObjectListView _listView; - private readonly ComboBox _tracksBox; - - public TrackViewer() - { - int w = (600 / 2) - 12 - 6, h = 400 - 12 - 11; - _listView = new ObjectListView - { - FullRowSelect = true, - HeaderStyle = ColumnHeaderStyle.Nonclickable, - HideSelection = false, - Location = new Point(12, 12), - MultiSelect = false, - RowFormatter = RowColorer, - ShowGroups = false, - Size = new Size(w, h), - UseFiltering = true, - UseFilterIndicator = true - }; - OLVColumn c1, c2, c3, c4; - c1 = new OLVColumn(Strings.TrackViewerEvent, "Command.Label"); - c2 = new OLVColumn(Strings.TrackViewerArguments, "Command.Arguments") { UseFiltering = false }; - c3 = new OLVColumn(Strings.TrackViewerOffset, "Offset") { AspectToStringFormat = "0x{0:X}", UseFiltering = false }; - c4 = new OLVColumn(Strings.TrackViewerTicks, "Ticks") { AspectGetter = (o) => string.Join(", ", ((SongEvent)o).Ticks), UseFiltering = false }; - c1.Width = c2.Width = c3.Width = 72; - c4.Width = 47; - c1.Hideable = c2.Hideable = c3.Hideable = c4.Hideable = false; - c1.TextAlign = c2.TextAlign = c3.TextAlign = c4.TextAlign = HorizontalAlignment.Center; - _listView.AllColumns.AddRange(new OLVColumn[] { c1, c2, c3, c4 }); - _listView.RebuildColumns(); - _listView.ItemActivate += ListView_ItemActivate; - - var panel1 = new ThemedPanel { Location = new Point(306, 12), Size = new Size(w, h) }; - _tracksBox = new ComboBox - { - Enabled = false, - Location = new Point(4, 4), - Size = new Size(100, 21) - }; - _tracksBox.SelectedIndexChanged += TracksBox_SelectedIndexChanged; - panel1.Controls.AddRange(new Control[] { _tracksBox }); - - ClientSize = new Size(600, 400); - Controls.AddRange(new Control[] { _listView, panel1 }); - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - Text = $"{Utils.ProgramName} ― {Strings.TrackViewerTitle}"; - - UpdateTracks(); - } - - private void ListView_ItemActivate(object sender, EventArgs e) - { - List list = ((SongEvent)_listView.SelectedItem.RowObject).Ticks; - if (list.Count > 0) - { - Engine.Instance?.Player.SetCurrentPosition(list[0]); - MainForm.Instance.LetUIKnowPlayerIsPlaying(); - } - } - - private void RowColorer(OLVListItem item) - { - item.BackColor = ((SongEvent)item.RowObject).Command.Color; - } - - private void TracksBox_SelectedIndexChanged(object sender, EventArgs e) - { - int i = _tracksBox.SelectedIndex; - if (i != -1) - { - _events = Engine.Instance.Player.Events[i]; - _listView.SetObjects(_events); - } - else - { - _listView.Items.Clear(); - } - } - public void UpdateTracks() - { - int numTracks = (Engine.Instance?.Player.Events?.Length).GetValueOrDefault(); - bool tracks = numTracks > 0; - _tracksBox.Enabled = tracks; - if (tracks) - { - // Track 0, Track 1, ... - _tracksBox.DataSource = Enumerable.Range(0, numTracks).Select(i => string.Format(Strings.TrackViewerTrackX, i)).ToList(); - } - else - { - _tracksBox.DataSource = null; - } - } - } -} \ No newline at end of file diff --git a/VG Music Studio.backup/UI/ValueTextBox.cs b/VG Music Studio.backup/UI/ValueTextBox.cs deleted file mode 100644 index a83e3516..00000000 --- a/VG Music Studio.backup/UI/ValueTextBox.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Kermalis.VGMusicStudio.Util; -using System; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - internal class ValueTextBox : ThemedTextBox - { - private bool _hex = false; - public bool Hexadecimal - { - get => _hex; - set - { - _hex = value; - OnTextChanged(EventArgs.Empty); - SelectionStart = Text.Length; - } - } - private long _max = long.MaxValue; - public long Maximum - { - get => _max; - set - { - _max = value; - OnTextChanged(EventArgs.Empty); - } - } - private long _min = 0; - public long Minimum - { - get => _min; - set - { - _min = value; - OnTextChanged(EventArgs.Empty); - } - } - public long Value - { - get - { - if (TextLength > 0) - { - if (Utils.TryParseValue(Text, _min, _max, out long l)) - { - return l; - } - } - return _min; - } - set - { - int i = SelectionStart; - Text = Hexadecimal ? ("0x" + value.ToString("X")) : value.ToString(); - SelectionStart = i; - OnValueChanged(EventArgs.Empty); - } - } - - protected override void WndProc(ref Message m) - { - const int WM_NOTIFY = 0x0282; - if (m.Msg == WM_NOTIFY && m.WParam == new IntPtr(0xB)) - { - if (Hexadecimal && SelectionStart < 2) - { - SelectionStart = 2; - } - } - base.WndProc(ref m); - } - protected override void OnKeyPress(KeyPressEventArgs e) - { - e.Handled = true; // Don't pay attention to this event unless: - - if ((char.IsControl(e.KeyChar) && !(Hexadecimal && SelectionStart <= 2 && SelectionLength == 0 && e.KeyChar == (char)Keys.Back)) || // Backspace isn't used on the "0x" prefix - char.IsDigit(e.KeyChar) || // It is a digit - (e.KeyChar >= 'a' && e.KeyChar <= 'f') || // It is a letter that shows in hex - (e.KeyChar >= 'A' && e.KeyChar <= 'F')) - { - e.Handled = false; - } - base.OnKeyPress(e); - } - protected override void OnTextChanged(EventArgs e) - { - base.OnTextChanged(e); - Value = Value; - } - - private EventHandler _onValueChanged = null; - public event EventHandler ValueChanged - { - add => _onValueChanged += value; - remove => _onValueChanged -= value; - } - protected virtual void OnValueChanged(EventArgs e) - { - _onValueChanged?.Invoke(this, e); - } - } -} diff --git a/VG Music Studio.backup/Util/BetterExceptions.cs b/VG Music Studio.backup/Util/BetterExceptions.cs deleted file mode 100644 index a40a6697..00000000 --- a/VG Music Studio.backup/Util/BetterExceptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Kermalis.VGMusicStudio.Util -{ - internal class InvalidValueException : Exception - { - public object Value { get; } - - public InvalidValueException(object value, string message) : base(message) - { - Value = value; - } - } - internal class BetterKeyNotFoundException : KeyNotFoundException - { - public object Key { get; } - - public BetterKeyNotFoundException(object key, Exception innerException) : base($"\"{key}\" was not present in the dictionary.", innerException) - { - Key = key; - } - } -} diff --git a/VG Music Studio.backup/Util/HSLColor.cs b/VG Music Studio.backup/Util/HSLColor.cs deleted file mode 100644 index 6dbda597..00000000 --- a/VG Music Studio.backup/Util/HSLColor.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Util -{ - // https://richnewman.wordpress.com/about/code-listings-and-diagrams/hslcolor-class/ - class HSLColor - { - // Private data members below are on scale 0-1 - // They are scaled for use externally based on scale - private double hue = 1.0; - private double saturation = 1.0; - private double luminosity = 1.0; - - private const double scale = 240.0; - - public double Hue - { - get { return hue * scale; } - set { hue = CheckRange(value / scale); } - } - public double Saturation - { - get { return saturation * scale; } - set { saturation = CheckRange(value / scale); } - } - public double Luminosity - { - get { return luminosity * scale; } - set { luminosity = CheckRange(value / scale); } - } - - private double CheckRange(double value) - { - if (value < 0.0) - { - value = 0.0; - } - else if (value > 1.0) - { - value = 1.0; - } - return value; - } - - public override string ToString() - { - return string.Format("H: {0:#0.##} S: {1:#0.##} L: {2:#0.##}", Hue, Saturation, Luminosity); - } - - public string ToRGBString() - { - Color color = this; - return string.Format("R: {0:#0.##} G: {1:#0.##} B: {2:#0.##}", color.R, color.G, color.B); - } - - #region Casts to/from System.Drawing.Color - public static implicit operator Color(HSLColor hslColor) - { - double r = 0, g = 0, b = 0; - if (hslColor.luminosity != 0) - { - if (hslColor.saturation == 0) - { - r = g = b = hslColor.luminosity; - } - else - { - double temp2 = GetTemp2(hslColor); - double temp1 = 2.0 * hslColor.luminosity - temp2; - - r = GetColorComponent(temp1, temp2, hslColor.hue + 1.0 / 3.0); - g = GetColorComponent(temp1, temp2, hslColor.hue); - b = GetColorComponent(temp1, temp2, hslColor.hue - 1.0 / 3.0); - } - } - return Color.FromArgb((int)(255 * r), (int)(255 * g), (int)(255 * b)); - } - - private static double GetColorComponent(double temp1, double temp2, double temp3) - { - temp3 = MoveIntoRange(temp3); - if (temp3 < 1.0 / 6.0) - { - return temp1 + (temp2 - temp1) * 6.0 * temp3; - } - else if (temp3 < 0.5) - { - return temp2; - } - else if (temp3 < 2.0 / 3.0) - { - return temp1 + ((temp2 - temp1) * ((2.0 / 3.0) - temp3) * 6.0); - } - else - { - return temp1; - } - } - private static double MoveIntoRange(double temp3) - { - if (temp3 < 0.0) - { - temp3 += 1.0; - } - else if (temp3 > 1.0) - { - temp3 -= 1.0; - } - return temp3; - } - private static double GetTemp2(HSLColor hslColor) - { - double temp2; - if (hslColor.luminosity < 0.5) //<=?? - { - temp2 = hslColor.luminosity * (1.0 + hslColor.saturation); - } - else - { - temp2 = hslColor.luminosity + hslColor.saturation - (hslColor.luminosity * hslColor.saturation); - } - return temp2; - } - - public static implicit operator HSLColor(Color color) - { - HSLColor hslColor = new HSLColor - { - hue = color.GetHue() / 360.0, // We store hue as 0-1 as opposed to 0-360 - luminosity = color.GetBrightness(), - saturation = color.GetSaturation() - }; - return hslColor; - } - #endregion - - public void SetRGB(int red, int green, int blue) - { - HSLColor hslColor = Color.FromArgb(red, green, blue); - hue = hslColor.hue; - saturation = hslColor.saturation; - luminosity = hslColor.luminosity; - } - - public HSLColor() { } - public HSLColor(Color color) - { - SetRGB(color.R, color.G, color.B); - } - public HSLColor(int red, int green, int blue) - { - SetRGB(red, green, blue); - } - public HSLColor(double hue, double saturation, double luminosity) - { - Hue = hue; - Saturation = saturation; - Luminosity = luminosity; - } - } -} diff --git a/VG Music Studio.backup/Util/SampleUtils.cs b/VG Music Studio.backup/Util/SampleUtils.cs deleted file mode 100644 index 85ecb43e..00000000 --- a/VG Music Studio.backup/Util/SampleUtils.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Kermalis.VGMusicStudio.Util -{ - internal static class SampleUtils - { - public static short[] PCMU8ToPCM16(byte[] data, int index, int length) - { - short[] ret = new short[length]; - for (int i = 0; i < length; i++) - { - byte b = data[index + i]; - ret[i] = (short)((b - 0x80) << 8); - } - return ret; - } - } -} diff --git a/VG Music Studio.backup/Util/TimeBarrier.cs b/VG Music Studio.backup/Util/TimeBarrier.cs deleted file mode 100644 index c253c0b5..00000000 --- a/VG Music Studio.backup/Util/TimeBarrier.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Diagnostics; -using System.Threading; - -namespace Kermalis.VGMusicStudio.Util -{ - // Credit to ipatix - internal class TimeBarrier - { - private readonly Stopwatch _sw; - private readonly double _timerInterval; - private readonly double _waitInterval; - private double _lastTimeStamp; - private bool _started; - - public TimeBarrier(double framesPerSecond) - { - _waitInterval = 1.0 / framesPerSecond; - _started = false; - _sw = new Stopwatch(); - _timerInterval = 1.0 / Stopwatch.Frequency; - } - - public void Wait() - { - if (!_started) - { - return; - } - double totalElapsed = _sw.ElapsedTicks * _timerInterval; - double desiredTimeStamp = _lastTimeStamp + _waitInterval; - double timeToWait = desiredTimeStamp - totalElapsed; - if (timeToWait > 0) - { - Thread.Sleep((int)(timeToWait * 1000)); - } - _lastTimeStamp = desiredTimeStamp; - } - - public void Start() - { - if (_started) - { - return; - } - _started = true; - _lastTimeStamp = 0; - _sw.Restart(); - } - - public void Stop() - { - if (!_started) - { - return; - } - _started = false; - _sw.Stop(); - } - } -} diff --git a/VG Music Studio.backup/Util/Utils.cs b/VG Music Studio.backup/Util/Utils.cs deleted file mode 100644 index a7315069..00000000 --- a/VG Music Studio.backup/Util/Utils.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using YamlDotNet.RepresentationModel; - -namespace Kermalis.VGMusicStudio.Util -{ - internal static class Utils - { - public const string ProgramName = "VG Music Studio"; - - private static readonly Random _rng = new Random(); - private static readonly string[] _notes = Strings.Notes.Split(';'); - private static readonly char[] _spaceArray = new char[1] { ' ' }; - - public static bool TryParseValue(string value, long minValue, long maxValue, out long outValue) - { - try - { - outValue = ParseValue(string.Empty, value, minValue, maxValue); - return true; - } - catch - { - outValue = default; - return false; - } - } - /// - public static long ParseValue(string valueName, string value, long minValue, long maxValue) - { - string GetMessage() - { - return string.Format(Strings.ErrorValueParseRanged, valueName, minValue, maxValue); - } - - var provider = new CultureInfo("en-US"); - if (value.StartsWith("0x") && long.TryParse(value.Substring(2), NumberStyles.HexNumber, provider, out long hexp)) - { - if (hexp < minValue || hexp > maxValue) - { - throw new InvalidValueException(hexp, GetMessage()); - } - return hexp; - } - else if (long.TryParse(value, NumberStyles.Integer, provider, out long dec)) - { - if (dec < minValue || dec > maxValue) - { - throw new InvalidValueException(dec, GetMessage()); - } - return dec; - } - else if (long.TryParse(value, NumberStyles.HexNumber, provider, out long hex)) - { - if (hex < minValue || hex > maxValue) - { - throw new InvalidValueException(hex, GetMessage()); - } - return hex; - } - throw new InvalidValueException(value, string.Format(Strings.ErrorValueParse, valueName)); - } - /// - public static bool ParseBoolean(string valueName, string value) - { - if (!bool.TryParse(value, out bool result)) - { - throw new InvalidValueException(value, string.Format(Strings.ErrorBoolParse, valueName)); - } - return result; - } - /// - public static TEnum ParseEnum(string valueName, string value) where TEnum : struct - { - if (!Enum.TryParse(value, out TEnum result)) - { - throw new InvalidValueException(value, string.Format(Strings.ErrorConfigKeyInvalid, valueName)); - } - return result; - } - /// - public static TValue GetValue(this IDictionary dictionary, TKey key) - { - try - { - return dictionary[key]; - } - catch (KeyNotFoundException ex) - { - throw new BetterKeyNotFoundException(key, ex.InnerException); - } - } - /// - /// - public static long GetValidValue(this YamlMappingNode yamlNode, string key, long minRange, long maxRange) - { - return ParseValue(key, yamlNode.Children.GetValue(key).ToString(), minRange, maxRange); - } - /// - /// - public static bool GetValidBoolean(this YamlMappingNode yamlNode, string key) - { - return ParseBoolean(key, yamlNode.Children.GetValue(key).ToString()); - } - /// - /// - public static TEnum GetValidEnum(this YamlMappingNode yamlNode, string key) where TEnum : struct - { - return ParseEnum(key, yamlNode.Children.GetValue(key).ToString()); - } - public static string[] SplitSpace(this string str, StringSplitOptions options) - { - return str.Split(_spaceArray, options); - } - - public static string Print(this IEnumerable source, bool parenthesis = true) - { - string str = parenthesis ? "( " : ""; - str += string.Join(", ", source); - str += parenthesis ? " )" : ""; - return str; - } - /// Fisher-Yates Shuffle - public static void Shuffle(this IList source) - { - for (int a = 0; a < source.Count - 1; a++) - { - int b = _rng.Next(a, source.Count); - T value = source[a]; - source[a] = source[b]; - source[b] = value; - } - } - - public static string GetPianoKeyName(int key) - { - return _notes[key]; - } - public static string GetNoteName(int key) - { - return _notes[key % 12] + ((key / 12) + (GlobalConfig.Instance.MiddleCOctave - 5)); - } - - public static string CombineWithBaseDirectory(string path) - { - return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path); - } - } -} diff --git a/VG Music Studio.backup/VG Music Studio.csproj b/VG Music Studio.backup/VG Music Studio.csproj deleted file mode 100644 index b512cda3..00000000 --- a/VG Music Studio.backup/VG Music Studio.csproj +++ /dev/null @@ -1,433 +0,0 @@ - - - - - Debug - AnyCPU - {97C8ACF8-66A3-4321-91D6-3E94EACA577F} - WinExe - Kermalis.VGMusicStudio - VG Music Studio - v4.8 - 512 - true - - false - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true - - - AnyCPU - true - full - false - ..\Build\ - DEBUG;TRACE - prompt - 4 - Off - false - - - AnyCPU - pdbonly - true - ..\Build\ - TRACE - prompt - 4 - false - On - - - Properties\Icon.ico - - - Kermalis.VGMusicStudio.Program - - - - Dependencies\DLS2.dll - - - - - False - Dependencies\Sanford.Multimedia.Midi.dll - - - False - Dependencies\SoundFont2.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Strings.resx - - - - - Component - - - - - - Component - - - - - - - - - - - - Always - - - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - True - - - Always - - - Always - - - Always - - - Always - - - - - ResXFileCodeGenerator - Strings.Designer.cs - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - - - - - False - Microsoft .NET Framework 4.7.1 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - - 1.1.2 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.2 - - - 1.1.0.2 - - - 1.1.0 - - - 2.0.1 - - - 2.0.0 - - - 2.0.0 - - - 2.0.1 - - - 2.0.0 - - - 2.0.1 - - - 2.0.1 - - - 2.9.1 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 4.5.0 - - - 5.0.0 - - - 5.0.0 - - - 4.8.1 - - - 5.0.1 - - - 5.0.1 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 4.3.0 - - - 5.0.0 - - - 4.3.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 4.7.0 - - - 4.7.0 - - - 4.7.0 - - - 4.3.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 4.3.1 - - - 5.0.0 - - - 4.3.0 - - - 5.0.1 - - - 4.3.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 4.8.0 - - - 4.8.0 - - - 4.8.0 - - - 4.8.0 - - - 4.8.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 5.0.0 - - - 11.2.1 - - - - \ No newline at end of file diff --git a/VG Music Studio.backup/app.config b/VG Music Studio.backup/app.config deleted file mode 100644 index 42aa5898..00000000 --- a/VG Music Studio.backup/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/VG Music Studio.backup/midi2agb.exe b/VG Music Studio.backup/midi2agb.exe deleted file mode 100644 index e5dc76adb27526e8e384996ed5b3b9c5944fee16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3404655 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%0AVT9Li3%49NmPV}S@bT*WYPp=m0VN? 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

f1`!)jAJx~X$qi@S+k7GtUQalm|%g*}JG zxci9BYJUf$1s8a$9eg$JCVE+prH8Kq>R%B(dw3**u9w4d+)eDV9t+pM2IjmAt;%cc z!Pn#NqUVAvU9J-7OCrikbC} zwrYuJt2!9FRAOreAlPUtu1@5-p8bNbfr}!fYg<^= zXm4m~OX^z%Kz`LFHu-H1`4UWz;ZnB7;jvjQ_|R8tOJkyDr3%xTU{>)ufExfMjy{^C zB;3@IjIU4zqoK}kO4O*3iW2VyP|;X^0gWzLv&IliD2rw?fbA|99|6(hK+Adm6{6+5 zWvD?d=alW`=V|;DwEhg%15?ZSUxcFvTxx4MBgk&*Grz-KZ>ByobbJo_%x*g}$!Dia z)96C+C5DrR8EWul&|nsVoWQ!!o|5C$qe2Ucv*nkQip@cV{;qXkJ!%hW*{C|$jZRAs z^tU~wWuxeRdq{-ne0!od-Wd2Z=$tqDG^332%-* zXvWlO%()U*zvB_iky1e)z>eE9^x6jeIMBXmq0Yeu+%w#c{h0STmw@R;#Z_T-D4xRk zsD5M?qJmkFHx?n~q+|*yc^y>z&8Fl)p(KF)xo3o=*sg^o@vFnC1CQ<&!$C-J+jY;V z@!Gw?8Cbsm0ZQ_IYf(c2BP1!9<-2F(xUJ^IX&enYennc?S=8i%V2I2=(regCak%Ii z0poZ9R9nl+GO(hwbosmm7_|Mb39Bvf>>gXFFH+UbB%hy}+y`Gw%kS((ign zqoMjw3t@BmZ)ozVA^0|!jt(>B#dLHyTwY8Q%R;^cH?j0+QE<6`>(497_q_C0@@F@N z)z9%1PQ;<(9Lz7hLy)=4ycGo9XAu?IkCf$Ln&}17#B8(bMy%&^Zw{-4cnT|os^Dgu zUZ6@$Iwz8<_dwA{4po6oI=w*@%{_;KImW76!s-}2x@!eD_w)`?3AtF_*!0%0+8U1# zm8)AuFHk0CB4O9npy~#TvK-7ry+f4Au6=F`t1>)pb_F*f^#W;PUXqdcCy@0nN#jV2 zG+`LzU|#A4%Ea^}E!qCNusRBlW}2p~26I?2SCHNyjb^Fsu@Nu^+#Xh2;c+u9uvw}X zC=-*FFzqT(bqguuLS!~)aFbPUkVbQsFl`!E`#E^rObc$#>IKp)Qx{i8<1JA3H;X!L z>f(+>OkLMbfzS|TCH!`GfsYO4vJ3lvW{;QmuxpP;wV&g^dSN;Dc;>e5W_dBUbyxBw z__hw8BJAdAZ5lZ;?d$%1C%%b>r|?y7^>c8m=^cu&vBM<`#;JFO)ml74Rpecgmcz!b zH@GD`yZK<0vCG|IwI`m!&xNeuJGOSX20k*xbc(jOeM0&nelg(~r?1$&G|_k`6U zcy#v)zPIZosuJo4%z(znpy=NgRk?2NdV@6WkxPc1cyCypipOM_cmfCU$n7PnOonZT z^S#6I=<#?A&Dh%q-`@2GZL+_U@%Vet^)PAUc#KTPSjb_2*Bhj1Utc!ng8QKy!lT)y zNejNg>lNC#!;>-aIq3S1w7J+8c!$>;q-j54vh9=y!s=W+Zng#A;`It`+~Wz`hCLWo zqwu)d7JQG_8?;%QJg$<)A3)ku7I|uD*P}cEbY1)4g6Kh;1n>v%joM5rDVrxMOMd2c zWDK`2@o2WsS2{-GWLsN2)?U>VFRR=yR*9=qaSjHjnDLzsTn=G>sRLi$Hr{(EtUkxH zH=C;kX5oSwg@uIOWD|dbH1WG?R!W{9aIKrHCqJ!qLOAbu^Fr6(FDK2s7hXwj<0O1Op%JbFRYHw`S_EtCXhmaKZV! z^+y@%ThI_$2t0v3T+-t4x~`0l@jkJ`9sziMtwsK*z^*IlkZe?+l;B8u;B1l>$wu)a zNe8{h?jacgXYl**8yIxmScYL?x^C=*ZYM?se$+?1Zm=h&x^DPG%AYvpNnh^^e(K)x zzSvg6+WVqG!um9OlHnky+0)cs>%wfoL2BaKE-dMpg^l z0R9qb;A%&R=K=2x3uX?E5NUwAJREQ=;$}3bQSx|LO~zAr76^_!fv8{}4p}kC$%}i5 z;yi%A%trCDpa|f)kOo7FPl8n98$y4=Y|Y39U6^rY0L3+A4=qcO{gQsu>)NnbdSaQT<&Lock(+d{1$niv(gX$y~ zoT3tW9Ua1$^mJJ5i>L4mK^I&lk_$Rfb$m#4*8tT`4!Xdqj-0Sjg>fPErdvIO^WS)M z^95I7Trk(m!7ZzP}MC`)A}?@Hqz+K`tmo^cSYieJ-pH!Xv%LL>XN4&j}|*dg(h40pByk z>8fU6kvaSq7zw#>=?V5sz8Lz{_B+=*%j%0@V=~x}4X+oRI2;kbHd) z2tOxM_R`3+h{|Ch%n7Hggu>UwuY^@C9^tErGx$oF8&WQX(t(}%SNVynFMQ;bf2Z_{G(cp_=PDriQkTa^W=byrA3?4n-`6!xw z7PG*5<=mDSD2yqj=As1KOf+y;P}fNRq?Y3M^6DYrmFZg zyql`xI6ih^o|$Y!FtDl^p9xBH?h$vutzU1bfv-V7(GHM-wZa7HRRHTd;9`Wd3XtSa za0M{nZB$*aFT5V8bqQ~yXmH86p56gx!N{PO_4Yq%Wb@GbOpWYmXmZiW%7>ohGC1~v zRE=yZjQokWyy>fposRalbg|_U*1FhZC9HL^{5DGUGWBPF39DW42=W}1sSTx$-n2pO0|JaC0MkKGmw|}L zB18q#nz})iH>jC}^*g|Mz=l;VumY$@8%k3&p+Z<58gmdHsbgdmfCbltHemF-Zx(HU z%Ci!%YHWB!AwYwvJVB!CW>FGjTn<=Q6CN4o>aS7GqqDl&Iz8PrMb`49b&;|>san72 zC)jzic`s-(Rhph0LD2Bjz)kPD#5fKgM~pk=BG+9^m&%JN#+}QT9K^T=M`hW7tKKn# zn8U2_EnI?wr!emdV9voK%pW3#A8%2L0pb!1Y9vC?xd>%GutoN|{IBR2cLDALgdG*I zImli4w95V)Px#LTJHtK$1SKi z3rs$sMJ^J8w|WQqPdp~U!Q~=mT(HcHO5x;Ox3zKsai=Z|}O_#Jjocehgn;GI!n2A{cnrZGG@5 z-QFAXs24S4b=_zv8Lx>5Jx8KCxHH|{{) zD0^}nT#t9=UPu}C=Vlk>#{5EbP;+B`GrFa0%u7y2f6yCqbdl7Jxm_4{{eSGcBVUd7 zw|3-fC9HSk{EC=$H^q8}gLp?+mD1|ni64g56g-9BqI{rzxvRXnJva*Ge{I2t6r4st z0e0sWAiYO_2mqe3faI`8x1cyXbxI7+p%siR{tiU|p2E#QQse_f1>3841u0U<>x8)k z@H%Xm{}h-3_UsnOl%2bfc@MxnO^|GY$in~`eCKY#_3q!n0qo#6{|Ki(@o2b2CBTF2 z;4Q%JeLP7smIC53LM2nxmHo79bWm}1HeUoj=P z++cFc5gx!MekqFWVG0m4KMAXOcnU|a14Duf5ZOSRQ^nN)eX9jNGMPAXkR&o95s~6G zw52iN)37SSQ&=wWgNqay;frkX0pT|TcG7_#ShmOvo&H(&#EgU%v9l8GU5=4MEG~S~w#ab1*M2z@z66F&S`n7OCa2 zCn>d%9KY2-AGYktnsDUTt~G&sJx6`C<^#hyka(rMn9IP0d-un=U%A}hh$Xh37|Kf`JWp2BMdLGYCzGXhx( zJ|lva0DiQCAn;O<1wPk;J3zAWC%}G_@TD1ouLW7*CsZTppYSip|9Av`u8TotB(fSv zCDsAKdJ9Pot3g)yW+nbh*k5@o5!@>+GZtA7q%YM1LkqF6FGXGg_i|VdG9!@%L0I)T z5WHwtMI#BmAY{S96+v2k&wt~a-+0_r3A`d?Mj}gsROM_SxR^-TbxnQ+UlOul;hG>- z`4$-RF=x7~5`0a_jKx|MI6E0NK$5T!X;l`dXV;=YdIG4j@ZED$m$m9U(Pdrzspztd z&*Wz@ehS|G#L;E#g5>Daz-(HO$Kc)6W$k~at;?E=U|?OA{gL$y%B+t*H`L;9ATurk z@P^l?!Z$1NJHURxf}gV*D>D|6Tcj^-o0qSK;gP0}oC@ycAh%>jB65qc>R2FH zO(bk@6G;umMzGt^vS6XyBCY;6VEB?)I4n#of#sIWNJMUts>}-Ks{`=3s}fvp$%2J) zi&W)iV7P}^TvZ7!w`9g*$t|3njBPM6Z;wZhbWvt;dKS55_}5fskscohEJv5s&1E5g zEF`xan9|VNzL=8AN8K|p#Gfsr^0N#-1^?U>tC@NA!d^&@zJ~gk($EaNo6=D9Y+D){ zj9_4CXabi$^S*^Nj-s{E3VD z=d1oh`Th63M=w^8eYiWypZq>RSn|=A64vt3D0CJ}KH7)jAo7u`c3MIjRhX~F<0&j< zcMc*Uc|n-lp_Kq}yagsQUtk50kzBB}l=J{#JY~VjK}vD~^2$k%pizu1Bl&6vJcS2> zu*e&T3MMD{LKR8rj|AHckSlGl9}3t2l9CH*imW8WJ`9L25-M9H@_RrHE-Se}`$zA+g#G^qIo4^kyExBO3%SCKve3GSgHHx>pY>+5%7EzXU~a`6(L;k)Ylpigtis>7WQKL1o228Oq?!?Il2e zjTodkg3C|=FeKE$ti!lX^VPn11VgS;R5mmsM@dE20mXU?O%C>-02s`QsDb&aA0Br_ zf?I&H;SpI%`c(sPv=a~eRpe3dFb7#G8ybl!Xf(m4sjPS?PwDoLqW$r> zs}oqB%7#WHD&77-aW&DfBb!`Hf_1?@9i9~rWh$vo0cO%D9(Q$u%T(F$SW*>dEn^wb zbXusiHHcHUNL8!0#*QyaiZV)7Jz6-j$X3-YEsdsFb>ikCR!y5N^3>V-=N-00sOK|5dSxPg>hL8(S_Me*C%8-mcpFs|@`cv}35f7E zir_=W_4L%C1tWu)b1Mh4XH_6pqpKmORptJzg~L^Azja2%Rj+;E#Pg` zUh5052fdc?HcGE0<9ga_Ef_w%R=OXXG*#N}+TE0qKk>$`eWmVW(RP;9eUgN=)P1Lf zwN>&D3 z9zmXiXx-#G{Mw}U0s%%nz_bz6dmtiGiKt-mv>Q};lbS(Tw*bz)HmsPy3Lr!`r5=72 z!W0^FAReh>WKRGKE;~1+9DZ$r>-;>xI^2dw6aqAuzzh;yhhIsIaS32;AUrb8HDyLQ z<%OH$ow2G$d<+_2if)@EYd3pn#df|u^r~e5Xxhy`kg)EdA2u^y_t1(KgoE_ZYO{w< z-6mhnz*G3^wE=smlr($j(I|PcRXTExfC$h-tIZyIFN!{H70;oER=av=wR!{mH43-Q zS4DUVF9Z>he<3PZ4|RhouT~!t)Rs*ixxAGGmUof)C-`cwL2lFqKOM*(tobI$jk4^U zOzi2(?*ikq_z!Q3S0|clC($Q?I{6*Y5zS5N=jeX2No{1)%PxVvq06)@CG#gfzLW2) z`w$9Sv+lDJ*0b)W=mOTPyA8uZW?iX^*2CWkLhr{@xC06Y+8S7eO_kbrC|t0;2_UkE zKnXDS3KXq--y5a(w;<#&{R#-L*1ZzM7^ec(TEZIxVj@cs6>JVpfhU`v7yk0JrbXChW-RM393~Aw32$CyW7bBL?VkVz@wn2RA3A2QG$$eF^+-0DjN` z9@vnO0X9ttSAtYy@UVO}1dnEWa1%mC*a`Iv`m&J(+@menxf&1BBM`HJ5dRW@zh)uG z!EBHbwn=>Pa7+_;gfTh0UZuw%rUL0RX94;;Vql+%3`a}nU@AzDK#T;!o__-P4|Wwa zf*N26=5mz*1I+`{!i#pvS5>tS zO>%wdu`ON~Z;LnA#ABUpRjsY@wn?UY?>-}j0$q$d1hEg~PyEG5-~H}>m!M*%mZ3tz zdcV6^!dlA^XE?}yw=!8m3d{J-p>bVkE3>@WO2=EYS*Ouo-7R1J1y5lkPzBl@cMla- z5`MXG#)y1Xfk)6qRtU}jd+BcA)I0Cnf$KpFUk*F(ZXk5+)5pcsH1O0IvwOapf~W9k zR5WrhqJr(ydxX8b66og={jETKpN+m=&b1NPuDKsQK8 zkr>ADevaFK{t=r5QWYT`!6b%m(BRhj5Roxlg2{hhO#ahH<*T`Pgqq0SLQ;Uqf8&#+*M=8B%3myU za#$NSK0U6bV`@x=&|pUE=zMhqp2F#1Q)FvI1zS3LleltR3ati^x&bBmYAZaNx}Al( z04vMJCoW~-5!PJ;!tNq*?DUbZFslS#cs4$TDeKZyu1m8qr5uV!Q%ItP%wX%%#wXFe zY>{2YC!p;gq>r6X&%dYMjE~wzk3*`MDiZb0@!D8*M;&&-J(y_WmC%2?1^Y~Zg0=sl zSb=6A1T5Pv-h<)3-{W$s?GBC|G-pCAR^Qwat68z47`KpDHM)^^V}{go;~&T7t5@(8 z-o`p=WQX+*vgyrg^0<686OUkv{N*rIbIaK5SXFIpo7%3b7toF~p+iqmbQrdQ!2O7Y z--Ws!sLMAte_gn%fP1g;`D!Yj!UxD{R=YKcS~vAVt;cHMs5Eymw*u*%L@Z5gBVN`U zb#^dQmfHh8x&)7|vTFkqLXQi&fx%(X_BcKY07LZ`lYI9ew{E~-HTNKQNm%be-jcB1 zgM7kpkUhve&%`pu*@Ork){dg@#C$aXPvJiSH_(nE09@=3_+^?}z-_T$N7R)>5MY0h z4S{+70f3*ffakEE_`md4_3Z7ppuNY2Gqn{ofu-uY@Vp2FAIL*6FF+(BW~5Z3vCbA=7-!|uU? zL;Tx-@qqI=sdgnSfvNY>1z=}rVCXAw(-;qC?SrTAa}sv3aa4Nl zsK9*GBU<2}P1xk39?~kx*b9x5fh;_08e3`F6jyUG-ixNhOOQ`veFqc;lY!xTpo!#N zC5@FEAhW4T0cD;IDbgKCXsA;FVT}dFZm1b44VAWDsm*i+s%N|jc<&KrltguDCWk9Q zxu=^*V2?us2=A;WV(hjp(3#R3Ywx{9a~fanbHXRSoHF>JnD=Ar@nvQ6GTyINTFsB$ z#BkCSih#DJs>WC%sgdD(=?g|e=|FLm&NrWdw63?$MM96Fq4~!lbN8%N-sh#VDUprJ zj3gG1sZDTmEh=XD@DBRWgjnUYnBql=S4|B{IQT1ykdEfWvW_^nUX#4v9*QrIHz)T= z;eL=JV*d1N9O$4&I_Sz$KUX9OOOdIhHeOfN(U^qtrZJwxcj`y8X_900$05p~!Q$1a zV|jIIfF_-xoAkkbP0sK`_F^Eoc8Z}Q{FuG`Q4?daM0>2cqp>m8+LCBS&zL0N98$B_ zdS(W8pOlPcH6Uc(0*z?9X@FmhD`RI@0Q6hivKfKDE*ndBc8@PC(gCOu^yg z?u`FC{Jq7?Urxil#^31+ni4gQwaL2Hm^N4Hmyzb1us7W@WU;?gvwsHJe+)L6r_A_K z*q?L1^_dZR#@94`HuouEzgT5iOijNttnS_4oD!t#emhVpnfs;3@=ws<={4vhKa zkWGmnKT=mZf#+V)TpvF#&Ap=dDqm?#@w1dymYEgxd}uE+|3VB+{&6b0z207RwSZgZ zYHqNaykn+0UN+CN7`s}@b(493<=EU|&7Wyf>bVjn5`G4h@=q5^{o263K14Y0Lv6N` zg~91H%1wGbhYch5lV1Mu&>Ljd@AeYxQ_UW&6Rwu**2Zd^Ste(^%Yn18ve#SmIiFFJ z!U&%n=~_3ZVU>OFk1aI!{vTQO@qDH_!jD+WA5Tl?zP{x=!TPj!uS~Gn8H5cM8PtD{ z+2C1cQ#QDK#G3;(lO*R?$DWFEW^>+YQR}%gHu9>`N68;<&fAkMRg>x(TdI=#r~zX= zyM1VFm|9j>S2Gjh26jaA_FE8}SH7S)Hg6sTlj*Gs@DqE)v7R{> zEV>l;eeva&Myw*9l;BQ>!`qf}!6n94ScM{EHvtvg!0SI1c(v(~!*h^BMhuldltm<% zmc+(H%}jd42B47}itj8Mk3^g9E+jH~M~lSer0^|d(r&YnCDAUlYWKZTucoB4Dv{J` zEwyA`zPi*@7{xN{A)HdFdI-{K)BsZ9@F{&kV3ZLlIIG)Qs%mSh+AYK%xDX$Zf*4i} zbV^$fmr^tpdkY{LZxQ=!jhzuH6z*exq7bd4qV%#38GY?Yww8+Wv!J0+pZ129w&YMd zjRgRp%i%~B6ZyNhtm=J!IZv#;b?L(m9KQW>RMLX(H3l;t|-v{@JC7W97 znyZ>nfpsqAXMQ3|WRWoutA~`C$<}g|C zvXHsdN>^7IlE4ubenrwY7){k}jqzqRKN4c^b7oY&MXp0pGLGt!DAN$u>_;)(0R5%h zUx-7vC=i^&M9`jSPN4aQNrk^*PW~aRk-`w<5Mb?U{-y$L#6k<2gdAunLiigDvmIWQ zO3Y_=Hn!Cck}9?QlVoVJGN5F;#k@y_d8ac;8p&pFN3+SkC#3KV%;9e3<#l!w-7hd+ zW)5jX0mK0cM95|~)3=iAd;eTEJ>N^N9TvIiiCC~~j1NezX?89Nu4Yv2snJ(;fz}InlP!*p`hIuvs=^m1maqL!K z;CFbkN$phedj|U?n_}(p8dY&0376NVM6;^6pRZx2HneoKsfq{q-k0x9wN)!s#e;nB zFYhhL{t#aW@^xh#BdFqGz8CSmQ?ft8_ra1b?|t*JA3fc$5)CRrCPRBBEsrgnb;g>2)hrm%4?>%Gm+_Q1k%yzAW7%I@Y`H^ z$2H6*y+cC6Uuz~HpM`d5tFwjnKYDQM=7-qpCc`I1&t5>?v){rRcHAmJVb(&EN*%sC1cD)U`FK z`R7SxF83gExSedl=;?MyO5S@^6a1|T}EJ^>+PA73Xy&aA!^Usmwz6YCR32hcH ziGGCCZFf7}Q#ZD0BRbdl#+hi-*>)y(x8XE2F~q%Gng>-VXopUJVtIJWUZM5#@LD{?=Q;$+mg zXV|ppoH}-Yiy|GV=_s@5a>vfJsMC>}rgED`ckC>i(mIRK3WrcTuF|HHQJSCy4w+8m zLYqp)X(AUnL^_cN*fcUulX##*q7%7TD1?!!ISH#CxxGZ-Iq!{U9114JVr*bD%W{G4 zidbiET{AbgsRm+<*I{3YRT(0PtO9O0W09^MF%aJd?}u;$RivC@LP1kJ-@2UH82 zxr$)H#roP_)m+<-Ww=!>Y+-D;PkmcUM{Aoi%NXCk zk127kwBy?tKd?_zOEV0e9)#^oDUy^Fq)Da>mJ|nZ2P3!b)7sLWNG6uYW0Vrr!sUz| z+NU{QU*(GJWb95tWL;ZTO{_LipGdZ=g)2nV>f?!9$+%tmw8m=MW36rRnm7(GsfDW; zyDMVBg;+JJ(^^#%R|}6|^lp8cy_t_>>{XO`pdm&*`m%Gb~NEY=NkVB zwr_~L4YRC%L0K~&ikvNl2UeaD8rNvXs%jha8L}jqrS(f+l(veW5&9#F-)gkrkVxex zQiowE9N{0f${zT=&DdbT$phP_R@q$b&kU4_uzn&wp(^J}!Na}r*aOWIey(uESGQvy zThpK_|1HeDl1X$g)ql8`kiAKS3B-Rret##>AM#`cYA{MwMndHBn@l5zt;H77R2)-8 z<>vLJ31;9_w!ALSq~&jj?I1JD0^Tfy74iwGD*r5;NivPtQzQeC2P-kFTMByCU9kw1Zl`MA@=n*tpqC--4gq;%4?1$i3szlYM|*<`7YM99+$k7OP;Xr#v)GK31dyx3g9Tc>l@wu!U69mk^%pW- z8e*SV!%WgS$ZoqLa3yP2;jqDq-M}o;m8IE!4<(qXpgK!vyFWft?TEt0g?rai#JJO7 z!-db1!l5viyJ+{=Nh8W0Dyap zHvxEUd0%y}5lX7OHL8GtaNe4*1p0>7K&OrBa0yZ2zlh`-2L-7^*lL`%xUN;c5Oy+i z+O@JP-X4!HRpEDpx7RTXOY3Rr7*-4Tu%HT;w-PQtZD(SLT)swgIT=UK4!*tk4Vz(2E6&CZ|sscg4u-mP<)xZzexDDzm+1A9n@av823o| zI8#WnRW&AxO6+M#KgLQ2NmvLecZLTD%=0anlA<-an@Y;>trShn?I;LkHJJZi=vgKS zr!hUolcFFVZv5Sfl`aN&NLnsw!vtVa!Yt+Lf}D1l)2ir|l5;F`NX}6| zJy05S10z7K3P)v~sj^A{m}vCCnhm<@OmSFqA2f9qvYP~1-hIe%zwX923Gn-MAHGTY z@cp{0-lV%K6n$3d+oklI1TQ_y(#_d*xkobOFLBpJtN5@Tf3F%_YNjFl1;bGtUbZ^S zs3Y}{Y{GX#3VPaRATc@);TsL^_7)=afWg0;(n>o1@PAE_nFEjN7rt%L{8F8Ia z)n22Ezsv4+B{PUx@jAnd|2yOFV?6(`;8s$wzDbw+h}hn=)34JTY@PNo)4p@1VRf^# zn(X*LZvcUSugX>wYQ7E|e_eo~*-+2T2lDp`%Vm6mpd; zk(6(d8E?n7$=jCTV04wv#}pJj{zUmH`L+C9agCiT}bZI&yI`(0tYTJ!{&+hnChUPj}fdUo=Yj5v|Wvb0DFr;9~Tkw=%< z3Fs{Kr=xKhgN9C=C++?68u?j?H%t?JLq7ATR%hW4(r7DJ6pxq0iAkrjqD^_wT+k& zwGzQyGM8wzxKOr?9{_L!<3Vqe_2RuaDoJ^*NWx%qk{Z?Gtt4p(lc?Ur;?>wvU#%7= zLIg6F@fI}1*Qv$JLIg3_4GRf?v@%2h4Q@!9urFj!a;zJaN!K@$a)BEbQvl4!<9vRb z3z&xWx{&p>3ziA$9U<#|7bp|b--WCnY)H^hB-?5zAsjU47}R9QGE`9}KC@dQS?`1n zn$MYW0@IlheOY9KrRt!AnQ#CTWbSkdiemquLpYfp;Yrp?cXXyZevlS_?my@|#~}-% zZb4M^G=$Lw7>^%DA;mQN|3=~K#y9B94S5DL@CO;1@DqJpr_>c7{E`N9j!tH;M6RVs zK^bPi+vX8@yG*}rjW=TICqGy8m$Zwb^2Q&OVs|cqQ+EvjJ^ypMu@Y54TN7CbCoLRDXq~sI# zLEI6wMD2b&0A(wR9AOoUeDx*uDk=-s6glO)4rB#xs+pHw|Kc2$k6D;29f~#E=rj#R3jz^i(S#G>t zt}n?fI>Wb|)awr9zt<|GHiz$5ZLF@XrAhmV6rJf?vbe;XPaL_E&;jxYdU5e69L!DB zz+ViLM15HeyfTZm<3$@>qS|pt1jklzj-ynK>A?Lzcr?M>a;nYStDw0}wwU; zvE?v6?BPr#>bxt_7RJNI>Bw2MZ1Y2)tToOxG)|dndvL@bI&uF6p2G9a!5^95 zFA}USQq2iTO4$cLXP{7pRWNcJsmNB)JUe(SO0Bj_-p!Kh3;`aitx8s@qE_IxN1Ypa z@kl2dn(*(xHF!KzkvqmKfcOTXK1P5u$+)8S^p#kmLiu2*R69bhVYVQgM~vpJl=^IC zzM2Z589rW(9dT9kr3=s~-cpoo!^#_y0j3(e%K|GJH#RNfn zq}mUf2n+5P?wV!;Y?njT3CVZ@fS)7qZ}-C7A@n+=siIGV6J1e98c!U(m{3={w!9dYE7oOs=wH-}OD_u=V&^QB++=X|Y50JW>JYsx_&Qfv&J{QOeRkt>& zqUQ_`BZ{FSZf!y5NLIPPjWe^O;@nCazXssb3H+k*Xg&ef0kai)RdpQhKt<060a`qU zP+jE!opc(11LRL^=zkERiykFNYqcHaPbO=PYVU{&7RIhHt8EPtvhES%)&WX=YB8X- ztp#!$n1H)aUE_AtSTn(Aj1(7>XfxgF2WtUv9g%Lz-aL~yioSD`n*pHi1j=}o<(Jw- zbCl>or=k;(^oX%{A4rx*noZZ5fND+^4e`=FUZShx%`lL49o<4W%Yp2_0lYC6&=%q; zET0In{~#EOw({^fBSay+T^VN)MyHo_B0$gm2S|U^cq5NX!El4K)T6(^NpJ>~%=O-W z=9%!vq;x_(>xWT(Q#^$;FY+OPaIx^-oBn|&F1GV^GgIS(6!U|`Wz>LM zKTbtA>4WmB=-)XZ8SN=;p(qCV8oY|m1*%efR{*x9I-e;*%|8}$(($efVZB^Tc-fbT})JZta1<`gz%WCS4Za5r{ln5>8m)7&!r2Qc$MU*Y3` ztEMr_()dbMbZbzsvWTIG#h95kFcsY!z%^r<{rFOMoW+-?+ylX^I9B=;R70vuX_!jSPC5uQB(O{$_x5iVt(E@N;y^bsbyQZKrZe)-cO z+Z$jC@72g&C$gH3xcD7)LHK(I;i+DPCF#nfLwYljF40KK5T3Q7Cfy6@Ak3p)P|SStGlbvK;cpoJOotD^Stle{>z{2{x)90|M4dDY;mvhepi+r~ zc!5e)3^3e(1zfcz==g1KK)6)LOL)Ex3!FoBxM3_{CUpM8CLr8_F!Y|42m`bvpQMu+ z1q>In4j1UsS3*$~y;3KyV(D9SY34yz>XD*<(8BQ(Pc`Y!$Ha}Iy+O4fr_2zXByD1CrO6i>wKdw6|l^JQv5-7Mn;D~K?BZQ zr=zGtLJDQ`C{;HI(`ynxMfi!9BMRm3y-dJ7p?^;KvvB8J{WJLrNn5{OepnaaD0cNK02gS|E3S`0CFYc3{EgvvUi|33JEvsQjJz=y~B^&51r`Owd6&gDR>G^qu zsvN0RZT0jYRH2%&GjV5d-r1MTHDjBWH>yxAw`TcyeeJx<>)TW)uJayg=Y@?&h3a(P zBkb6=_;MAh*RiYYSlNNV$05`}y>WitN;{>gr4|R-8g+pcp6FP8V@ov)HR(c~c4}iC zd^zXio?(_vJG_-5Nf`xNRRcQOJ2>>;HmJ014DNno6&zd`To(kr~w0nJDUzTaN6wYYT$s0{Ny0KhH9_+a*)TMY9cdH>RVI8pIA1-b$Z5SD`()V=_AJDS zF$%JQx(|)`1fIgx=zkF@xTtCn$eJcQEVDofy-=__qy_E^`;3 z@nz7RZOVUU7|Vel$eJ+2YX9AZ#3ZUaur(TE>prdlcZy; z{MfISpw?Jq_!DKF5a!T|l3J`*5+)r8KZq45dZP|M!|>fYY`iaPW`F%t{s>ZD)+y^B zM)++V77U*v3=A7Sz|P}a9l!WvgbPlScnNQ&!%}vL4xd1nyCMuKnjb}YEW+3q)G)lS zj+Z=hbeQo#Gmpip5QXqVPa@o;lTT)`l?Vfbya&R-{L*`} zR!gqib&7-^(xsUkeHLLr-2o+9PJ?67z|cj;(lam^MRksq>|%pu)H2)%W`AA-J@;;d z^LqbfwJmX1f(qRu@tbT%SfO=nQ0qb!bjsyw&guDT0iHtMieh%IOQhMYZ0_E5Byyg} z+(#mJwqKIL`c`6`4cE0Lc_Qg)iYxZ9l4uM++NJwF&O)JGpw(5;`!u7-k0{9mW8Xwnl+=Q;!jw;R zyfnl&2xFv3m<@{j|M)u?7u8|rM=^=oNkG0EN8vaD6{~;s)HvB|y(ovFpj}9{) z7%pIm=XB{Ezef0Vo&35kEyWRtC)n=bM7<}b!bfe5?N2fQ^>1{BIO&< zo02k^#765aA_MAH!#9+9i22s)G96nZyk3{tYzryFWaL`KlzVl`A=^m#_Ynm!e=e4M z%z!ukAUh+YpCCIL?$3y7S-J%q?ir&AmfvNj_e1DegW1jIXm7zOF*Kme9DI?+n2P+f zXf1BI1AmS|8>?o*a%YsmS!ZQZ>IL|Bs?hU>LR5a

2ekX#8kUbTncR{%;Awr|2-1 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�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¥fR4fj< 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�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%0A*0FWCEks&L8vIIK9qxGY;uP@r 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%0DTBguUP` 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%0D0Amg^sr zL&y%;+=+s)jz(G8_^-?f07B!?I&{&fgS^ntg&WJZEfy^S`u?EuGe@~ zFTUgbe|Of4+H1t0xV>hKAK|=6BHj0G~llG>;(dLf-ERzHWfh+pNN{2e-ETvAg0kJ{_N zF7AV?q0uyn+G}=-_Hs0Wv!Q{<_gR3X9&Xda)3oWyXw!IcGW$stPBGl3E{FW{MT51w z+w|-&aGU--uuWGS!)^K-!yYo6+Vp>yahu+5c0h2bP5U98u1%MzHl_SyUeu;fUdC6A2WdIs9G`?U0J>T;+}r^fQiORY{UVKAz4ak5(I7hku zp?Llh`a8X=;9a{rp5H%-DD<7Ep+mM?^-#<&%OK`wH*p@@ z3hU*Xk@MIJ74sb}2>80=>*kA?A2R~+Q|3m(P}5w;zOR5&bY_y{AYTL0Es%O3IcMvf z%YdlasUvkWkY2(0K9Ihz>r#&a>8jC@7lFj1I`SqEH8*xN|ARPlbk4q?L-Yz{Jdmyi zol^>=yHQ6XK(gk#h#O8VK*|Mj36Ls*TnD6AAj^UD3FL=B1_bgrkU@d`0Z1sOm;Mou zEP)&}67~pW0+1?!oCYK=kn>2YS+8k~q>_kR(?vjfTlCUzC8<^&Sp{TJAkPBniR+wK zf%GYa^M)H^6!SSphC)?NtMPx#yg@5jdGUHsMU>??*?B#reU}=Pe?z?F1@JKc1 zmbcJjuO8~7IBUJXp1`>UukBba$}LUza&Gx^c_@SllkcM3l9NGhDO9jBak+51`@i|4M55}bmT5V=IO{fAU*SSbl(9gpg0y!MW7-3Hiajw$!lmh7$$TxrtD1`Hd8$T5D z^y4v?I`!rMaNbbfaDJhc@4ElzyrH~0ghmc|ao*S%aihuottfL3pYq1te^Pm)Dip#C zY`%-;B>4rLH?~uN8W-h_iobE*_z>n#H8_+v$}`9to37_M$+BUF5&)Dpit!;kH*dU* z#EORTU6ePToyd9Pb>Ch0uQVs=%OG!zY3IDrhq@mubWLca4d1c(t5GjXs6#_Zq~I$2 zRUr922B*Q`&@O_ID!9PlV0r4}R2rP62ImYvCvO1uin8)ODXL^EqBnkZGQv$4tLGpt zhvGQ~DH$Fd&*O0iCV^vZ($E6xDJjZ|v?tmUt+6`AQKh>GY5qCs$G_girRM;0OCROk zO#X>`%J3>z+Wkc*@{7C{QSZJc*c-(D4mdeC=sDp&@}WQ;1~RA+&Izt&szv@`*wGb5 zvxV7JY~d`l#stBkzdT8rzaMNNm*sM(tmoh^=`qst0`v^@>b3YiklqJ$qj~=EDLjNfn)*c#RmgDscazG59`PTl6pi(CIjgfNHLK3qdKRYkRR*F*MU_1 zgb}WL-wz3K>W(EA7A@Mv(J~E~7ppSxIrkO;c1G$U-L%9K7q?eeHMbWQ&X%iz>ctjX z7;enhdmI(>t`yZ`1@y`mkHogCvdo#VTULFjKZCMdPU^BkwOlVtSv85)`M6(&%^4>SQ`F1lP+9W3 zsqCdTv|#A*^m<$l>A{fX#2;&L|Gx-⁣ty*3CLcMS?3unlA*0+FM?CLu&AsNei7@ zy+8(@XM|g@&ncbsT$&XM2hu@z^zo+1lhNbe1oLuGw{(EXCo#Qkdkj&=K& zIbN_mW0XB+wkD;&hCO{7xu%3{0n&wyUoZcB2&7*ipZy z{hFkxUa!IEFH(WxngXeuO-aouqR!=zo|_;q>mO;Z!8ka-w(1q#!ZV;b^gBj~6N|Ql z;+q*^3;UhYN%PTAD_fC0j{_Y^&(r)$UXl5=D80F+=l86c?w^g)vtMO|%L+LhD(gCg zC6sCx^dwr554!)5luF?ZmqU71L4(g5(6b;O4HXTf*W+?X50-d7JseR*Dx%hcLwj)E z6`9{0ZLV!->&~DomqTUA5r(w6dYa??8R&62q({yOJU#IRMXzO0cZWlIWY6Z>RiOpD zGSK64NKZYBs{cFnYL6xcGtlF5NRK&-A%#s8QLm@3m&+kN*I*QuD|*t?2|c}U1?wTs zRp9h*OLA0Rb)`u21(w(hg_c4p>+RHewoU`1B@Y|?3B+~T@LB_2I?hy6X|&!daB+_ugB$(o_Uax`ImcBs1EmW zcjyqgW}9#if2@C1 z%X6*Su!zQ9+X)Kx#u(*|Zcu5Sb+-mQM^Wi1GZAp;Lfl<*|PVnE@n6A#9qF*q@U zlQ1|q3L#stpvmCi*r|_GWpJtt&N&7LFADhdlo=enMB?KV8JtRkQ($n)4Ni{1!OM%j zvW_x1B?f1V!I@@o_BS|124}dznPPDMh35vce-#>>zZ#r8gR|A(7*gGS zP9CPgzIqu_`AZBvd44_liw(}924|7M+0Wo~8Jr!Ml!1n@<{8mKgY%BT=`=V41}9-~ zeq(Ur2IuDnCuVS-GC25&FjL0)IM;e@049;+a zLr-jEYz#9vYyF%&S|tWa;Z|ri)^VZ9sn>&@wEAO?+9z?PXsvey^uQGD;>@M$pb3AqHoav&qIX+U ztj6OHPfs_HhSfT<4oFYZ9=ELZK*r-)2mQ@O{sd$k>hB@{2Ewybm-D$%BSM`hh@#g; zz67NIn|fK3fGiiB(}7ei)Hyc*Stm-r56JeXbBvPu==%4+xfa&~`QQ;_N!& zjk)tHODh^fEAT^du04dDQ&ATxe^?>dL~m}L7ac5MDYE0&(6ghJIfRTpXhf(?*qj3- zv{>gvfE3-UBMBfIg`O2adRuhPqd>Bi%~aO2KzeS~Iqv}}6ka{}3nN0?p3yny0a-6N zR{_bsP3Jra^7MPX)3}^sg2mt3^#O2eK)y zm%biI9Pb=?_WTwIU0A9>yBjTyp(QF>=-!{f8Cb_s5jcUAhAZ)Lz)*8BIIO@p8+_K(>lVr~p#b&U&a8jX<`D+;T0DQMlvkiPYD|THB+c zxUjhg9QVq`^8$JXE5{u-4(mdTMClJfD(5l1^nU``DBAsiG4P@21t$a9x<;3p4PWMG$4J#^EE)qZqduS7|3GL%F9SfMFMGl8pv|E!E5(^AUwx* z{qskX61nckL*eK9^|GQsw*6Q~ZUnM&m5w|DWP`{gTY%&U3x|Fg@hmte02vS&^6Nl` zJ)~=10E8}YOj!q{YnjgZY&NpQ#X52fkhP-jCjxOVicC67DkGEb%>buY^yM}n%T$&i z&h?~O#MlEss_xKB{{xU7oI55TyMF#0zK-9ga#SSVI-hh6sBA-La_|s1)TpAD%l zH5NqZL2)y}NyKUyIQ$)Zx2$#`RieFmfXr&rEnE*|F>LU1=G#E}x^(2A!;$?&dmT@R zh}EwGDH49Z00{5$x%SKj(*Gk}^J74^idwt`q^E&7?g^!iSX_8x2RPZH-A_NlZBsYT zlgG{jGF*(*?LhjjV9mt24M?`g>dyihC+1pz12XDaUFwjrnDYzGg+O{mElvfJjj*D6 zyLJCMkYOTATn40D)s*bH3rLml>JNczQ|(TiSAoY61*(2FOXrPrc;2#?$YaZ0gyNh@iJr_ z$X3xWz5`@Xl)f5Bxys=btDW_2%?%<}e+tgp`}DGY2c%Qgi~Mu+IIKa$T#J6&Y8dW# zwKyBdHeq2DNRi0%Hvm})De|c6)klD=eNZobGmu#~>d5;*LVY@N;4z2|74wnmn$|W< zQg!`2?G2#@q4_az`b8~X1+qibeH)Ml z5mCn+i;+?I@Dv~eqNa0ztPuIGspxDla6YMubYLX&_Dh!O29AXCKn z6$R2GO1}mOU2dC3>f;@WT0CDDb-#@`@T%ADKLWC4KfO(V1!Rrz>bpRub?Tl!a6Hy} z!efU435AL@%@cq;gN3{o^V5Lvvn>eYnhAvWh+L!@$Vd@Ui-3$1k#HxFHKO!2Ko$w) z7eF?M5q1#BdXb~{Ivze0v%w>P^s4zYS(pz*j=~i{7K_oU3rO~I-9I-2scX`ajX<`D z(zgK_CA|7S6A%i#|5q=MlMIsW`0oj3u_S&=`NL;kemrg*m2>+Z8gf1`Y&L`w5_Ky>*FsVe; z^eS-Htz!JDHkLDkTAxb`*hBMK#Ek>qI#VNq)N<w&WQSa;g7K^f`1Hngqwdkz| zGFxc=5s)np=(YG2kfNnJazq~bg;+Z@1F4GZoCkpn3XlB~$a>N4uaVSky41e;h##>B zayF1b;pe45szkK>0LVH~_g?_X`L3?{&p?K$ED>poR@WiUO8AWx3cAog!5L`NrH(6r z9)C1Q0pq&h`e z*8TTh}DTeidO1+&IiILI^2xi1Z0%R30DG%s|-M1 zy`A)k_WC7|#cC`d&bvSgMMO>f3i_$AxeUlM(H4zBPzh}W=>U?gvN37CiKK+bZU@q- z{7jsufNT>Z{-1!16{UX+WI*2ky{P`QZDkw@j&WC^qvPKey{G;7?2*} z`HO%wh&BH0K*o#q`YDhq;lo#eWQn%;5Xc5G3Li8D{t+YEG$7r=^Va~$5p`b;WP`}_ zKLs*N%bGS1`*yE3nWX##>qgcgg0ga85FG?2h!E0YrYwXdu3yPV_#U0 ze|*Lj$gsMlm5D<0R& znhj*L$j|o!DSwJN&i+PxL1?>(%*ViK_z82UtXF_65pD54kUlkIboNrPPqI?z+3#dz zHc_t}AZwn~HBSZ7b15Tuo1mTEcgPWXE(2$K4|B+#JAs762=XeB;i4D34P>ck<AVg^XUm#<}Q?s7|8P}ytjhKcug+8x! zMghqd9vcT_;A&m!93aDm=Pv@%C+5a?0qK|a014f!>)8%uw(!{g#b^tW5A%Qwh@M*m z#Jwm-?Sq)WKfbG7aQ3ruE(WLQGTok)K>86}dqW#`?CWE#t>^(&qAeZ+A@5dVWAsi@7Y48X!c}>ocb#tFO{ICjgn<#E7f8&WRmQ^SR(WBQo}dK#D~C zbOUiO@(+C}0RQ-|+rU{U;_y#E#);naA&{{@W_w7%VI`Oqh?(aUAUz_JoCl;=<$02t z4`iG0>Wx5Fij1@Z$ObXbd=ALc9=-H8fY9Z}5B+I7kdaH=Qw<0#T%Rok=Q5U}QHDl` z#bR^_gEQ<==8)7kfDDROUIZlPNuBe3;($x#xMghzGO$4Bj66g4{0OV(j{;}lHeG5a zkd062NEFBl(K`17*(!Ya6p$>e{Ly4dd;SWfOUxV&o{s!{xz70tkSbyGl|UAvLY`M| z1u|RA*`5QEqt5@Jz)(Ms?qtcXH{J$9m$y@qU55A(5j6qGQcODZ#iaz)dkfJ_a>SZ9q zgvYi4887A%qtAjKwBRuO>)JCO$S6^IF_1!){Xh(z4P>Q2<^t(`Mlb#QKPQZmG|k$X+X+U%+pFd7Hh5DBzkuXaYRj*04aG|*YgV?!$pjZ_!=w} zH9Z2z!V7h&uL9{4el7dwy)CKYogUY!vnS264o^_d+1c zL}Xs?=*iOJ@O~gY!eb8riHoO8F92D2i*C=KfMh+PBg43S{&k}YiR2C`Y?sQ(2r_>?a7B#?C?!ruk5 zT_A^l13g#R69qC$WVVZeOc8az4M>C7yZ9k-xvQfXot_e-ETnT{9UfA<@AY(+`KU+P*StWRx zV;zJ+{~V9gt0G1`@%C&RXY3+p0t> z-v-VSOe+zJNqZguvQqSmp91N-N$0!`WSplN6GS@G+NTgG3w;F6T2YI=>oDS9uIm{G zq*C;_89?arVzmm$M#YKDZ);3ML)e!{ewo2x-vdsOSjYW}5YgZN31o+ubsrE#-%%rD zq`ozd1$}6isKrchs%mtbYk&+F(R&q;Wuh;C7swzg;>GiifQ%AZ;x!;eLh4;0J48Gm zTn{C}^Ctnx6YV|=NS$b}OMr}nU%c4(HjqNu?m%e%?DgD7fNXk3ujxlXimuR+L+2nH z|3pU$fUFT-JqySO!oqI=;kScv16LD}A`u&x0znE-_6s^QP>B;1Th#2!MH)k!6+(LM zgw*OD-JW$ovIXa#K84&ZRl|b^kbk2`~^xdo@ zd(B1NMZLxYSt{CM3Xssfy40CKkR-Ic5dqTo2y-Hl>b5!aK)~ho%K6|d5-Yp!09hwS zkX1m+RsJFQejsvh=UpIUe!yipWyDYmF?x;x(jcNPAIR!HU1~OvUeUkilO9pihk=X~ zk?<0b1~EVUGmz1utVzvi3sH-UfQ)@gFa6s<`V=QpooH=DRk}sHuLNgv8V^SuCXXYe5U5EU#CN1+w{J-JU`qa8I(moEEHg_mI36EC(n1eqE{?NKdtn zEGI-n_ya&j3a>r|WSqzVBU@p!$cJS>w!1KbQ`p-eR&%F><-i2`vgnm#fovD;ej<={BBH(qMCxe- zk}Xyf-y%e0+M9rE5ElL#$SjptN#i>}dd2K?Ob0lk^jsi=qJN!9h?oP^0Ff3h2a+vX z`Dq}VK!WmQAKU^2i6j}B$P(@C(YC~T;hzt{*)EX1=AmDR*f;}7r$BB1vQ%W_RY2$( zj(^Dl{8}84mBRBc0~seg_7;$ytMxYh!hH0dJ9Q)iWB}l^)ZkVVb5U~!at%-Cj;pcv3d@W^~tt!BVitpWdgYg$RaToya=RU^qs!| zsT4i#??CE=J)As|!4e50TAs0b(P{{m#O(DQ{$@ElFH1&|&whg<+; zyNH&>K>CE{RX|n<kOMjRSy;7EzZCq(_W#Cjx1BLbosiL`FgjkS!uI zuLZJ3)jd-0F8d3GJr9G^_Z?mH>p-%)bmWN3ke{DoBvR{UiN!)sAvjy_U=Fo=C6FR9 z_iF$`mp2Mu45UZ&l&66_BYgN6(NnGgk}aZl4Ukb{tb7(ouc+zkK&sa1rT+^EK0@sH&#lE#i;$5X(>WzT zwhPbC2huIZl(j%ciShkSAhU&3)|HqIDw`vbx>$^Mt#NsgFbSLE$ShIQ`++1>D^po(fz%0)y$EFMD%~3&11Y*kM-IOVh^iOq znF?f<7*kq-Oc54d3}lqh{5>F>#Gb=CAVs2AZUqt&^PfY%1)IevTn402j4AVhR0#|3 z1X3}z6%6uEhy8ZNsS_5RwT3yf2fh-n&{tFP= za@`BwbK~%DK(a+98GAL_OGMOJK!%H+atn|?k^P(5HjpW+bk1Vph&a3%NLSM9uIC>CvPHD=$3PZ}9(T|Zcnox}=N3q{?Z2eLx=XETs8F)R27kntDm^*ZD_#D-}1JRnnqf6f6i z{$5=w1|<7-9k~Ta*9sl!1JWn*YUp~@UDV<wa-c&JdE7JChxe$nIp31rcO zy3L1oV+|{8jsjUP+A9Gh(WFb=LsBAs))MD(o%0Hi?RV?Q4j_GEX0`9Pk+lSJ2#_3+ z)vJMIi_G>dAZQ$IjQb%Fy1eLp9Y{jtgdIS3h?5~l+=zGvmau0wkU=qiT>?bz z&fN@Tv9NhFkmbUwe+9Bccs1)Lw5dS83}mbr<0b+bBTBCZB5S$;NRP-`_W>EMVuRT4 z02vfzeGCK!YChb1DLf`(H5bTeF)~&H8711i0Z2&1SQn7aTlIGTE|4ry`WhgMRO`_B zaC~GYv{j5&8^I|T_WT>jNRjLI{SL;;yY;e;2eLv$%Nanrh2}~i^jbJnCC8?VfULet zNA4gFuHlNa3P?;To(Y@Af&DY zQYeu7fGiVHw+%>-sMql~WBd|jl>u3}PPe%M$c9UF>deZ>nUxsuwbA@G za8?SQ{CQJOE79*T3e9_hiec&JnN~oyDe~ODqd)Y*MRIGe@?S<}N z>KdckUux#4zl00mxrp0ptsM!xnePaM^9go=i1Up=(izSpw6O&&51d5Uyg6--ob#9V z1OZm{c2&H#gQbhI8n@WPa`3;^YVR;k3$9k5Skh zZEvrh6GelA8J!nxNdz!mT}cKRJE?k34b@;K{FLW~Zccq`+x+Ubx(J@wx8Z3LnyY%wgvR!^>Iu2Ih2*E* zP~h}riG^uP%-O{fPt7F<&758_F~Y{@I*p79HsT!sbbbVPq5%%ax4jZ*2b27SmO9yqYf(ZImYjtU? zvG$Bc^r6a}Ncm}%k@D%4;YfKoYCSDpnHwP=ROCm>n<^$#o;ZKTCsN=VWz|w&Uwg(U zu_AMMHliXCkJ{fkjB+Mw77sU@8wUwe*=XA6* zezPMQi6`2$l%XdA>iTuj`s$8Y0y!>*@930PpcN@m!p!30Y2o5|PCI@YdOt0r!@1Gs zc&xfM8mU3zEIq@iN%6dC@v|!3pxn)sNm<^Ws$}_$if|-%GLmef4NqJ!dRs;Y^q4*8 zhMlyC9hF}jt8R{$)}gy#h{#R;LqQmPr!2S78UNa%^nFjvOzQCSfz`-^&T!X`*_1o= z5MyU`Br%w zJP2*13*X(t7`MQ=6`thPg=djvHR0m2njHKaMt9OP5e7Tu|51+`K?e545pvV&oKex{cwe9m8F9X$^YwMC7hI z_ubIyL{!b7foH_FIApg><{i5lTj=1En@y7wV{MU!0WZ?h+J<8?YB^q74jPu~80tQ? z2{bwE6C3tvxouK%7~+LQrtX@7IZ3Ir-1~IBx4Z2t%PmMwrmL|j(?Yu!bS%1qHWq@W z(mB*8GMstl)pkVVt$0x|Lw;^^b=zELJ>#3FJKG);X?G2;Q%2fxY9mU^gfe$;4p<^G zC)Qen^9_U+CW-p~U97&fUFW!)eL(ACI_!uN*V`D`-Al-P^Z<9(>h=SgIZt#JGrTQU znrYloa;%sZ!Q$z(z}>D-V@)6a57eP-qMO69u81v&;DavlNHx}5^aVJY&(jC{^)~x2 zo1y21y}X*-3TnZN$Hh%em`7u=O)@n8&s7>`CHVLB1T_FT+;%7m@I(i$h7>2ucq zwjR2tT#F}(GzXjK&a<4q;5m{r=?YJ#TiydvjLGTnKin8I@G z&3+2X=gZm-PT)Em?m-MN|Vfonx%zCEuAW%%SklgRkV zfhQBr&uH5}CC$^8n|3{?aRb+9UR-MIj8=adKVYxd8dSNHGky}{mB2m18G|Quw8R?c zM)75g33SB8?Mul7;S!KRoTD?zgFGot?s%tie5ZYqhjfw}^b8rZ+-fa^T{4TY3-+Vnw@=;w98t=10lPXU4ERCnL6LY0P~TKFMhe(@5*Bp zyG6$H344ork2eA=GW7VW7N{WOM-X=VX?gmI&lK&R#}EOY*)10C)*bav)6hM>RC zZJQIt@qt~f2OUJ#ma?|93D^nNYKn6T$#ct8(@Cjn8cttRXiL_V4sb^*%J?5tI?RyY7<|ZHp&*yR&&yRXXfeIyVn3O^xgXwDWpH1iz1tXnl#fw)vCz|R=ddgA@=SCLea8!+qzgYWj^_ENn zMvMmcjXykqey?& zcP%|$rjMdLi3}2!-K2DH{MhTB;Ug){zKjN}024EN%;puA8#qP<6p(s!3J@x&i_U9A zMxqxpedAex#H8daKszCl(bF!i<{3Oo)7atIf;PPGoSF40`&>YcQ;!t^LIqgo;5}dG zJ=DxhOO-(a3b7BFc5=Gm$1atdU)vgof9Et9V_kqi+J~lI(@mVnBVt>7JX+gWpK&wh zJCn`KLohrkij%Q%-wWd25q93>R-=W!0(4%*qE}Y~UQqG&ncbE6Zc?VUCA@Jujd_9l z-#cYjpcackpJ-Lf%hUk3&MbFc!nMEivIh(P)T-@s^d6{h`Vx7X=*^bGd9hYZ1nb*c zo1L#YU^=)9yG?qs@ZQ4S$pK#z-06{6lT9-Zx0{+_BJ_&M%`8W00vQe&nne1cPa|hI z)N2wM%|JAMihY0pr)E0lX@z7wf_pMMInH>3m{+==B(bmf?UhPBKlYSoIwmC-Xa0i= zSg+YrV|uZ}MFDLO27k=NS-E;c*&ZrTt>(0ZmfK#9ec43o zCrfAM!6fUsTN!*1ydw^pWNHFgLbuO6|QFL z_Bo0s(u$qIu#!sLlK*{ltyv~U&dJ4hAlh2#ovu&sxdMId#>>%olt6RzLMOrFV@HvS zV*V$QTYv+cb(j-Y*VbasUcHt;->pif`v2!_#&@{rsh#sIhrd$wAAFM2xH}(YxzBOj zN0oSpho?EzN~m#|9?7~N3c{&gyaVw6v*)sIHR;K-+pyJj)p#*r-ly1!Qaqh({8XOK z>8(m_o6BvBXx7>^@*k}_YW^t%G(DdUbB(RrBfjVNPHwWSawj=Y`Q2xj?)Q@DWQgT` ziJe@tdPnRtd)%q7DKy!gShIRk8LnA%fxvl}-LBjeGp+y$`&=u4oNDf)i+HQ5+)PEZ zTBn#O>4j(#nXd~qp^TQ(8b8D3v?h`1a#|C~cscFKWH9fsyUm-%E67t^c$(oHa-%H*#t-c*TZ;v2A-JaY>)!pp5 zY4=#*tG912HK6jme)b7#J1c1fKgHdWoh^Cy?J?|Z$)Com%xA;@b1Mmah?YKRTwT}H zfp19H(YHtO4V=W}!~#5nqPOL+YgS(FzT2DYp0vQh5WL=v6BpGne5{m~AI{g58k*3u z<<4;neA0!KCLAUB3{BgD@^C?{y{a}HbL0h@&` zttwBxy__4S4>?6z>+#FunpF+uI4^m2%Y6FY!KpaUgsoDPG+MzGjQbq-vPv zYE!Jz88`_{AK9(0X`B~EN%)oFwpw_k!zoeE>^>@9^rMe2;MS&_Mo&P&1>>akaVm;adt zlj3IJH-}F$vbti@_B9#&zLrZ2pM@9RhB4l}LYJbK zMtQ#B$k9ZD&JZhYS`p!i&Qwp71AZRyJvF*O9tn82v@jIrS?a`&mK9@E=0aQTnCY(M zyeewAt)BXAUuCsh)*9>g{ngedc*?WfC%sYLxP=Eh-UlP-oemrluI*@Zzu6sYrO7mX zrHWq~a|X7Z5n!7|G01~@G5GFA+JZ6=QAK59pL)$LW3rd2&(YR?!7fAk!M8e^`sPmkorFH?n;ocgv z)^Z|JuRER9qtA=IEwkXo=ya}+aJ%Q84@d>&5 zEI^OB6#hxh(Mx>^P1Wq3DNa z)byPv;EtsIKDV9w1u9G{J+BKJJpWc`lvMr=kVxY*r>3e-UQLqG97)vHN3=IUW%Q+J zKu!S`fuI(#bRg*xS7$nlX}#}jI<5@eQgyYpk4_A1M29pX`Awt=syC)>QF}n;I>3_~{u+Ku&1V76Gk|^dyE~^-!=zqvcJbe>WIIRO===y?>Y9U}xb%8GwkL8O+3H{pcBwo=JW<-b(u);$f|P2#QPoZFKUy_(I7MAB zLSWMqZR~!*x;;{d-*ifDo95Av`mu{?63Co-ak}Dv0%TU9;D>*t(Tgc-J3&|}xYHNK&#F8E_ z_tmswL)z?<&cbUa=*3~YW8*FX@iv`xBAcc>ZmC@_lgPD z+`@B3u8E@|IoNw&uGagQ@{BeG@ICI<=>9v&etCLjf+i$h2|Pn^hL?aFNgM!z6YwdifBW+Zj9qI?mL)x`kUK%aGUHSh7UNn zSl;dnKMSRJku9^FSw6X+)u;A{tceH~zw2b7f@*VJ3e8r!ZnNPtH!`G0{Z8lT#34TH z8RG}1^6T+C!wD7NemscJJ;%q#&zDx{;jgyTuR%n4Hyb= z<|)$L0o(DJws4^ddo2aOB!AJHR+#Ph#sSV5(f&7g8*M=u#86h_13YTb_rS0X#LuH+ z&V-N7;isA1HHfn@o>^FtknchtvZry2AQUdRFRNy0%ZTJy8@*&2_ z=$zKJ1;x%UQXs@d%VBC&5g}LBCY_l~X?_}>;3sKhk3n*(Wr+e8XTz1!>I#dCOmWpD zO3?M6>-L=F5AFDGI_~G2p09En>*&yCW1Zc2teKqO;7edr82c^=Y8+oci$To&gB$6- zrgQCaWZR_VeZWGhk=!#d(^E_8raG}NEOZWUoPN$H?lEeN zsPG4kp5XBdH8P^~o99?=CKHVRj!b1foiZ)@ZU!U`m75ivd#m-1-UAiEiymJ$v@p0Y zs#~hu;0|eW-6*s5%0RBsH7!i9f0{SBGn#jT+QiZ3bwVZRxl8YC>b7!-)@$fnIq7;_ zN(J15tlch9lD<36lEHm1P&{1+fSQLet7IETID2O?8`{n3kY?I?2~qtMzuz%86)E>ka-f zoA!Xatu6Sqa|~6ss^`%s+pKpc)wafB^igY^l-&`E3XB{;7-yv zyCI(@HGSf|Y=7_-G*^*N7|RMeo!^e4bCR_@A)p;5ochM21Nuf@X>BpOHXSgH;GvT9 z(K%;k6>&e3hl#L3$HxYDf?kM^$l(jT_z+=vL8P%2ui4V%I#Sx&9ytprez!E5950QM zJkdsr%&8UUR-RfiA?$pI7tv^l=eea$uQh~SS0H~GGLx{Uv?BPvHYYM|+Op%%t{c zbS}P(S={M(h#Ip3NL?dsB5+fQarCC*$Pjj?(=ta<8-`=8bC7z?BZjyqe9H?(5#Dly)DYQ*al$)DM_6lHL@ zIOB$1q-mOh4yAuhOf`h6}=3VAEru)LucgCBXM7pk0}J+ss-SHG`{(LudNg01#E zjaadiBZG+xlBiyi?~YcZu=`8hK~bUK=3;bAFGS#45qbE4ep`JEpBN4*#mC@+e6U`Y z|CUxZy8-}lS= zL&;EJ5d0eSSmERo7!fvt(Wo|(XgMoEZyp;#Hi3N=Sv&K?=9wF*6iPUabML> zJ}pv@k8*Xu8Rg}w(_hXn}Lns&$gHK^C#z!;eANMQi8b(UHv_{QP%EbDU1>1l%w1?mLg~ ztXd|6~q))rO1(LBf-=y<-{qE02wcFb#j30VQCZ}+LEx?tfC^4z_)B#aP-3& zRPE2Bl8-6rVVFDgCZ7~G;>|#BIpFC{@^KCurws7TN$2;>GNPt>?v>O^&)lFj@F7@| z=C~AxBU3yPlgyNl$J~Po^xP6f^RqI3TIh=$TV~*=XTG^gKz`f_Hqu)V+mic(sD{ay zg9LHS99xT&z7y=7Fgb-8US6ZL6;!;(qU;yM5;-)Gt^t76`+ExcLUzmU#+(N3>=(XmYQBDIt( ztu3nw7Z>AIX*x})4zhZL;&M8AkzDo%X~_A+8eoI7k3^GG?I)>COSQ)pLDrBi+Hy** z3pQs^BC)*bY*2~BN>k$ePwZ!C_=J9TZAe$^Br8p=ldSZ$&cM%Z&C>Zf$?|O`1~o}0 zL9I>2gVbc=@eI^(cxOn>u#W87473t86RmEurcDRwN_5+5cc{tKbFSf;)-hAhX)!0W zNg?kB;`kYUia{h#zJ=83%R#!7r5lRdX8#kn&5q~Yp1(^b&9~)4U8$vmirWG4<#AHG z$`Vt$sN=76oU|sSc1=q4N&lqdBpU`#z1$Ci;9LMcoYoqdS6W|;Pd(Potc=XW&0OaI zAB$7%z~EOaW;$a(UeHd4G4H0h z612}y7sXG9wk{ANRx)j2;OtEU-xeB?8(dh@Y}?)MIt8I?mpqeH$vHW`rhs zrHjw{;_PiH9fl3#$L`u9_yvf@L^~32aRELNf_wV$(VdjiD(o+CzQ4fvT7mO9#qX?C ziUMsl6le3?p~zoiTCo&G+Ul4=k!i*LKjyxzxs6;$`1>*YJZ1Tk*pFw<@t$%`W+oM< z=3!sDWLdVA(QPF;j`!Dp0K3TsNst8CE#*|r*^Sk>69hrYj-Ki6|@)`Mx2lq zjb}6t#`#A`3*41mi#6QN0+)L??K4fc-+zId$Pd3-GNA-k2oI zRzP)wp=C5_(Ri2U+!C~}VRX5Fn{IEv=#(NzPA{`_u$;XGN0#(hj-kx{g@L)7lt}!g zL`f?c?S@gppph$2NGlfBH{gm}JClXSR7@(POt(#$Bt=Fk63nfCH(QQC7PvxbfBPks zhjaac!`2FbCvxq5dvNi?>j6e(UDQudPzr)cCZ1)B53*@2lB1#&=-QryP^G67OUW>O zA_~I+drmV*dN?1^P^}Rdo%a54`2_#}ZUO%qC~1kDQpBsp>fv>cmtNS*5){#gK~}N{ zW1v?b>|{yf$}uiwe%TI3izldC&8I82nqbH0ZlKkms9E`Ax`>q>)LriMPw*ZICKT3S zCg#vE<&vOz36TVWqyh>P5-ev$Oa&wyuXc}E{%M!|)JTq9_si*^gb78ClwhVKJeG_DV15r{YYeL#xktVkL=i<{~LG9<@N=pTVc z>aiR{seS>+3N*(Oq_r7Bw*Xc_q(K*EUie*q#h1PHK5hrzK%fC2ZSK{sOxn;Cn@ zT}Hr&AUSsE8Uoa)!)S0EGa0YIdDTdcolv)cpB~FGB%;F2Ah-a-)!eue7GI~3VrvDU zGv0gwjFo^xSBj7+fN?C#1uzba?NphD$rQji@&;V5ccuWwaw!+USTsIP=Cv;sz&J)E zsK}j|AUOllewPt5(_3X&j-jl#mdaLgrAw5wg3)d$Er4+?c%CPu6)RZ)Q!U-33t;&b zGEPCRge7k_4qIsxa~-6(0LH4IFMx4ma-}P|MRHV>mg!4bnngB>rDUinRVgfzydevN zj9*r(5x7*bQc7APrxcN?VaWwzkd^G?B9@$cj7yn^@+)|E@RM&~<*2u00`?HGAhxsH*=q#a{Z?ig8GT*t^FBs+!_%bAvVKo&RE zF=R-NozQ6lH`QY~hEn|kjumK*B}f~TMFjmqfuUvO_X-Ikwo?cYX`jdq76MXacwy>i zoI#;}<_sTn$aGPSBNTTofxHCCwGTRx05$4b8XU(=#w&1hHIic|)Ggqr$FdB~{j%0D z-3*^wgSI!8-u?j-HrzfB4+ZYw)!yB^J&G|BB`cV4yS0@|vnFgW4py=vr(~S_Wh@pR z5)t9(_J|qNQ!T=*ifc()8x+H3?Wl|gpP+cV6V(}7_7bz>NQ>JACP6`Z0r>x%uoFx8l zQ79`bVWyqZS7a-KXog6YIA7qwn1eg{dfsl0frf$iV_{Ng>Mh!#3+{a)Pd}L#g55|DrWmdrmv5q*YO!qwd9TnhcGcK^^2u`TV!{lwbo&& zFaimhl_lj1R7-^lByNwKJi~}6#es#-fwH71=mn{cmR&;%3hPH|2yHS+SER;M z_5ZFSt;VtX&}Nnj8rV>ZW?2%q&)dDfh+O6z2+uIdyL-%}3{ROzv?n~I;FblqAjqu? z+mT+kv{*sO3Pw9Z7s_+g5>@dmijq}kX^~@7T3Tdry6z@azPPl=@u2HiVc<=|Q&WkM zh$3QmhKcLINy#8sa2-AguVxZ!k{1f$DU+Z_CI#yhk%DVk5H%z$Qd(rXqFw0<6cJ!h zT_oNTaMGYqRsuT&oD>+8Q72&5Zvsvl6w1m@K%j?9dT zhcXl7IUz3%3S}iR2wi#-!&@e_Ixa>A16JW#rnI=2NxrCJ0t0$$YN_P{ZjxpBsBx@5 z)EH7h0~<=wEK6#_Or^!DrU9ocI+w`y5xNp2hPO

BPu^?ZkMNDJj=x$4ExAGQ4HN zXjVq%H7ny;rbM%Rn`M?IlU>9zVKfsltHQHPsR!p`hOWx&tzW46f5%6SWA&lNkaT{_ zk1SeC(JV``bCA_MQQs;pOR^pnGs(Mo$V8$&;UNX@?^U=b$1}SS%kAUyYvNzg6@49X)j?B&~pb4P+ftmWFIfPxJlwn+hnaB&7M zpb#!n6l$+b)Fj$dAC_T!Wt1;8BSCSR6j1(2`c}rf;0*jD(FPgmJdB7YZGD2`v?(+t zqYZ~)PV`&H+PLQM?|)%*obhcxF>b-h7<< z0KfKD%?ea|8;c&5UP?l-=`|dgC`m5ws4!I_eAhT}>559KBN=xyDyHw3tP2zDEs=V$ zQa43bG(+~5M!jVHor_grRCDiQjk0(FWAR!;z1H0AgnBB>ErS6On`ud&83@fI!cD{y z32rW!Nbub?M>T?{LJ>wX3G)}*?YX{S?rM$D z4Zj@EcCkCa3*!ri$mWA0L@pr=!-AHh>sBlx$I;p*GxK0ol;Gv~W!l0XwhCQGV$~F` zK-?6nq)k$|mTgAc+*z%_AH~%2`xG-*LDraDa>_{0GV94Bv$S^2^nsB%N?cjt7!ns% zkQ3X3KTnbH+KhEmd$vP&6bJ^SM)Q6NyVN%33L^};Yc60XaR{l=g|tSqx`?09>_S>& z%kE|riD6jOZ#vmav`#C#&hB~H$M^RvKbxpVX{E$TlE{J>k;Ij>aJzGxbZ@#jvWft+ zTaKQsuvT%+`t$f)ZF64mgTF22zCBJoS}#j0?Y?c+-8 zmuMZq`gxW*>z8PqT6~+vMoFi{Taw6vexAhb7-tP7nN{fsFtaLd7>pSwW>qp|JS)&_js2JK?(c1To67#O`7+;v@qTX}7kk+DHHSqbbAx4jbZ>G^ zEwaa*aF5f&)U_`C#p!?F|N42>@o|{&iv#@aevg|`ck^veu>XOA-2m9)Vg9^W_mJLi z6c~9e=Ih5El>9;guK{rOGN1i@`weCYo!jh#UnxSn1s5@z<$V2ocmbawn5oZ{1X5ov zRtxZTLf414a-+rC}O3NkwH9%NxzRzO> z`YS;|(@*#N#q%0azS%Yoe$WO-Q2q3{T?EL_bWv>hfnj>_V7+REMSB`|AGO+9P@313xs`w*X*Hnr_b}-J__9L zO`Q^li|(IU@O!;8O^|aADlUxIl(K@Ao|={KKb0KI85< z?{k0Y`aZZ&eQ!qXR=IWnw;DjlzY0Q{``d8_pbIaQb$?NS54(U&{{scP+YKR)@d~S4J;uIuO znfvB(^#Oc^xo;U{PhB53*4F<{l#M=k96yrwU2hnLlK>?$zUmB_*ncMslxKw0_mV8c z91MxD{Xg{W&zdCl-IF2)@(q&uULfL`Bc>7iMGQlX!G5F?e6(pL3U6bI|04_j?Hh~G z{hUGR-~I(wLJ?#e_;Sc77b|$kqEnwok^%U{NFfW5g%v(r>rBXz$A>Vtwh+4 zsTPYcGzDAhE&J&c_8gN9wH-A1nUJt9LeRXQ2q-uU9NYwP7l*}k2^EbURD$=1ez?y7 z5HglLCQGiP2R}04n67l8$(C+u#&f?1?mbNR2SX>Xf|Vl$MG4xzLQUuW#sG_4BgMQ^ zzvD586eF6+Ui${Insm*?A@t_9K5SMKI8dZwn~zeavG01j**2sG1Pcq%FQ!~O+pMA9 z@(N1|-)yGXF87>YP#71axw3s?;544D(IQ zM+SuHuG8TT8srwT=daEEMbl+V_lNVdr_JsS@)@@}(Hy4Ft-&rXS!3LN20Hg=4G@|L z37t{1wfQo$D%WvKeaB>RGe&y6mrh!cY^d`y4_+4oAxUD z69L_RqEPlP0zhf?y`NBn9~p2O^g3Yk69ENg`AC)lAY`n7B-->0sc1m1hw1)c=;URx z+)_{s!G0v~HwzKk$sqgDCUQbEyJU`cBG9WXgUrgnb<{VIMT5VtnV@Nkf2$ z(ZvBb&h&p%pfpM6WWEr@utnvmU!p}71c(@5M;!O9N!rvb_>qD`@iqqceZ1ql6f?$44BRHoEP~dkSsn0kF2L8x_i=rD0#Au>WbQSIp=A7N6%#+rY z5@5`!K^>wuOm+G#NUkxZ+&k*6VYyFFp}s&|=r+x7Amf@(SM8JvwFk#}xP!QJe`RJ^ z*hb?K>&5#B8>5zs^;{$*)T9F;85zf>XbxcPV8wK^g7X637qhw9v<u#zJZ;^tHpY{gq^2$qdiQ~`tNRIp#Oc1ccIczp~YZo zbMcP(a0b{HILqbMwi7CAk+#80#!SN{EJ_Zo;08r(nrwK8@>NI+BENf9is@r5s(rp#@=>VJ^r9%oPZkTK#{DPO( z!bb0aQn&rv|CPDjquu-MVMFKXH+fS6@WF2F4t^#(34$+nJ448!-^Lw&u{*cJ-|6P; zfJwh0JNV#sVF$mHjn}~!Z>M$mjc%fpz2_jK@Uzhfl$VE1H%CZQXn zDGH1k`?u-#_Dk#-P+4W%#a3mbPv{GF=X1oJ3$K8{>}00G0Ytyc7^d9pH|PeieVP9T zT{QKH`9L-b$5`D*o*Ti8Bq6(s1Np;ExF~JRIE3OQ2%%WbLmnU=mY z9_lE7j5+l3=fL|-#+lkv>h<^t5+=QNGk`CWO_H3@YXbg(zl|Bl(O-45x0;YYMoegWpP+Z}x} z@fgZyBCulMj&y~lup1eD%FIBFBVMGKLy#feI-L0n*+82m0y~?sx(vPEv+tU+;Klde+ z$lu=M+qd1<F$HLk}!jKpPbHz68u_x0eu&@$P}jT_Qe}KylSZ51&%!OK~@L`zw5B zhwPec>WEBa;zhqFBm5q1&IpBx_gO?FiXfqJGD`72mVeV*T85MG(x z0dR8}T-Rg3X5SBp1FywCO!xDDsuBrw%vmBuM z^*9ImcmG<<7HKee!IMgI-3g+`$Vie>HpAomDv*j82{@gB&adnJ%i;-4ynwknY`?(D z0&W2U>-MVeQFSr2F;oFMVJ~R7I>fSkJjb#XQf4oUn!+s0n!pqYn`v~;@jFpT#%{2h`e>n{k*z__~(Q%MyVgb+$wn@E(6O+LANOAwlU z{K2TAG9n^GMl@7<vsXbWMUwe^X4H1b3U;ay_ml1 zOkhR5omt~78JvgCsnH);cgT47;`RzW9PYS=nIn`bI{*IHf;xB>{RYsx*|sIoo`cF@ z?Rlnq$`lo$z3S$5wm)z7+bP`tj6HGEd;=*t=g%^(5clmii4+QXK6%^0ebTwg2NXX3 zwXl85({OT0kb|}btWei(KQ~M2(=`Mbs9xCUf+op0986}sIJdBV;KjKDkvEUV8%rXz zR~IQEz+$!M7Wa_5V1^kGF0s8X{mRkT<`%vFqJc|n;_%nqk-=JJd~SwK!7iYfXIy>| z0U~@D9f&Sz6;yyP!3nYZu^jUXfwrL^Ttnx+Y{ywrBLug^mNridrCPeZ3?*&7K%As! z_bOmI;;F#ngesh$yWZ=K*v7FRNSH?d)km%>nJ@}~FpIUkcR`6*(Mi0{)8!vbCB zC^4DTr>r#pLVuHr69Q9t!f#+7Cs6n;FA=t{SfT*flPLJ+Nfdkoyo9&lK}lCk!$#*< z@H{ZokB{e&Z29Cwa7Zr`KVza|;I!Uh8zl7hz%qJ!aA3xi-X2^@Zx0xW=JfV}lX^oH zK+xPCR9b5fEUmN$R?yjlEvW3lLLE5SAH8ou&wy`XyGqN^YgtoSJ~akdjK`zg?) zWeHe3%HB z>LpP&AzxQ#Cx9vW7|5J_F?cVZu^*N#3EPBxu_7t?7|5J_A$YcvdvpA_C_~loLrryM zo|0re-0t?k+U?|sh)&= zsV3oQgd>?Zbxzi~9ePIk#B5W5nwT;=%ur@=**i(0vi9xhp&2qRST1l&DU~Xsj!tN! z@r%#4)O|vheQ+-syjb*hQUj&LI57eZadV~nk%mM+9O7_YJHia9h6~IZ2o{&ODUDtu z9N;~TGKit`4=s*Ui4r52qKk*pjbA3G^7a-)pI|Fvu2xR*vhhqjyySY9To%5A77D|j zGJL&7L7_JzI%08L<|PIvDPm^ro@hW zugMC;Jy=H^Qn8PH=WQO|*VEO!>%(MYX938KQn>R!4NGHR0t#BP%+>-)*aB?N7Q*KB z`gPR>s%(%d!Z{OiL_3*RA}C8cJSnGAG)qbBj{{_242ng`|0x> znSTV%8+difT*ClowhtfhnfhhBM_dY734d~I&lmuk=%OzN&6YpiZ ze}N(U%|Ec3f^dYO1+E*|?{3G^W;UI@aJ{hr<-0%{xc7uKXWQ4%gUe7N&R&SJ973W8$y8y!jE=D z;S2yFQ#%R~MuSL1qUgtuNR&zvM3li}N8j9|kcvgCA3Gw^hLLij!L&B4p<&S$cUwO1 zVBNsGm`VVdBMgCFNSJ}7guUtsFKP~ilHydt`rhnyw`=zAtB1|9FKNhtOXP8v0n~aI zhsF1KvtK-~^9(p}2@5FmO%*Ic@Ei+l9r7pf1DCJ}wJxv-!6}Q#;|{ym zITlg-=FNd5Nu7hcJWZGT9IX(>5%UXXB$=gkH1o4RDIirgn&^92eMpLaCj3c3jIa-X z^|+IdlsWmC3?AXHg!Sags^M~fZ-$!sTq4mc&T_!dlmt9`V+ww}5Q(n^0z?FcKq$e4 z%+;YVN!kIP;_9dI_LJ8bA%v$|h)u-eJ_SmdD+`@fVC9?&25Rx&ed#zbPecxYxX&d2 zy?cTq{WjgComD+>#c_Bp6Ds8Wz2}dMJ;ePvma8XCyS;I8)(Zz+IJU!d2w$t6b zs~|%WnXeM$?B@t|qc(@vyNDFbMbP>W9 zh+m7QRTQc~a553kRRYKo;S0qqC4wyy4|D09jYHTH@rih=P5@aVd?wwb;{z7Sf`X#$ zIcFZABMadn=x=s#;Crq8nvJUfl#8oKgN!K*|8V$ZK(cWaIHbZ(t3W0!8r*_?vazwi zyo_65UWlumg2lLsa4u-38{2_6shGG5v|u-fY^xp%TO=OF%0!=M_ogN7D)( z5_*u?GF`GLfh)_1z>;Pna5AFS`q^e`Ma9Qw{CqoY0BijsSqmtVw%kl0Y1mBF%dKRN zIE)6-t)5Vy_6(!pgn_}hn$iWV1l_`Yk?`Nc7pV6xU>iB!76t!?gXnkg7yR!4TN1yx zjgcqebz=PNGE;LiA?Y+m_4zub2Q(<@S=9}d=y>|=3311(q!C-Df>6})Drq=j zdLPkKx&RdHcwVo23p?3(bdM-B3@4fHGnNV=7pIWxEP76HXQ`w?gkiVGkdBc4UL?{x zFi#|$GP4_b&bt*w2=K}8L=w8{NVJH4_KE@YE3E?1-^uhp`Z>)XC+(crHa+!7wek3#ZnY8Ad!qpcFYerDh1x z&q)&brSfa?IYr{8ute^CcSOKXm|bqrN$4aj15qLb?Mh~I9H9=PL@1aVaFi@aiImOq zyXgQ&%7PR~k=V|XXTt<9sC!C*DtPOf)V#|#pzE9GV5xjx$t<9Q{8 z&>|INf_Pp@!3km`gr1I_Nz!2c=Ev^Qh#pH^KX{EKE)%FmqU;By(TE<)x}SgZqs>SH zBUvYM7h3eB!_ZPmfd~?AIwUcJ{ggxPdwGN!&bEU+i#QK}83KEd(a!!NnpvE@oO)(3 zdq@kIV}=u0IyGT5kC&c9Ae^=4h`8@g5(tr@!6sMD@9q5o?cFE)+s|-b-ey8V=pqAi zK2gjdhR!j78~Zv9V!Uob9IBB+$+0rVny#Ad%I+2^*(w6gk-r8-3_`XI0;n9pkH#|Z z`8M@|a^&D1FSIng-#dMH*v-E$U{&KWD{BTL#|ZhrWdw(pyyPKRj^z2o(G*Lc#c|By zK1Bj@=y_oh$456BZ`tr?0H_2!6p6iQfFc>H4ofCfs`hLfQF{;xqER)S-gJXVI8h+< z))HdV)6c)1A6B!O4b(@W?S!wFo%l|zh@p24JsXH=1+5AWJ3DWwdDZ)&BGROAD6L?x z#K5ZpEH&a9g9WseQ(F-o+#vDpjNw|hG)EGVE$|Lgn{>3H4H9;^w&Uv02S?$&k_7R! zYEi;)ULlG%TXErBPY6y6jd@*{PtDnCfPmMalR1@!tLyL|EsI3ET0X-tu79kExCGwpwup*o#{@op&Kfrx6aIG$043N1< zfHQCn8K*@ck||1ir}>RgpV^j813U6ClrcAKgkzTV7B>RA6C1%e6a)HWOZ+_?FVYZY znf>WEL&=8z5~(o$GNdd>ffR8&&d~Jpxp@!5GqI;o^qmudyD1}R%^V2ThK1|K<8ENe zffPyE#>mmKFhye47CB-TrbLW33a6nTGrS8y%)r#jK&}ZfzY?zWQ$JdTza|iHtg;K6 z6s7$=frR4_ht&S=kRaF)lJHN_%_cltg6NIlMw1vOLF_Vrb+`A~k??dRZA~-}TNp2( z39biU}kB~7TFF?khgZP*RAo}s*dclODljv)qTDBMUJuL;1mxx%Nk=JVhIC}89 zpTh-PGr0NQJ;Van_I&y6YXfT0JLSc&eY;SSeQV&rYpbowYr)fNF?h@TuZ>F*jz!;D z-YEfzeyfD|Qr*cVBtP>X&|PN-yh+^F4IuIRmM!-A+MSZIRXgwj=-B3#s=GJ1zDnRT zdH6IF(+}`Asj^#=SQA0}Z&q2N{)1It!Dk5#=e2?6^z1>z2Rg6@oQOrq#&XfGPB1)u z&7BZ288*I~0OEQ5Sci~m6ug{Xztc%zDziOv1j@v!l_cW^s3hdtD)5TRov2bH>H|bX zIW(f4WWtDgLMbCMSHDDR#2r$CMU-d2W5pOCCp31t#Qj-tP(0gLwP=YR8%hUvI8@k) z!^+qRz{_?5Ac>s-tj11Bw1l0yL&qaTpxEoiqfKmHO*C$qYErKKf(X|U)c1CQYmP@d zHl|+Sq%rlRQpOB7vk7ceL?Nn&u(3uVgcH;ppR7!|&E3WAQF5C6zKyiQszg2!1!^ZK zf;JhAG-F*vA`c~-5SdGs3tm!0NwBKUxk~mKiEv)*q4sZcWZLiR(>kw#5@LrbOv#9O z_ym*MF|;JiICQf@kKdlv34o+Iy;06Xh#e~p+gk)mv(5DJ(LMeGW9{d!4cu3Mf7alx z_0PY-Kklzgof&5IQfSKn^W@4%Yl_C^^5o`exq(NrQ5;B;-#)+H-!xy`E3L;OKZXkD zQ1@2`74y{&E+&}Y74hr!N*0C)}Gf3FTt%jxsp9PF+^ z%dmW}V_8JAEV19>NPuf)?TfGQce*ZjJ6l4^?HApLQJ{iUkL!3|Z?WFPYZZ5&9-!)I z*PUQF&fc2%24atT6sx z3we&BEGh1ceGOrT^l?QWy9Xu}2^mqfqF~3ZFiLxX1eY?eO<^uve8U0hZpr%oSGPyt z*lb2niL@OZ;NO;ZCqShRX}Ti_&F;|{Dsh772;!z}G4ukOG~E$|hMdZd<;K@nr}6b9 zn-Nq>*~=Yo8`;-auAVtyR+bWa_=Pn&$|G2t?N38Zogg}bxS_xQIE}w2*^Ho4s)2tT zDYpL@!{6B!9x8Ey=m<6r-RAu?Zj)p)f=Vg3c|VfdydTYNhDyx3%_!oUdxqMXa$9V> zdt@DFHawZc>z{<2j_5o?Z?8)^Wp{=ctN zQ_(tilc!2-YBf0tEJeb?ME{a>4Ju__yC!i?cW%GIf&ON*YY)5IGRp$5Z#(WT?s0?| z!yM)nbZD59UvGe~uh;OUN}K6~e!0uryziA6N>_D!2}X-k^BzHYj4U9wb#_uXhVn%4 z+>Sqg)+?*ybzj9$TgDlfo}oD(9G8OQW*e%Rg3$bKOw@Zc#ojG#)2V`0Q@cJc!@;jTXe>z|e5elq;%+ju;S`KUaLK4Y zx?gY+iJToYfGD^%I+?un6Bow$a8{< z<{;iw`RcO+6MV#?(;hZQ_IubA&Hi(kA4C0qZ|F}@U&nkD_5H}{?Ier~WNvk)y1kX` zPOoBa3-Hi!sbF27HC4SjuuZzff>U16JE>wiin^V5{e(N1>p4@!Pf+g}`hce2f$7f7 zI9!bKhNgz0zm`9=yXKW+*7uLg&;{QUc!|GUUK@VQBSto_f+w5o%T-#lOvg}H5uvbM!qB#god&lbq;C1O zw_W`EF9bV-m+j{7+t>9B?rk(vz4qD*=rYs)YW{}>f;mYHRGt4PN$KyLib>H6CE~A@ z!)Aie81jY(>|<7K&l!VPZ69Q0BKM-iU&;NE?Z;i`Twm|e0XsjP?jgf)U*Ouw#23K1 zF?TCHL>TPvhKF(TCdTO8Km39?7gne6BZ`*%2$)1&q7agd@4_VjNHz-q6d-$QT60Yw z_U(nKs1qE^_X&yMfZmlM=A*;Uw9^p6sKOOobZLS`e|hs}opvMnvt!K?(g_A zmmdQSzbpjONZfefcV8qW0?bhKJ4A+}4~P`G!^VMO3m|_9haH52yDw}!3I-@IEMeZs zhZ0cW5_SOX=ALR{$xND#$QdD$%i2+_U-k!J{JQ<6(PjMem*!~)UXE(vwGM%aK=}zy zAYqtTtftFmyMZ=<;|0G6@eVJW*Zp+;xNr7zymvx^hkMpRrdjj2cwQX#LaYnGf`+hF zBN;11TF;;5NZ^H4_ON(bdmx_P9*Cv3heb-;BiT2>Oa3(p+bIbRjACf;J(^pJj2t0k ztqbxn2`Dd#88Y>y7Grd*D8Xr$$N4U_2hhB2P9=2?=HjBr)C^6cF^MhD`z4}QX`4a&Nw)x zwV7y|mcBP|w1(!k8JgAkA4@aSLC(T{o$nwVIG^HR7?nd~=VPF&XG9rd72eB8#&SU-myl|jL* zr-0(683kA_f5!Z6_PX0O`}ftuW+@QLX5Em_E)I+D^Jc$zUMuh*g_~;d&yvg!9PzroA8vjyBC`+tfg8eZo6rU5q(1JfyWI;FrQ;XI55X>jOl8etGz zAq;@C!o*bsRtN@+W6prVOOt7x!9=n386xoV{H~?JK z)LbsIQ(&IcwYh0nHe8}$6a8Rzp&cWP0ZIb|H`q?}m7_Ct+2|}=BMCe$)dLwYf%CW2 zGJXpHfq#LIv%E$iB6WV0TI~D)tapBcq;-Bk(l|fzP&&U6OY8gwN$dPZIGyt&iq82B zM%MY0Gg}w{;?M`KnMMGIiDG3I!Mz?EjHEdhaJTC|?k`CUcC_Q6!B#q*5kaMM%R!?- z%R-64>0Di9;PQZaVo|_^5w4mV*;`fEdq4McyVEcYHvIcOUafSdarNbpOygD%%QUW@ zn!N+hYuUGCLypBDx;mO|Ha$14+1UCT*Cae(WNwJH+yXGinohqtj^1~Mk@vmL?K##_ zp!V<}b|ys5<6#n$hrrY&fPg+vnU@n5AG(yOQ0QEW;=u{)BR84a2)9HA4ayG0OqDh@ zesQ3w(VBy`559+UEC!Zy&&onE=gGCCBBuujs-#86sr;dnhiI5OHb*$aLj&9ibcBO7 zC*~NB8OOFNUh_eftoEE_-_Q?0$-YrRD%m&mwCpfXJH}h0AB;pj;ejJjPk7r%)baLp z&b9R9IoAq{$(+wbFp)FBm>v|IaX`X}VAJ3}jdss*``FMMQ~L;!+ccpP8FK}8Fl4we zfDjNvWz3O&vuEUKvs+DJzY|`J>bFV?&1_Rqp`GLLeZx(UYb2enn(fN&my!sIFOeH0 znO+}_eD6{#;Hyb+yX?N1K0NH^aBA={f0QvoOxR<5coDKP#}iwG0<*Y{+0MawAlpTF zNUo!>kR12grDsmQYFd#7;ODi(*uZaJ#tuSUEf3C+L)XeNmT^_Cc84^?VYf;MZYAHf zolL|{x34FgOxg$1O(yum=yEOAw!VI+MB4Q$Xr*0WPYhN=vs(_03H4;J0mTfkx55+H z(U?_hX~P+HIyvXb1eJc9&;N!&<@DTfW>;|?g%{pj46ST&)#V=^XB)irjJa+)^o&S8 zu%e+Ky5k)fSj9(A@`1IUY(B8o)6ECgYPwvD(S0*)u(TUrVDB@SF(|0{`*N`LnEW{a z!n#~MdSVYCtS;4itXT};69+iL##Ch%i$z!Qg*<=uGTq^Z6F9>F8sAUh|J-ItT(KLw zeQjdH2kOGs?%&-WmhP!pd}9?KE3FaA%ow~qF{sz==fE03o7vcu?1a{Et`jDgzIdwq z5*7!W>;SO{v28&sZhulrEfyA>luPhf*i;l?fm51sz?^DiLlr0#bw~P{>8Dp9Zj(2N$c0B{XbuzD2YkbvkG^|@LyB1&#fYXUFy!Y<8=%qE zoGc#hFXI--OzfY|Ud(YswlM>argW{Z_GN|RSU{p-#vgg2rD4JBZ_1*h`4R^_V26a- zh#`B}yjIH|P}S_awg2O`V)X`ttR(_h32UQS-2VC}z2;`y`m1@K$6G1j3Pyp9wUIEZ z3(MoUeTJsUCD~y3WX6n#)PMonPz*ncDzID#mEw5uQ;Jj@0#)=Y+EPlNi#}4^`p}`| za7_J!rI%Z&Xw^Mg$KZ&ndwn<~6(mQ^^zpH=8@(Yd`TVuHpEUPp4Nguz{|5iKzlyW5 z%D!QeF~h8V71Ydt-XP=q{C0oSd|AYaJ!EW+K8(XdYVh769`?ewy<09cp0n&A?QNRv z^pTqn-+pNB@!Kt>0#bwMi_xUUTho!p+k$d2OajwDngaJ`fND)Qk-vLe-7cW%zGAy! zn<2m*+m-x*7a;4ZgV%0=gH@8QKNbY~pk~GjCtAc_)&pjyAYf*S`G< zSB^u5W|MoEE-XNKyy)t!D{;Wwktwu**y7A{yA4m*fasW+NUVUV-$|^1jN-a{-U>sS zfMiTZ+d`S-k!@S9b-1}vsanQ{%Kp%3{&pospY-GzG#IaZk&fxJ7=9 z)go*U8I5{qF86fQiOmI1HI3uIthq*`ZgwR}ABG=Cp;V6?Z6{!?)0`buX2%a>aVc1y zNqNtqht4D?(?4n+(x@#KkMuBj;7zsCR6hkT;-fYx|I-VBeG0Ic zoEE}f^kDW#nroOQG9bOC;_dsRaf}*R_x^I==9ONivGsu;dX8x8(Ktr7);%43dRqtU zwl;}8qOC{c7}?t8`v0MUE0I#`ZLR4{;pveRwD4$pMlI};;s4MUPNcQ^78Z{Zqj8LE z?dEC!p|(!UxQ=M+(Ky25y?aHmE0Kedi*|;WGsDcMI_bP35|~T1IsqiFScXDWFAfiS zP4ZY$^abXc2C24%AJe@eYn%ed)3Vd>&0InN<4UzN);$6kBumxYJIjx-^ZEq2@n}bE zeSf3HrP$pPl?{EU)i8~Hytai$cv>zDHnpO-hsSWAn`E|Y9(Nm^`%gJtp=kW>>9eu) zmS$7rHL{<yFu>F6;dM7}=DXcy2OI05UUFvsZC+e3k0N3Tr3lr&=5Tn~ zZQfu>^$}iqp3S$1#b*5-X#fd4UFg0}m#|gTeu4G2{>yB-KiChj%dY=$c;C(!kM`4z z{j}UXFJ`UuxXIq|f?ekw;^SsDU97{;u%V%)yI6mpE*Fo@boczany(KGh2?zxe0T{d zI0hRyW4C!~cGLCqyvKG7=;!8Xg$(|$lMy$b7<=qq*Jj(QvAUx%aT*%hp#bL=d-Hw1 zgV)pFLI7C$Z^^%|;WBv!qGy1s;7`-V65IgUlgF}PtD{>Z|JZ@thBn^6?+^1;%OAEm zcu#Ge(~lt{^{EJve(Vro=SA-hPx!JNxEE|KST5lly2th-dY}iN2r&E z^f5vIq`5qHLn=mUOzRFSTs3#A?Xp`oT^wNCg0mrT3BVus&6PRnVB#IhaPA(*`Je^^{`M zTLqpDv=4?-yE&^QzIzJ4Nv!fFuu6&wmiOmKWo+y z+bLY|lLbW(yj#8#$6PbtN$N;<-@U&rK4oR$2rEhtyVE7?OogcpjLYV+MRvz=Zyp6a zwV@0R3=MmQTX}kMCJi)|%e((66uI4Z-09|2GqikT%0#mk=q#>4ikn4&3_j2|rvlG1 z6*q9_C@Of%L}wH0l5MXM>O`GZZ4k@Rb%F|}uv2wEKRhj#OL#vKrmM|;_t)3v7RK3j z-j@fl=7Ro>r*pxNuUhwXubke^;79LB{tQmzw{bv)LRqy}iK$p=Q2L(v*F98VNl3*9 zAi5Z4-h${qfiCuw`6t1GG}?VKul0FYe*z0<4*bK%B%dd@!RP6{`K86~{vg{ofXdcmQ0KQ1Sp4#FCQsvQ(SDfycSLR}#PTgw$ds{6jBRmJszUS&t>|AVV-` z&y7nc;ML}G-Y#PjbO|An>o*Gc3QhjRhZ+?Y17!?~niY0nC94ia%gO`;9V?5XWo4s^ zjvK-1`@}s)KbIBVd4FMzP!bkA0&0}69?!30o?aEDOSj2yE~gD2YwgGV7|G_$bV0-s zGRXtw;1Yp0(WXe!&>TGzrAva+y2+X$VlS3EGKM}6V@)ZPJ7iBrRW`5k+eVi-g}8~) zhE2Kx+~|^$Hi-)_rhPQ&OMq)aV6Zq1DPWhO5Mu&Oo0#~&+5q4aA*SQA4UdSdeF}*} z4aJ1j+hoBvl}tvaEo6x8z*?t6*wSK-?dVs>v>MlqPZfNvv%whUhBv6&b=UCBZf{@{ z{CWz@xNo=JQgHe!CE#5FF#GfXheO#1v@y8=ic)Y1i|`?X0*es5#3FwCJi{OcE|HHO zNzYJ^U?s%8w~`9PJ=h#^*r29q-W0fmMc7^m6)cMoyu>1Iu?&M4xI{i`vJCYIRzlos zvI21r))9x^$UGm__F-EX!vrJ`)Mu-I^3ZAT$gd%P9h9|ylgs7wtYojMbwc#E;@tuHq0)q7d7LKxN2NJ!w#Bzg3c&4kR z6e~0+%+(IA#wLtHdSR$S@!J#Cv2GMnbR2EpufW|3-8Jjg!6d#2zJe4LEj=FhdjqnG zfGM=O#j_+Rj@$@Z9=St;>13bK%L7q`dB{M#JP;vQJw6j-45JYXQxT@6+EIvDb`Xh3 z6#e)SiBd^|h%$KWAk7dxd!!uEkW?&M{V)`XHjI=L4JM?n>Wg#Qx5eXpjTczLj1`_% z=w`k-5K4+G=$a#ycs1r?{oGdv1dt`dScaRu?sm=oef6+e=4r>kCGvPkY`fVn4vX*e zX1{n|=NWL|5*AR6n<`j@;5imBu(qfCiq>Z!ON3*CcD@p}EYgmFOXN9oW*Bhb5*DF3 z3oJr#%EBw@@lKxu$&w-uDt8h_qzFio)H%2`eRG+k6~Z`Te(jA;u1J}UnO~5vG$PVM z9vuq^xcQmzCzUh8KK#|2-H?xzIr*9J4u2)A<2Pme$!_%Ei=FC{?3{z4Vclx(CMs># zPNfLn>+pq0d@T?lA}|EPaSRV0*|$!i1WVe%zcb(J?{=@?dO(0oTO|BOnuYxoC}m#9 zaE60NGN@pn7XRIImFR(;8IbWrWa;?ki}|?k`^)Cbd}}V6cwFpZWHE1JOu>l1z=Jb~ zU#@#zew7%W)q{b~JxhZlbNinior&KTVckf6)sFPDmihU4k~;M7mOWwNTr<%; zqhp;+GQ#hUNV@C{3(c_d|E(~^wNOHW_}CDJ$UIx~BoXQ6nOQyABv^nFNC@MqKxGRh zBv@7%2?|vPP`ddtp|XXd5}x(v+3wm!52|Y7tKXYrpx(owLF3OxtFE~dUyCB*hVO)$ z;Cs%S81n6A@f>b}dAkKCM)5e0K}Fb3wuGi9FI*7(;*_B~lK=Nd?h`X;E03 zoX-}k=@RmIyq3SMfJtGC#KR@qE}LQn31JJwuk8Z~S(zzRfgqeXkj|6>$P(cT#T+Su zEfNnC?wpN7*b?!{({4Iz9v9E>E@IZslK`?r_zX;9UnbA`oDWze3ku4}dVr2>+(+!- zz()rAH5*p}C>K|e1{qU={%*e(Ma+aJppgnYNIn}^4Q_#XHm(+!mvIZs3vsnmuozbn z&IQeMV>=Kh6%#9gmN>4mutnlwtW5NILp+2n5T_9{O_o9x2nypW%BqN~Mm1qvWnoKF zUxcwTZH!3-TOuAr%M9rNvP9Sqml?V~V3DjBE^}l(Kt~qhD!*pcPB&;?RD_ceg^o&+ ztu)aNPDa!|{D}rAs07cT?BViUlE~#`XrYT_-D{!}vUFaN*RFW~yG3C3D1KG>C5Xg!;5+7!4;3493-z z$h~vL3q89CcoJSGRu1m1J)L%VNp2r}s?ACI&@dXYF#6pJaTlwkF^Vuz7poA)t7Y|F zOi#(aF5~sD**tK#@#s1WjxRJ01xJ$GkanWGh^9A7+$}0;5MkJTF~sH5--|?gSLKO# zZV2fYyMu?+kCWetBy^3CXc7JFP4MYg8o%i8WJ(|XoaT>{cus6PefLPU^L8FQ#RBpz z4T~IjiUm%{Q}V)x6dJJN(b^9V4Z1S5LUN*=2TGF*r_0@7=#vY9Qsn3~nIS|!CrOZ1 zYmNl@oFZ{kSkU&qJ0jpG%>Fg#By<{(fhZAzb|v!(j!*|tA{3J%oZ21bTTT9g@c9?@52K!}swSol>o*Q>g`) z+uPLr1g+IxNOTfLr&P=3zQ=aL`AJ&~-ZARNs$=VgFD6+xtd1EZSXau+#&dnP9mexY z3ZX?R$OQ4cl7bV&c3eFjJ7=W9eCEgQ(TE;PTt9e?Brdu2ZDi~EL1`p$nE*5rWqe$j zMw=1II)~^&i=K2CS}G|JLBhR$BxbOma;SYTk5I$ecCaxK=K+u*vnLbn>@T93#mUR5 zX9lx}w17EgIDw^86GroRIXMKvS!<4n`|c!xP8=G{ye*8+zjS*V_Ha{r`|5_d8YbYG zfG#pHmk>N{cB?6D@+dNhp>qu234!0+s}EwluFr-_=TLI2jQZ16vt8L8ASGKxz&Y~Q zpol@pnkazE5&UQ@^PV@k50oPZm-(Zm-}lY*;bAxbz5us*%*vX<$T31bu=nurl9xOL z%aJs1KD9K((r0lTv$#)@fE;>Wn8fkXjmBFx{22f$0S`rDZyKOThN{Do36-in)JD`E zM1p8kO{X{AAQDa#2)(t0*!1-CZ|8?qe=FEgXglHSWhcHt(4t*oOAjDGhs`o=hr1Q<8w1WK)15Z?1YQ!}L3ur5+wjw&XLE;@3!?pJIWsF5` zL@SbxHnc&)4%c>E{lVZUoL7<{R;m^y4CfW1c(WB3&h>=ga1f^3W8Kr`Q**W&AmBCV zWR92Jc5eVf_{X=T;0pbD^cJXjkFra%Ygs=x`E+-x*3~m~YT8UvA2!JVR~u)-fvp=T(P64? zC9=7nm$(e3#$}S#M43#onwHJ|MpBnsdN-+Gy?|^cIh-8v-oY+o*Y-y+nx9u;^^n=>DuuX|UW&>C zI+-w&BO0;Lqzoy7O%XRc8y1U&*&k?!M8e$ro+5IUxvM9TeeMVvX)b zCU-g_7$0IlKjz2Z!#JLXD9h|miy2BbB$r5qagiZqK?O&sqm_Z& zz+rwRTGJ;SdD%#UeaJR50KREc~d3e;*NXp_-MleVj+8MkpQCFcu4 z1Ze5$JAv4cBan^^si&DTq@GgJkhY2{n+MTCc4SMcFd@q*LB3MKpm4<06CUDXf&h^8 zKnm^64Q~siGr(!`Zk|{s&VwdM(`i~s@HA|Kc;ytNB*!RZb0X_If=J|{WD_ED$#TI< ziYN(I)j3zmJ|hv%i#^o-ZH`R)eSKQzHBdtAFoh`@F%O?$Qags0gc*l!R_O8DvpNBg zG^aPpc?hv%rD1!EK=HJhK0Y?~vS1i%KYwlRC(Zp?gB#61{|5iKzcM9gn9)n2Ed$Jx zDl(3|r{!jPF#FzT_aER%nfs5;mxV1GAB+4LDx5<}yj213jz)XVXvzD@! zkc(3YRz#X+JAGsy4gJvEGR&4#jZiiuzas$Swyoe?eR%m zMw?+x9To{P<-FV35>jq&U2Px*DoFLXjz{bk>pkqOx%>2hWzu*{_zYJ$Z74ZDBc~Ra zQ~WmZcH3Pou10d~plxtqmoz7o4+l0ULxBo1j+K$gNt!dqXcV0dJc6~^{xsCo38Ev28~Xc?)A)Om%?K)`8u-VN{Qbum{?4}WP>B;nN3e0|Ht(l# znn+q1_JVhnSbC&Zy)PJW*NK3!hJ zmnv!&UXx@so#6Gm0-_Rtava) zRv3H`?-7*8a0+7E9uE7|lGmSn@rwp}Wp%txtejuNIj(ZcIPKIkH0P5gy>qB$3PvNe znRh-1d*i|8LqDN6s(6o}yrzd7yg8BHe{{S?P)_VeLp4({8bPhdVZZ%EhgI<&L3xc6 zwI_t_YW{H@mOPA8&rs|~Lp4({8llZ1hjoSTpAcbHyhl)8d2i;ViYZmM`k=!EAIaz%8+LN`JMxOrsjp-H9OlPRzu(CE6V%r+A4PpXW_nvB zzjH+&LCA-tBID${BbOe4>$?AyRG5rbG8`pClLviyEiyf-q*%|x#4)oqq2gdu! zVS&!p1#3+|f&^n2!i^w!O_m+6bL$ghT+d#|bX0X!x2D4}F5KW(+tf~771J@)^1aLSx7q z9)yoswefBYUbT^vk%`=25`QK4JhmU*C6M>QC6M$3bKR6$3Ch$LMFHdq$F@@dcLhqbFzGWIG|S@tBLqMnW2ta8_|4WhCbm& zsG%hXzE?-6fuU+6txsj>(;|dguOTEt&O(+w0YfB+RJjWlg~7KAlw5fDoR_m?90A*_HmCWd@y0)t$_ zZn`BAGz|<5#{tJ9h5(AtIzU+qQqw=~_m|C=`PS6w9~b-WW^XQGp2CR#3%m$$_~qIw zA-Z(n>TU5jU*FDJp!4${3e--g&B*o_*r>cayl#I{rrh(F=4l7(MJP7ZS)3?0u%xn{ z?sl8ETPRKZ5`Zzu`2;6*FhDI<(`B>WKvTeme<|MKW%Ih9t{?Z!evY?0NbqnMIfy=M z9v9CGc!^wwbpcq=9=7NxM_SLHWw3$5IZka!!>bHIn%3 z+=*jao8YEt>C0Cft)WS7hGunI$(TpK zxBzbogi8!b7d6G1i|kyP=X7l^PiBHDJG|-#%8Qy&@}e}7u)^s}OlRu)(pk2c5_npw z2Qpv+=WnTH{1yNL{{kOpd5u6s>ij4*zP&1PegK9tm$EWxkhIPZNE+uy9uuAO8?m&` zZ;-UkZ-mo1KceWI-(X~&pP$sW13=vR;F@U!aF{4oW)a+ru0iO}vB0}n4|?fuSdTj^PFZN=8Rxpv?IBO60hWs7*{ zVMge$=5QS8ES#H3c;DMBo?{(_WDl=R-1T^vt`sSU5a1t_c{w%lp-VXlh0Y}-9-OcY za+4{6aMf67P z4-=a=AOd^3e}|1O>+kRksC~2549(HRfwDH)zr}jmZC1_fWxBf!H_$;!z#O+qb||Zx z^Q7d06;CxW^0Q>cu(1U}D{f0sN-Y*Pu@J|?rg}6Boa&|=FsB;X1I2Yc`Z-EHyd6|% zj-=k}*T82H*iKRJ9@Zf4VETtKRw z{-mc^*wDW=Q4`l@+TO*Ta8Af3Vh~*mue#8DB(M+ohBMptIC zczC{%TeLC}9$%>jY`!r=LGfw;edR7I9LGYR-)UGdyOOf#XukA;47CwM_OM5#mOY@V z*>{Wg$8E*x9RgWP1g;X+IkUL^=6EI9 zVEAOljE78&p%{J?RbaUgD#h{QrxeMo#OYn=Rz<&}Ev59i=p)6g4;@Ml$J9SqMiDnm zbx)S+UYpCvHL&v?%ri1lL2}egA0HdLJsZG3e{Jq3&HY(}mE_OA!9VV=;%uz4Z4!?ZGgEy?Z7JT{q}L{vu`Tz6}qb%jf>Vdtq$?x6r; zpLCnyEZ_GlkX?Lr%yt`YP*bRm`QAxXM@I2wxs2){e6_JjZgjNWj!7PID{}3?O?^r& zP#a<{obO>ryj!v@0FNcoX(aZWN`AyLG&;=ST|K&;r=9^jBOi@=N_;;q4X{u6GPrwW z$+On++XJ4*8K*r`7K^sojNIg-DG;`ZduHdvEdtNljME})4;hVmXfF52%Zbee53V24 zT%%Dp8;FEYx{st#sz;8tX)o4k&W&zL!}k4Q-kf8^f^{Nk*w%V zuVI=Z9N*t$kw`NaZhh+X4$vyI%=_BkH#^w zwVP~|o~!wi8oBi@rimemoc_2>!;>arTXE6;--OAKu=mK1&U zy6E*`JsSdRwG{p+;?;HI{5PHyo`x@@aJB1o3jf5N(dO`v?Sgf$-GxqAZ4KKIaRaYp z3S{1!8=d=4*`DuvJ}t6Ej&C1F3j$EB)urjoPh^Xm@Zj09u%%Bh#~$s}x1xRV(XF6u zPoQc0@D|x@+3w18?+U^D2W}sxdEb)!Z7w-%AJ6Jw-nwh@81%PWA00sP&UNQ54Z`2rP0r!3?at=**WjTBLmT%bkheIXu_LII1>La`*Vg#h zhyNG^wD~XYGp^3ZpKTL3o#CQVa~(_c!TMJJAtIN+1d?8JJ6=FC_z;uy0KB%t!lB>1 z^hH4DP;nbOFd|61$Gz9hbtt=iz5f)+=(c=YTf+|0p8eTj`mn@nd;f0^%>knvzEami zy!kKF^&e*42SRjm);*Co-OsNcmecj$q=9kUD*gVvn{Hn+-=|C52;DnKfdAo#c|Ri! zaAhuK&3rH6PnX-5X$F6}d7JMt-(R=e?DyIFunV7kp$6LSa9dE)09X4hY=npJ!Jyz; zHW{g)Att-|^ZZ9Mhlf&jaYXk%+Yaub^dZ)RC1KY49Xt@5@2cer=W*cGm-S&kIWqz@ zhxaWg(uE<)2ZBf$k8~H0Y1rHSk6ynGEFuz@u)TpN)E2V_cJr{?vdBOtf$T#f#M=S_ zeQAe)zM(>R(g|<*g-2%Y-nvIgS{q9sQ&MoG6H?4VDPW%;@em?K5|TGECj(D73F!z! z$Sw#n`~J`m%_3L@ar4{|wn|x!_GKa)=gaUPcaPA`;7qa0D+ zIB>_J=ms4M-V$}yW+U!~6Ua#>jZLpg0l5`4L&k|z*81{kvD9?r7TPf=>fF+AXWwFA zN4_lohyEc!Su7$53O4rjL}9>l;EqL6cgSbka3_OQ5boyz1;>I+9xh(9$xBg=8yHQS zqA!YKjf|9BG_yEm&J#54abq}t)}K}tiIgWH7v*XJHyHV+WkviMJULT%S`=zknFt{g zp-4q85P0`cY4Ri#qCi1yUnT!*u ztaWnA{E`R!ct&8;qz85YNbAUG|`Y}{;8=p5}cq;Oj^ zJC)<);wPU;NCjbb^2riTPd+*D(u`5z(lSzVo%1tCY{R1C=1Is!VJ49x{tTX+X=d7} zXq6%rxj?Czqe7ZI356($nWILOED5gnE(&^v zDl+OGP#pg`q|*4$;fTh64pBe;b5yFve~wW7_|H)jjQ<>3dHmkU2Sekn)_I2m$DK|5Jj%27@VI5BJmQ_0LG z1yGn&VhWN1PR>G7z@p@n0uH5=6fg}_bhg^0fRj&^6mSGGNkNTnsidHaRw5~=p(RQR zIQxi`0uEV}6tIYWQh)`l-(VN@zd!x?>pwm<%^%I=eDd^qc-_t0gBoy)>0$o7*}Y$y zGx6;@pa{V(qkaAB4-tI3TTldQ4>$ZzNExoV?1Z;s<1MIg?gI|3e7}WTY@g=s(uRx; zmQC6c-{hPcr5!o!A*8S7h#LLQBlSxM0X54;)({`ND~x}=n*I1OncOar8w`b4f#yc^ zfB#i)BN&T(K`-B4rg&nI)cv6Jf6@GPKDlfU)mW>O6GUI{k}x1Qd=)i?agML|Qeap9 zl$q4}wnG!i)Ovwklair?Cx95YvIIlxw{<`JRi;?%D^^9;VeCiTWK(K-al=t>5gCa( zk2rSNLxi;sZ}IE8i!&UWHQovx9E^e^NzBOF1)5rPwFn{mbTUHC?}JIeLhCwy*2Q`R)mh!73HKp@@36 z+lT4wZ?i>9*I8~TqMGeBY^QCN)p4bGS*Bces0d9$bhQXB#Jr{T=6F*)HB(IpNyTbT zC=J~@p-YiOEp3UQT1*>eG?G{xH8U^5XgGR^dbTvesMu0OJ=;FQ9FjvO=a(cj!Cjj+ zujH}-4!aLAI<6Ei%amJQw(~x5uF@{AdlNO$u&B33Ry59l645xkylAY|?EI2z+ayA^ zaX7)qVfZ+aOd@32aKe4~v4^Lp*7$TydT=NrX4!JW_H4Cdl9Lw@)oia`3%L#_t3%4N z<80(GQ}HW{q-F<3g}Wi4E%i<5@Ul!f>o}(RhHROZ+L5!7Er!V>mv>rO+HBV%De6hS|Lt>CC)m!(&rIhIn6H=ujvlr}(Bv!0w8-bb0)YH8OCU&{J4 zF~Lb)BB~bmBbr)2(IseVme-4QGyUFQ5FzzQil=7!VZVl}Fl`5tvh1-c7(TqNwt9vZ zOU>{KgTI!aL#kt^A8QQ57ER6a22&NR9L#R*jRCUg=^?7we%!yVD@;q!)GXm*uvchH zot6%%j$K6#XgGFwYNlpQSQ7->MdaMfiW`B=tpzGJ-boYjP(Q;bYU_4JM7dD~rFtbHU!oVjeQSg?FK`7XR8t@d)iI6R46N%jqRXwWSFaKZ~Si=PDB_eh$kql;$6)*`p);+mg6J6*>lv(?qIw30=_(UC{cIJ)=_hs_J8f0D zjzv*^WkI>~wlXBLX5X8NI6&)``0SZao@CQzzpaC{@=diJ)NG;QnHTVyqRSDMhU zbx3-4wltw)XOYzGTxmkX&LOGUk-~(AAwjb&`4R-@fz|bl8e-(k0T;8lqBuF#g0Dh} zHaQR+d}u1d;Y24mEFT`nKW`dy;xuA5@_L6SIf*$sZdWbr(y1b#+5uamsbNr@nb%Nu zn^0?z{r#j>6uhmWRTTQ|p_Q%I@7NR&;JmlS4>>x{4{@?nvIR$AR6k5I80?1}Ww9S} z$T~k{DX9FArO^8!>^81xz1|N61h_cX?zpXCEyHAangf4TJVlrs&wi__Bw%xm)l;bL zYHlNGv8g!?hGDx6o&(hHcY2oEj+4JV8CrvxSgxR?*jX(nMZaWG)d+MEL1IZx1Xnf} zE+kY5>X;0UyqpTO&`z5{nmu_%^BMxA1YU>k3feX3eSNwb8|s?1{zD4R!Ob61^V1@5 zod(^;wkp^3_M6qPZ%eLKn9+44;=ti5sh-YabXVe}VvDi+f+a?-!wbpangK7wTa3!@W>0Ob@7RZKC8DxvT@>F98SOIA_ z?^J2f*M(|$(;^SS2NxP?<4RE{E+ds?R2*h)M#a%mWK~__^sw0t%3Z8}_9bCNiOOsLY8kmf#lDo^O zYCDiDqv8Z_*QKe+*rbL-Rb*5gx;&%eu;m#Qi!B~aIqYz0w$6HPp|uKKo>6fc$TKPy zTbxC3*xmvq)tyV|I07H!85M`FCg2H8TMQE@WJGb#>So>6hws*H*wP+B^(i*dN? z1(P(q7H)F3+N|gA+t>B%aQF7<;q3Ny^5JpQJkDo})pXfxHw*e^pB7ES^6;{G-A~t# zdw8yV+n@g_u?*3qEH6RJr`>ecJT9IW2l5(rj$w=>Wp@GWAWdqXHoKL%9S>Jj3k?zB zXqc_%&uV5Kj)d8y&RtSy8kS+Z7wlZhyUo z!+>q@6$RcRywf_zB=wKQwg6j;KX40ytjhs6rcaLr*9gRJLf z;f~ETi^F_3J#2R7!({d{-8F~Z6hi*p8??#O?OKx z3iA;p*}$!aP}KS39xm8*7ep@B&v(q4!~OIQu871E(BVV-+_%}*c0B4O1+GDX*>VF% zKHDpI5|U6bjwIZ6yj((ybS8P3?q6`rv)#5wsAeD?;oe1WOf;(S5hgecqs2RWHBHhZ zB5h$3-S&u$v`3`1t)!LJR!@`Fwziit$_^sM>sm{aRdsS@Ba(}>s))y^JPJvs$4$~> zdVkqiVQ_{SiGQVW@8SvaI6{lbTVO`|>i)=ZA4V%JoWA>{g{!2+2sdGdZH8|Wj3HUu zg&4MjNHL-f?mdjN4(;Pek`eWqq#H~eh(0I68KJ_4D34C8UG$lelnm{Rpy5|uP6EY> zNxm~~5`JZboLpH;xKPX|y$LWW$=HM}=R~7VF1v&(FJuCWlaT>sL`*Yag4yD*_&#s;i|2LRI#M(ZOQ>l~ zD_DkTQkGaDn!WCJ&HjD$uvyAzeDbxz)& zA;BUCS|p8r|1@3hCA33Wj(F&|R_D-(1Z{&B4$&M-k}1b!epo2^)*XX$8F&JUA6*$O z*in{&gJl*8Z-f#ngZH3ZHw~#c&Ze^$TX2-HJHIW_5DL>i88u9pG7X_Hpw$8{K^7XI z1r?G88KtBhs3T1}C*vVdmea35ML1$+VS!W7^DaUhA+zgrw!zqeLJJv8CaM7igHM1V zWpoh+X^GiF95J&n%t@GqI8tU4^~zv?A!T$1=1?uu`3Pb*U55ciXk0_9`2}TqP zB^yxyS~Q{{)$)j9p~T8$MgyQ#3aH2~JE9mGLSeF_fl!z-4WTf?5v4PLctk;XH%wsX z&O4g}by8z^g%&ah$A>bL5JSpHJE9n~)r=?x zF&rHPdR>G#Qq6+#LBJ}&kTUWI2mv1-LCnS*AS7%&1jmMS^5kLx#!)7NEyC-VKtNG3 z>ELxt@VZUS;%+p`L%^j9wE@|9Z|U_1Re`7*8!0< z?n&TvOpfH9tshAj4>bedrW%S)i;`QnjO_(r!Q)VBX)>{uy_z>Yw9d^?(@ z+v7&P42w~_R+7)D`ct(4&J2C{qYK&0h#r0BEA14Fbif3gcxjZYwNwZ>}QA?5(wsQo`Ur zOsSJ2+(AkS53V0^GU$m)#T{HT;>6Vx6^hFwu$md|=F`=~ z>r>%`6gU?Sl>M2z@L$T4jT{%!DYaSAIS}tinQFt)=vg zM^_u0>Zho!VmzkWlLM{e!(6$Tb zX0-KSnAws&b_y%+$to>{!TKc=mOM5NrYujH7#1=OLr07bv_!Pa z<6Vq0l=P8B2lGS;z<7p1>LmrtHW$cJxUk3ELkE>0ZlecvqDqP-L9Yat&o~%ItPfjM zL?#eKpB;&W)53YQ-{C0p`a7)ZHU~K933m^%n<(q7I0pHW3g>4;g{R$S)hh8h>yv3D z$F4Uzcaq)aF@8kML8dYu+f!UVl{itt9OSn-gg}T~l{}UJwBx!``i3W4RJ9{V!Vp@b z(#A*)#s}EtPj1u7BtMJ|^hAZ?G98z*7Xh1`&kpa~dG8`!1EovACCy`st-3W7iQ0IP z100L|-WT#`3*1eN4PQgL_3W{!76q=iMNZ&)szhMh!^o`%VBfI+g3&+@G8@N zGg1&tx$-jL_o5VeBAAL1B4kJ-qQE~@7jy9NPEwx9jpB}p7`8zCQg)r32j)n!%|jvy z<38pqLx8!I1w4&OzB7P{K`*{{g`c^{aET!=4JcMjJ2nM_E+c_{5ETlmADPO+>S@Y^ zWqgnE7taUp4>zy-IoxM4gS#@xxIub3-YNOD3GS7w!ee%?5kBp%wH>UIO#iF-pRXxt zTx3g$KsTDGWX~fq!c&Bd^c1Y`dnxh5WB<7OQ_}(>{61DtGjpg#bB9Q{vvG*LEWS)$ zL@mnOZNb;?gH>rYA{WN-s;OtZw3?>aE%A)KYe@>sZRfP6*rgaRX%#89J!cleDW@s{^TdaQw=mj2#6!Ij$!04s#Icy( zfOsqAm%%DAWXC8e-!gKEeSK&Z8)qyQLRBh4mR6O)EJ~_WM+31YBXKAyLi@U0xHOh! zNM9tW)n#%;_c_3u4{oEwXSl`vL|Yw($S|fc&grI)H`t5?&kVF1lYW9##t~*r^Oy@M zq2K?st!j87W)1&QjezHaP>7$mVXa<*F%5A(A(7+zVrp+J6K>yw=n}1z+Wj=jX~+nY zjA?U%~ljDL!6fYm^4AoWk9dM4!h`aF;>1HP$=+4$Yt48v7-_+r!_8 z7rMlMqKL;dB^F5AKJ*`J$}vS8)s%kt^X~qLjt`7@!;g~f&r><>d*zEm^szx@j=V^o zsbB|w#ZzG39(s5h_17!&jOyJIy4gk3{+EwfU;AO|CyH=HQyhN2DO4gH(G=f)zA029 z9MKf3pKl742uC!P#rw}Ug;IonfBN&+e|&11{p&+>*X>6easQxQ|M|D~ z%589Q5@by6LQuDJA`9$pS>k}J)8RD#>UD`UNxBY{fvTM4W+;+^c^WFcq2)fYz2^V* zn;R$yvvUU)^4RS$1yD-$|5Nv-&21#P+Gzin^Vwl)v;FPug*{iddwLX>Z^w=KQbAEA zxonE$kd$T5uYZBu3b__?A?*_}r;o)-01uEzWqf}9Ovco7(i&{IVF?P}Xz7&9^7mI@8h(AA>DI;N);s#0~P|(HAGsduVT`k71 z_PQ12Yt?V%l|ssw8Z>mcoR+mQ=!h~NHYa6CxQP4K1 zfncACZj9)Zd~lK}t8(bbLJ%fvh2RW~Akqj6M|PsJaN7np-S7JyK8YLgQ#z!011+QS zwkb!}n^b&Y~IEOk%ch5a& zQrC#REE?}2-w2UwWK5`Q zp=8ak{56l9Mh9_JrqiLg;*AcACmS6UfScERX5jPj7M#m0x6w)Eb=n>ZI^jz|RNm+W zacQFi;*v&(G7B4>fLqf=7dJWqx@<`ZNy{3Ypky_==mbh%e2+DOIH69wLfw!w#5iHW z?eT0G1(&{aWyqV|SyWJ8W%fqGB6~}TxfuZRoI!#+x{Q>)i{6!arQxq^|I7L3!*z0H$4$w+asL|7dh!%{ob5PZt zkHW8R-?{U-HhFW4I(BnVFmVc8L704$%rbuqP+}Dz{4&+)WEDCcU13PIaI2ixXh$cu z%E>uK0EH_g zw4OdSTnlWcO%2pSB<+U8A@1!4!vXGoNQU{bJnsRK*sP7?uE6F<&)!&}r-5{7uXt8J z`_Uaoy3RvZy4#WrO?tB>^Gc<^VnUmIAyelW7Rw9`*cZHALYs9f1bH_P8zU%u&7(C6 zoOu*nkUok>WW8KqAs{_(p~w57u7Q_Xw_38Erm(bYfkfZ7ZDHfu;{cN#udYL@hhgtK zPj=Fk>|-6eow?tt!O=gfZ5QkCPa@rKC8K6^t3KJ$>AiK>`u97pD8JCkc%(26gJt&n zShOS%s?*dTUxr8~&k!S4k@6GDTb@c|4YAUg-kstI+uwJR2-7Bs)+wHdoJ8JqNmerk-Ltt~le zl8S7!SYmO37E8|UX~Fc=o|J@t22x2|b|cM%5C&2{)7B9tvhz99@}nmb86g+Zld8Ze zr>QmP@3-hf=N4_+6X4Hv|QB6C&{6<{WF`~ zbxz?^*<30ds#I2qH)YEzccpw$mLEmQ=?$V%bXD0DsZX{5(?C?Qb;K*w&Fe^`_^ykpxQ7Xj!nFeC+iF}JPHx#R{YOVS~vsoM#Q0ecUX>%0Md#2qA{0Q6a>u2ArshkNxuV{1fhe7s7J5 z)BqCeg-b+a!g>v|7`qyXsO@Ta8lZK`XjhmWG~ z0o7mcy6$oP++l`j*gRfuc26))9uEBu1MKzfWe1BlaHry}%m|(DB~>C%sVexeRQ2bR zsY*pQRTZo4vYi@=T~mmt`*D*7wzkQ)dEGjhWOtx!X6K;%z|&inO!u@w}t5nrGK zkzY=20Oad`wX1W4k=}qP&|~CM=n5kP10&xS7rPo~(9{M%fgLoDLMv$U8VFZQ>JSAw zj0|dETr9`BC;*|!YaqN?T5Y2M1>sNwTdm7944S+K!j;-} zEZ*T88Pvdd`?UDl)mekGsDffPhl_3Umas5T@?+XV z&)orDI)Lqh()qv7rven*FzgrQ+hc~@KqzQ`m;KWg&JYy4iJ`9nF??0-QJ{dJHNx{9 z)W|L*HKGf!pDkeHs&uDM5*zXP@JH#Tv=zcHA9kB{c{?=Z210@P4U@uQm7zBv@(p=$ zw}2hr(r~1y4S)hGXdZ<|(BuX}z74j2&Pxw42+|7C@3znTkDaK4QV14{3Mf|%b`fkI z4Zu_^MrbU8MFo`0%3Xtzz8=KHKCIp;EL>t&NV!5zrq1oV(oKUrjZjX7Qg64Lt!&Dr z;goWy03veW?E39$vF!1L7VLNxA1es+ssMMj*luywSd54;sDT0V0-QLO!x$N~fKk|F zuQZAp$JLRt+!m_q00tGdfYsyDK2VTWh`t)uFiXT!T%`yohXz2HF}M~jTTF#YFo714 zs3r>8JX(NZY7SjOu}c`xS+XjW(>}AH@$S)uDvpzcI8^}CQB!Q$6`v__ zxikP&VIqx2u&96n@fZ>jfu9VAVX=aKb_d5~_lN7{ys)23Fsy*7)oqJ)ifaSFP(Ou+ z)Zmaq1rQV4st$O$Tq*#8gOnRZ5m$)UA+hGTo6VyF3^8lXc><431tg={Hq4ZES_N;Gx5cNu3+ySyomNGt!}7!X z1v)Bur&Y2h_7vzS#hq5gQ&(b>F$BNy|8;xST|@g2mrZ^tT@@^-OIPrTYZZcQ@fv7> zwiwjw)d}4L7wwNtH(cv@pY`Pa+f+&ykHfBclpf+C3^kd2A{m ziOV-~SsX@yrGqJ#O9ddk#$MB7+B_=2z#eN24ntl6!P2GJk6kVe09Ck)Mk83Xfl|CC zVSK-4JOLG5K7Ufg6?Om)ZP$?!3@c#jYHy2mimL%!azI2~A6M>9^%>Q`x_B61dmE!_ z@zFZUq8^GIe2LgxU>u1>@u&x5I;>(Sc7IjOMh&xS=xeNTxvO^*i$?=6b&jIYH1HIn zSE<)h77Z*@?koxjFsOksJ4bJUljFPnq1!yvMVBzMmhkEv$;H>eqg<9XQ7Fi(rS;04 z&4pI$b!WosR*M!+7vfcAHJf3j%LSP=gjeNoHn{5kZ)WqWa5s-pmBrqW7`NUuh;vbf ztzlL=UjS!|m=|DM<%ThSt*u$%kO7FbG4N`PflZt;#;?kXKGRB8nVWOX#n*eHL+Z+6!Gj9Fy6kHdYxgLFeTRXLNQ?Ak$uxA61pKce6J3$8 z8ueyH67fcDe&zmJCagleOxIQ!Sfy|O@|}H~3#P(6GV`HM@K#u22`P;ENY<0XWOfwP!=^%gi2LN z2!|RVLZK=kghLIGv)Ea(DsU5AYXHxV0(gRJ4PY1+{GkRIZ>t9wE~gq`Ffef06^?uY zoD8sn)yu2>^Bp|Lhj`j$;Fj?Kc=H!xD|1cNi@T=Ie`|LLO19puvGX2ct7BL9GTMn{!^lYFJ-UO+Hhy0wkwCVubk73pEf>p5Uff77D zQ_sV^F;%ILSTTYXu*ymUyXaS%YB`bTnkseEk1kcR(N8B;vZ2o>)!T?XUQ~xmzCu*V zqk%_5W}_zI1Ng{1x^*g8#Wf00M-@PHTgnk{w z@`HR%frmNjppCCMGF6 zkYC3tlG(nFsm_B@zf@{6yH__L!>P8mwZqpd2h)jqwMfqP%0{DeIg#zb&|acm-KvI8 z5?a;U(lBG9hgMiGG}B`B0)xrBYuhSWUxTAlwN9iSK>#nQ<^}B|mY=5)$|JJ}Bh=c{ z__e;rQ&-zy(gvWk-^)DDm32WhY%?Iy5wQt3g08XQnt294;=ygB>PBe{!WXMj`PO-E zfn1$l_vC`xdLG}j?@-(EwI=ZEPP#z$sH@( z1kJ8UTWbL*lVQ<o)ccFm7RFKM7JJ8~cn~nb_yt#=<_|1_n0yZiM#L zfy=N{lb_}c8*~H)BUDREeC^pe9ZCL!EMETa=9Vf|t+Ad>J|ksZj51ZJ9*$+S9D*Mb{uh3RZ+r4}ZBIcWv3^R3jO()aQqrZzant1gf?VdiF1 zn9g}p>lW%(za41vTN)hD4)jn3d;$qQDl0pXi-T~ikAk2#B)VvDC&Idqh4+MIBuZq_ zXjJ)br(IBOCW}I~BROd%YE?AAnzT=d95`5P!^=vrKwPq@PeEJ_VYOj3m6D)R>8S() zmFxpa1ggE!zy5ErELb-o7xyhzC%g<#?QDxZ?hW-r|Mif(p#B02+q31z#jZQ-7QHhk%3iUE9ZKCp`{!yHGs~+qpb`6g^)Zx22_d7og@LJRp+oV zkn$@I8$QW1P-Y~DNd+7yQ9&6ThejsQ;oJ}VPtWV+0j79W0b|Up@dBJC9wJVR@opi>%l6y+MshKB%I^aa zkj%|d3y5;507SEUyc9=ymrDg8FHgqH#3?>Zqj3Rw`0>ajwu1ahsS#i9a=$W2oQL6Fvn#snaRqDAhI^2ROqzov3HM@ImLyiR+$ z#9V9vwL)EGg-|(b5a%+P(-eyGHb{*N#zytTX}>Oq!n7jeL^rkBxWnq+>qBn5V;465y;F1 z<>oh8n92|sa$|#n`LAJu7;5MZi2MnWO8-=51)V!eS3`jgM1DDSh5lo@sWKCc^aezM z9wV1RR~Q)>82P@SGhLNgLQ@+61$NLp3ay~YYaonix(Y#z3~FFps2pE~AT)Um1d<-C z5C!2-1H>d3D}-U_Ye4A4WQ7oJ2xUuDEYAy zF5c>NYzU-^K?Mvyms=ScpHBrSlMk*8iy=1<3VIfkO0LjN41EmqU*61x0))gwQvfY(yK1$D5y;``*j#udW54+9!fa#&N{w2PznH1K5(ZPVoH>A$W zS7ruHZ2%NlLGvgyf+jZ*@@)VY=1UJS2+|7CBokpN1dBxl6rGB#W(mUP(Ev=vVuZ$0 zy&_S$YoHPu;&FE!EAm~Gn-x)RP8Kd8WePc&I+*0KEI#FFB$7#%*AY!5Sq7JKsIYD* z4P|L`3G=D|r?Y<)A9NrLYGA;;VEAueeyYx-z^pnQW4IJL_()%43zcTDqDtfjbluTH zSidgyQbAf{0iEWq+ysO}10WLzZuF6mQhRZ;bE(5FRW43aUlm)|=22l^!-#2M%xqd} z**kVMuZXpH}U* zGmw(Xjx?_pW*BpxS9UNXqY7BM>6^sqWuQDZ6_CW`8=XtMcsAv7sQ{$c*lR{UHjfH0 zCi{C?MH%u62$n7tYdx}#g;neJ12v$E$+yL#fo;mC-vmj`cmgWA9D^dRumf;t8^se0 zD`0A4x5YZe)c`IzAfhgQt9Dn%!2rqMgD0;6ltm)Kz`!Wb#yll}C~D|yKeLf4@3>KieUhK z4G8ndLWP@HJQ{$ha}GdYta7*jtA>`Va5o!R zZHx;%7E@LtFfSYrAAb5>EPJm(gcux+NW>|ZXjtsEkTtJm0ehs^Krut z{It6AkVhjcwSXfIf;9wGJiReSR!7r}jq+Fg{FjodQa44}wSx$+JG6v2veOX7%O%ky zUGe+XAG!~(yALx+5=q`~f?&~1VVWs#hkgLcL7aR+mzwZ~xNW z9?o7rY<4e;-F-J4;Iign121nZ9z_Rxbl>KJsW6Yse0Wjmvof`q^!pkx*280E;X^;$ zRSm{*oEvy!%`{%6!ia2@F9MYU$9dW|;ZC4Pj6N z!@M_F*y-trOy{>X%G3?7WLG6t0`R5G@*DZ1T2+9Ib*nopsBUn5^^#wigIg@>Y*P&d z-xAc?)Ibqdsj4Evp$3Rhs0s+-Py;0LhT#h~jf#m&?1WmqV@F;^tOPv4waUWqD5C1g z-&PMWTuwE>U|`^~D~uEYP6k-PxdHO#BLb@9V1U?9H@*~%E#~w2-TOcOy6t9fx^BJM zJuOydB6uyCX`^8~V_7>*Fdb`=rneX;S7sbru zk?^9X^$uzN-~~_5n>R5nUImVBa6y7N`c7#+EdK#Wi(vt_362F2-FF-tXl%XZSgo=9 zfMOj$^c3=lqJ^%&dar>UJhlbRp~rlJb=c%%h~w*nmE61+2y5Zt6DrHidVknGFAv?~ z`D=&ISyXEsdRNbaTGCFA!Bpr!KC)MhG4e!SEnkFb-nFY`=D$m4am;Tr?vrv2*jZ1x z)ioOLA+|bp{+n16_hs=JF8vr=rDd{Z!^qXVj@DeZeqXG$JrOj#F;UoH@b6j(()JLosHQ+s$^rmeN@TIdc3HPmwbh&l1BrNhRjB=wgXQJReCh=k$H6MR5CB?Sa1pkXJJP9 zLkay-4pplFPKZrD81h(5C7uYUd`RZymP$aa_>f0jY7wc2Txva$zNJzLX?zc*l974T zq>>MLm!y)D^{_}SFZwbF<(2Q=kSIgH4r2L1KBvIL9Cgsf*EJ}gg4i^^kWtA6DH#e3=~X(N?}I%_BF2|RN@Kp$%g|e45}A*?Dq|*Uio-*_RA(? zBF(JHz{or@P#K5vGTzq?3-hFj{Q7kjXl2N+V-?A4U&mDE!Kh!;G@0G28<62tTie>< z>y?A)M7>%%0cSlNoy%!rWlgVcRYN!VxjucW2XB}$(aV(Pm0>2+V)a@ElXus)RkFT@ zC!gA|123uO1??l2pQjPZBeMr1)Y{YdwZ6wwS3O|T2B5Ux%RJAub)h3zli>)lR}+3z z)wXrnj2l1V!Fkx~MrjPp7u%#F3goK%x^*q4K(5Yt`#l8LdX@I~soGULT(Nwar&nF3 zg{_Q51(oe(Og+8oN3H~tZ?sM~?OP`2Xk~4Q-9eHtw33~kwKNG=Fz0W=#{$)2c-G!t z4F(iotu|baHlUMcW3ew-__`RAJ61X|&8|pWYXK;eVbOwAz{sRItOOLuP*};4W+|-J zQlu*sjouk#m6*)rKD0aZVpi^Ktbh^8CReYZV%Er91X{JdNUpj{?{X87L5OoBctJI| zTpO9tWs6{|ZRs*?!O~8*)ySqc(^e+6xwf&W&9j9;1L>j~SvA19m1zU4+t@e2xP_7Z zBuI^H>@#jcujrKQ5{**P6a{*EQw zXa0G$+AO<|!+pQQ%bzO0k2Pqk=1UU&km|0#J5G}QP{8$!$CNq*Z^7j;=e{^^@kicW z|2dv!g6diTsxX~Rt<=KAFDI=4cD|K5RM5~wIEDW-IL50kkT+rGI4ma@NgGqxfi^$l z=*#%keEiHH^^Wf*^`IQ$6et;ot^E2Kwd5247tt6yE^6D$kXO~}Q43*8AX z!&5J@#UAyBx=fsKwfwl-tT*S~-)FP4{kQ$0f9jSnrv7}s-n_uAp-`zqzqtQql`QXSTCON5^{)SrVN*$xIG9HU)V*?&hPt&#q;W*g>BaN@QmkX zJs+f?Fxl*N|DWgK%L4A?A8;&ofFC5-&3!K^9i_qu#Z4Skh*ui#+yqKT3>IS^D<0t< zLH;@qi!KhP(q1ksChsC?xB2k5`(63JL!_a)rGL|6ph`m3Ee>iu_51zevH#_A`St5; zrYbr*W>|uZ+4AE8(l+iE!(o5DoZl|sKXZ4y13#tn`@I_WyTz;XbEOjG0}%jbXLpM| zcspD>AJ&i8Y7ZB}WIces0quuc{JI}3+bI>y=zNk04y-WakQ3|n*-KW3oQ zLy;(57>rLA1{81V1A!W*ofw%I#NeRXt#Hg01}r;gZ5)Rr;#8ETJ1if%*@)TU5D|*O zIfgSP)S~f51>y$3R>S*h(Pj-C49ZiE9*j`eS;OY^JLQo9!-XO}v*8hDL7f15pir|D zV>OpgpeJ^Vg!)%gCZp3dG+qbkxr^0(7Wa}VfuaLT6%f5qw1Doeq6ORv3HDD>YSj~o z@2_CUvpYO*Uzzy9>6XSQ>~$cKCQT-j8C_IDPPlm_#Y~fU-4H_?k0vTcXRx#5W^y3< zDn^qUZjKed8OJd4pQAzlzTKFx{Nc;%ZVjmZx-|suPn4$_Zf~qcPf1h}_otD|VRkO( zNgJ3b^^2$LK;FIih@M}ZYRIArSb;^S!Hqid$#U&rEY1@_yTDv{Ud`o;1UJGJUeE0kinhvY^Mjpv%zzr^3=ZeZ9A)-Xs6(jbI6M;9Y{ru+l_qUtdPU}N|Aw1O2 zhj-F6!v>Zh0KT&(F^8={hY^b+Am}ZK=s;M1``Tk^eF%t z2mOrmNb_0#K|YHfTz^)7kk9&fvMw?HO$uG@VYPGjO(53=?cV~0Q4hDU{LL>+T8rZA zHX8l&Xk_{e;fReUvyEqw5K-sb=fixdDn|W5K3{sRg8pI({&Blesw}_UDW`XT(}ls` z`}O@Q7zW;h)=oz$vkc!}iIt9GT3dKH2JwU!(2l}oq$pIqlz0@s$_IaYfEC*Q@Gz`a z*E&ew9$GM}gazX_zy-nnVYPVNqXQShav0UZg2#j)@l6w8E~`ptq9UmfD3{rgd2l0etp+6}GBaGQq*QsGYE|T}VLv{dwvwr8D7?(hR zY{qVc2<;gdyMwYoKwN(4=5RhxbQr=unPlZ6KN=Uw68X^#GxFgs%0wRwiUVe_COGi-!zk_{BCd~$ zGhb|RqBE90)kk>V`~i=(MVy*^v-!e@5iR~Pe)fjgp|vQOCk3YIB%e%Bnq`s#x=g*4P`VBy>*19redbDN2{l&6u z3_goYC=hPVK^SKNBodwTBf433VmXgk3e496=M0eH5|4zz++6QaUc$C_5?CQdlja2`N|mW0_WuwJyluMN@nf1#CMdD!E*6~NsEND! z9SZ)6AXAK4x6c~0P%0x3kEt-CmZ~VaBalz0h3b|6kU;|LRKZv4rV6N!su0b`UN?6Wfzpzk|eh#BrSng7P1u&OYCA4wHzZ}!Z zqY{j})?W_Eqhz zDxz3#NnHt`Tz9&&z#O5t>+xruUXW)E0D|ONej%ACIqmI z+O6bjNq00^G(=0avstbuWst9_**|6B2GfW=R&x{l7SFeuzpkynZh6Zm7Cj3Eq4;1) z{J@n+b4jSn5j3sEc(`6&uW-tXgZSh(THgJ}9LI@k2Vo2>n|SnGxh~#O7oJ>~9`^WS zd+MPB@lGnIykq~=ULaR)Ig@&k=MBsOey2D(Gwe3gjo%PLu@@a79XD9<5WZ80?t{W% zd*|!8X=ww#G&_vW@BS`eus~po&Mqgm;2bb=6U-|o&b~Xp;+i{j9L}%u*t`3yI^b@X z)#uvnU-jk%gcJLNT3r=;6VZ1lU_DE{ zjAwCbcw*P$3-t^wGUwE;-3W5&;izYod^odNMK4yODmm2ZAWohV4A7O5L901^N)?s8 z?aESK{s!woHY*6SUWZ87j|WFRGFTDueuJZ;-fwQ!T#9}?4eF7>B;Njwpc)T++C@Ma zm=)W<5wh`2r(FcdV47+FM##ovn|2W(^SWR74GwDB> zFrwpF*i>3#l^Jpi=%sEV^#?>rdfw4D_4v&r$7+#E;50QgvCH!!p-IAX= zB31@Po)yb);^t6RKB=Cjz;UGl%yceO$3dTb%GW?oeZ)DYa`Hll#2^=%T@}ZFeMl%@ z6EiJyV_M0vALXy6tYI&5Nx>YhMJ{3dkS+oc3os6+91Vi#%&A@H!tFSFNPEOtO zQ6{Et`OGG#Zuu;^Q@32s>J&zOt>E#8TK9uXg0(EvafEs{HWuc(EE$K3$(Gn3pgJ-6 zK)`ipWULVf(7CfVE?vdXFL%n$q9cAoHaLrLyL10s`D%TjA~K0Z z1ap;l(5binx|RPNHx~cCg`lDejvG{l!2L-VMMk(k1t;7pd^4Cbbf zvmW^}4`nr*EKW5v=1)-ttiYnvrX6+UljYjMSez$9Y#@4d%cq6P&DDyE1jmRiN`*E& zlrLqGj0PO?vzq0R5K-g(mO-v*jSX!l*Qy4S)oh@@5FYAhx+WDe%6MH@Gh$H$1dWST zCaalYX@4`sf7}jNkIT(7DJs@!WYiG20n$}NJ6WvrcKOIik1u|SHPEsyGVH9W32JN?uLv_bPX!E~{O ztbL|7^k1|9BniDI4f4YLO$xomANL!0S7dCt_HU%{8%Xqaw^)AG$=(L?11Wa-yrzy~ z{v<`E8x|M=Wg0+w zDb4;(7lvmkUM5cn=pl)7-K>am6uirq`yRR)bluxSDX}FUnrC8LS!=D5LA_VGgFtdF zU4>AUIVfA`m^et-Dhu&Vp{77ruIWvora)&24fQD);nZ1mU8%;Ey_L#R_wpE3!g7{7 z%V9Z;YGJu+rWL?kR+Z4krPp#yBaccj?$T{JB$rbqFnWitAd13yFqt|~!+>#=c(JMf z3U4nh57!6G;xxS1SD|7b2|11z)wLBdu%x-aZ1PVj-s4Ayx4N$)c+<-$HgB=c8pAz) zba<=VSAsXJw2qT3%O+w?Gsb)TNXXG9>b8*JO|JskxW)QmjQ9A_;jL!}D2HBMv3ZNN z!5HuHqr+R>DG|8og%O*#Soe$Z9zQy~)lCk;n_knfc|WB5Wc=vxR+k$DZ<_hc<}KC> zW4OnU4sUfgL-3{-FKpgoO)tiK{OIsj*A)bBSk;U8;EhPH7GqAn5(elUH+lh0{V{&_ zI@QpgQEaIsl#D`m&iQOueBB$tfkio;F>X zP`V~Go@~m-5N1;dX?k>ffVOEqV$jYnhFrkT}@_b;jK9HsouNEu~PLYSFT@jBaB?Bei^bw-5^b- z+h}2Ot(k8KkX0lqxQ;EVJzL!rs}d{3`Q;iX!6_5ixMI)m21c4pVS4SKFG^G73X(er zSwkGP>yYnuWEUb!piP&lq1n*L@oGYpDb8$-@T<;{WQwwOTv+i!Q)CJ5SW#}#O`z(wdxRV`!M~bC`wUecV%3Z6_EC=IbY^h)F9O152 zHsvT7mP~W0tzD}k;wtesbki(XydbU;@9m3#^NBsFTqqu+N?6XmNI5KrQ7tTYSEK@% z%c>IExH(deY2;A}#@!Yvhvagq1mhiT!mjNoYB^(O0%~`ra&!=05`P! zmTqR|YpX6gRA@^$6lm)v!JE>SZm;HRD|T6{Vlv@Spsky!Zc1Ca;hV3mxPdyRghPS0ZhpQgZRw_XzP55}yvmg+ivoT17CZ|(hGifa@0Q%+ zL35I%kN!oH^rehw(X&txiVv22lB6#!(Yn;6ZUm9v#!DamH*?!3QeIpDA>Wt-TQAAp zh=FAjkDe>n#hWYFrO&NbeYL+k@Xb8~)GcR%c3^_36%SG^bNF`*ll zaCgHIp}*=&95z#o5bo_>ad^m+f;J3oriJRj#h|ysY}s<-%DodUZ(_)67J>5OZbjht z;ATh&TUZ{O+*OTigV?CdriJtKc&)+yKsEw=FEeZQ9{O>C-DYQ&!|k8Va<}6pf%RpF zFuM3l%emJLsFcF%1vZOtRg)#C!LoORy6l7X8DF82^Wo~I{t z)zyWR&(U2lZNSl6>8$5TRx0b6d25XIEcG%W;S@iMO&tkJVHe;gJg|?VlGD_-03>yD z9@wPhu(VI=rlYV)$ysQh)XnT*laiy(KB?D3TI+}8B(qQI<~OjVl7q@VshgU>CMD;P zeNs0&fK5t{8T+JeLIRtVoFeu~-JAe6DLo_X)8d@N`js^%h9OdLgrSb>6fjI4?JYo< z$_SX6!Qj|wG9w^^0Ui4atB9A03lvd++(WC>#N54HbK$xG#yGIX0~b`fzjYhh=a8pF z3+NI#%Tm=~4s#ZXOeLvHNi_#9^QG!JLp;S?O0b?foWU&~JVRMfLz;y-)0nRVb~5f1 zg%wYbu*j{jb%drm#epa||6%DM=SXngF5hl~R36qVCoUEooKUF~c%{n3d6vM8&D>b^ z5}w`hNX@ad0y~gdVp+&EGsz;Kip(~7dh2WR`Q8|rwX(!c&1`d+1dcvYWw>1?J^n;} zpztV@&7$;BQZ*NOJc!t%(aD~Aj#G2Y(9j}151viVG4pvQ=9u}+Cg+&>EQ51QR_n1Q zSmZbgbvfqxH}>i7WHlcHJ>o&!D+7Z&>&?eN4_=gEaHr4t80c|`G7RoKJRbu+q)>*T zXkcpjBt1G%hQVDK$hQOC&@aQ_E-~a|pxf+a7~Hjrd<=B+y9|T7=#h_sZb_G6a92gu+$BbqBhsyRIOqFU!k${BOU4!H*E~a@PsA1H zRXszRN0TGWs(Pj{i^d2e!R5^uT)Mk&>%~*AVvJ5a3ZpC&iPD9~lPZ6+h~{Iso0I{* z!3U%ZX8{5t0*JWj+x7F43cjqsJ6YaTLrRF-2%P%%gt+C65MtU(t%<4WpiyJRBIXPf zj~MbWJoOzU@4Lo*$9HNe^X~bf8xH+$aoFs{2X~)$KEqaXzc}>WW_RE3{BGcFGsebh zv}&IE{eJP-L(aST8$hzz_UrlkKmNMyW@mSc{jluzhuyG#41vF001;%g0d`lb&2q7l z=iiRUeM*71pulpq+4t~_m+MkQPg*R*^38IyT0xB{lYCt4Kf=ROOLbj|2Khuu!+3Pj zcOn`U_(&i))bR>CdofSaB0Oy&@1<-J8*rq4@ig5c;@TGSN@|8o`AE~wsB+`fclnn!+|*9eU5}P!X+4@ zEIz(=-sb>P(zOSGx?jUMNvR-A@;%@t?$-dw*`>CG3B}<-Z=@KPrEkKZjwS zmLK&BCr6w-S z{4Fn!hAqrpQJrYs7*Wn$8m6$f@zF-W4zx5=09>BQ84`%z#-`v3I?o&o2(!5~Y+;!p zSO{}zxWZ=HTagY0SJ3O+O-P%7Ed~scwf*?oA~0g&F$;P;MvtOP@p=>}&FWE*>vWHz zOR<$n^eDht#|(=d#X&4)=tP1wEg27IgAthnea|?dUwiI&~lh0!fy8lJa~VkSNDJN%=e>M{-Z8 zbguj4LC^Og3|f=nN1F1e(E08OU}ElByrUTywb$_qR9iVie^_6~%YY+;#bS=)8sF_j z2miZ+ODQIW4PH<=>#pH=Dg567j?TRIjsl%T=66yybXp2_aOCkQ_axE@43WlTj5LOV zVoZUST*oU8F8_2uyrCEJY6>pe&>&AXV&*yj!By+cwFJq0m9e-ot<0%pD*`}In zD;1}t8v}TaGao^`aTfA2;9AtqJJ?Sj*86^Uxc>7F`gwFW_#f?7mQ&VpW??Gy0iC5ix$>OzFka`!rHqO~2|oeW0dOyR+Upy2V;9poht( zM8br*L}ZF=N+hm~yDK)XK2Tta4M0e;yYNG;9Wog=faM1TnKfKh@R=W zy$*e+lt^q>EmqdtDHY9-NEFqJ*rg}uL*oX0i6!&Co?z7yT41bE3N5w_P--?fWqgc_ zi8fbbOWLUyTHr0*zwD4UFbVS3C~wXV@)4EqBDlHQSXJLe?JU79)t=3;x?lSK^Znx6 zsZ}o%-c)^DjmP7khmYbUx4(XJRF|I>if?HG(cZuHi`}VfKVPs>dmLAkhULJpsEU<= z)*ui%Ip68nGHc6>QiiY z9kljhA&ixR(=Oe+Tf=j9#stkZywFpNIk-4f%QSM9q4JWY=6al(n^-Hy&{|WQj3lrtwTO!v#wr&@iK&D}i=#yo@Wgtd9&G%;R=%Fj}dhB9H#8k=HC`Y#xGlq)X90_Cy z(BlRMh}3Ghj{vmGa{_-5(S#|tGGaY(tmU)d<}+o&4K&=4`gqg1zqBT*YOM8NgOFXMv!a==r@=uIW8rDwP1ItjSk7Ae5xTnS)3HY0bh!M?6T zyI&_5_yaS_z*>zStC1E7Z6EvJCsFvD$CL>U#mL-&bZMj%{4*;7N4UH}8W!>{`FD-v zM&!uTY2(|*5j19k5`#bwlw@H~69&Ljf&_Y6Y;1cm`0n+7Jmr~F11$P6R?tchkxrW1 zCBhSoHF$&cJiJIciMRN?YnE0Gr34(Wn0vxYi+Qq5o}cHp#N;ic#7&;hecI&tJf}@w zx)YRT@peK21}|lvuy-j-hPm|=TqSpA_m^}P$+vyTEJRXDRZ`4?4+Gwb-u}+->NAP- zXa%~sAf?qI&bjguR8sBWP)evl)pTUxtOwOvXI27uqS+XJ|5y zFE>Ea(lF+z()T#DTPFM{>Svvy#Z@`7z+Bv&=`(6s!dt4Z8mI0sQVAvSu=dj5n=Nbw z`GQ-jEe;7Vn|?c;Go^kng(246{?>U1LkcT8M`rqM(Xbf;fSVy7s9`{DY76O(G>7d`S&(O7K9_30^TbB z-loWca70tUbLHRL6j>0CXbN~W{d=1t6T(0K{MRo(|Jil>=ezD&ok44OW>D|>{KI-O z*GY;5!zH=-1Jh82)3P=O9Z|*u=AwmZ5nhCHtzFYoT~ynWRjyVLJKcJP?YQ;+5&%hYc81Y?Vb#@uI*US-mI zX|^)xag)q`_$Eg>zT9rTW|K>7KM=sg4K7_}H%(t^H>5AO8&VhBO%uoMHlAaT#2oNC z@tmtwRg+X8*ry`CO@}2}mE#d6YlYwpj9bzO3&)e9vT%b4w)gM*9lU>OS&rI3_Xcoo z>gVPz8M)SPX@Gzie8eiNJiThk5c>!@R;V0R8mY!M=L3wwIh}!@93yh zJbxUik4Q_#HYpw)rg76Vc>b0RgK0r6CXj~aD=84gwVuSF1G zHO3ST%^ez%;|~3>9}m32n?a5HDBder8T2K}dT${!<>5e=DbF<&l9n4vkFZM(rRia~ zWxYNea}5_gRx?%Ehz9Y7Y0rQ;8rRuAB0i!TnT- zwW-H*GnkD@OtJu|TqTr-w0XB;(J}m|XLE>?!`!rFEB|qZO!tAl3{vlrKC42~a03tM zWk`0P#LFOcUb)MmWX-RfMhA^Or_o{Yj7BH9C)x=VU^hA`yCJjOMyCMeH98W-tcim>s)80`z*s zC8RY9oO2Xh!ahoUWW9)BAs~Hjp~w3{u0T7CD-5_v;;o>2P(UW#VqjS7Yp_hW6Ro zl7k1S$VQ7L0vj!soX^vO>FGQv3I7ZvsBqr)wGAU&G4)dL zQg4sxRJ=A_B`*KCX_>gvwTVJ$y6+371a)d7m?qP5Q7c;|huZefY;xB*g->O3sc@)L zStZ_-Evwv>@-ZoXIM4Qme6cI(rh3d)k7aWDwyl;1NglH54i>O?ft*H8RQ7qtX6n87eB(>%)0@0(aA)KI-O>k z-MQH^(fo9$FZYu&E(G3Qr4z@AF^WoJnF~rqE4rWggCN7JS~!k<>HE)+5q*w_CAb?} z$F!KPMmcu-!9dAj?GFM9QxjOX-B~_*0r}1QZ=&i|%P~D_o?eq(e-0cxwnN~+pTZ%z zr*|wY%bpoR^bB81rdRY?;LlwSB$vSL-g(ajr@`s)VTZ^UH$=XeM6yi%><|h3blXaX z$QOV)!$*=kcmZ-a?5|(o9O$t^B!J?F$WtTZhR9Rn($$y@k>3kcGDMylRGSd_GM@^O z5xuOikGLO;R4jgX-{? zbq^!ux#TY^ajGjnhw1zFdA&5F8j*Gb1{EJLy=ZzE;L*+n@F}uOF|eaT|Ga}`qzu6k z4m|O$vt-D1NqGW8XyIC5hTI4To_MF0UUCF`v^VzAJ4m4F42Y_RssBoC}h z%LMC^m4eN416wNF4O}udSbyY_;Ril3&$d2YG2Sfex^lVZ_33i);=-9&t|-Avh_mcV z=!)@XH8o`?BbOT(d^$E>r)M1DP6t45#Kn$AUg*RmX1D+}HRN*8j?Pd(S%?;7ve9}` zg-aP|Fqx#rhcfU6+=wgH9q#BLI9V?qA-7sw6M^TJfz5Ct8!QlV!Frn~Y^>gha4AQG zYOpbB-xxg?xks-?q{Z?TeT{q^lGeya`eOND|0?;4o+}@2i{JNfrFQeJgZ~ceM_ift za>EOkJ~8wtns{<`+z$tO;tyduXY0yZm+@ESGM8ycWShbhSlYss^(j2o!VP^A=4!QB zE>`{J!(!Qk3E9I=y&~k*{gmGY|AQZn0p-`oHgrA2m3IrR^4*3{Mh3e z+b>Y~4W!2JA9oA*d9#M(?d!w!op8*XKOiJnvLi_gK6H_>&`BicD~@jeeH3phIj;B& z??R|8dv%QiYkSC5`i~FY?K}0q$e`*NlJXP-o1RFqYDo#j-ii?xpO^Bb(jU*ux17aSn;7*{!#;9Ru}T1gwV+X{}WeFmp($citG$@1;ilJFEt z?E0bm|3AF$VrxRDhf!z}T=x3`OmY+$B_>e|vESPy$AVF4lA}~la48|)aKI!-fe|+e zTtMl}A_%^PQP$HhKfyNRd90IYDpy5V{`zSwM)!LZ*$<%VM8D#M8(Q_EPlP=~?ABADMe=N@m* zVAPvg<#@v8a{4&=GVB^ZC|0I%p*sK98287)nlLV`xtYV-r*hr&V&qMKBd%0gYXcEq z{@;S0TLC9>nIl!e4~(8u0dV$Gw}4F`qi3_=ZCM{WfF2jHJGjb37sI9;{>K8#HLvoJ z{~hN2vA}x!D>K-OS#Dm$84*}J1s9U9n44F%@oi#jTLcE{c%VdVf_%WQC1*rG;Pc2u zgwqo3&1M)#VJlz=4AV`^^LBdWPBT|*lDtehF3K>K4oNrU)6)H|POjh>C0i!ME`1#; zQ>I5bEz>z+$#<#ysaq#N>S>8|(ltsSFe%QW7fEz_f%mg%@ZUu1yrmFbhPdaC*)vzVf8a&leWJC4L{ zTMl7{9c-vbXog9X2T|;nnYzi&=jmtyO!Xzdw zw$rtmyt?^akzvb-uH$?cWZorCzIV7`eZ0&STd9g@Xw3`#Gh=;5_)wTk*l%&UFh{=A z2P~~SoqW@JY@&DH`8@5&nz{hb=jlY&qu+wBLB@})k)wq4#;NC}GS}Ln|9bczINLti z&H0AoM%M2FjulzIE3#x{{VvEFM%Lrqk>@ukH?p?4I9(B0?c4HvHJr$rxH2zzzItA- zhR^-#+bg`WQhtFxz-;j+*^527bEZz51WZwrS%Z>g?2UsZ=EU+gROzO z#Kacv8Bwl1;PcFt3 z$v5=b=Cjv$v&qegEVYc4>3O#FT;n;O2jp21$Q!ihkq4?iOT3~tRhJ9jrH8u9lJA8t zyq8zga4Mu{*Sr@rvV?nKQ|_1LH9e1s3gB603^Bm~UdKfB$5~AL3x}}Mix&>rF5z>4 zO)87S8pG(@fk3l&guNHw_?{-tpjU@>F*)t-W1~xh~|@q94$wA%vJz*J7?FZY}20 zJGGciZa3v@C$Pzdbmt8WdrA?>hhja5<1H$mCff+b&4vIPmt59{8LVkOGG_~>T1i{s zD7ioKMS{k4Sve*|o+52UvZ}+0GYTp?k-8#ngZM3U)mfFeTr!J91s;r;KMWq#4g5%W zzJq-lXAVd^hcB!sLg<_Q6T*&t2|gVeT)G<~ZQFNu-`0yKO#UeM^vvSw-_TCoB6#lzZza0F z)0C&)H{AJ*(EWM_7b=$~bB#;->umfCyI_|BFeE7z=`kTW<(GbU=nk9i>H~Sp_xH_zmCf1tdPwcR;>g>~@=%`HRnhe3jyt zH##MTU1P&~I1G!`@ZY`@HHq8##@%iMZbd#{2S4lc&w-EL4z=rC-eCl{fOby0(P=9b zaKUh4+YhJ9NLNag(;hipCc$g;%^(}sL4$?F)1R)FhD!!rHCS}?$)&XqZp=RxFism} zM0^uGcdE$fzNVxbbpk$&v2AjX-dS&3(x__-fei7O$=@L?zV=RPx?T$$MSq6yuRy zh^WELN&)LK(q5-(OFUzSF3p4NH6(oiM;tOv334(*E$6B!70Df&&xG8-T@d+f8&h9* zp});h`;?tBE5rrZ<{*9?APplVOgqu9Xzd>kXn&_1=ed&e6^H3Ze0eihyc*8iZ-mE8 z-c)j2@$7w$h|)*Man6yHrx@5YzMH}k7p%dM39BU~6wBYU39FXaF$wE=0>Qaz7qpT# zYPS^}SG!_&DqQO~Fh)u4P6g}!^e_rdlHLG1nMsZUqr@bWdvvEU$+2J*n&c?eli6^< zBu9Y}H;KP_2os3>4pDC&2K8+$BS)E{J~vaDDj5l7+`Uemu|l8@r&bB`kGu3S@G@)+hUUIm8`ANbHoCmg$$*QnDLz(&T3rxp{U&K5PKeGD_MuORL z-tKd_V$>p)JC!Dz&Yj7hj;oNZ#Ad4uhHWJ~7*HaXZYVac^0D%UhpYU@%yC|cm1$h4 z&e$?%?pcINz(FUp9O1{onlP?)*Z|_gDXd#^*dT1~|1Idb6>uV#IZ_4u!00&@(D$!GW4e#1ruZec{IfJ8mAU_h#|R_I?j1Nic0W$#JfS(%*G| z+c?A|iXUr}n~&Ss)NH!bZBW$jK4)xq8>!iK_I>xD`M8}r8(~ju2nTl1^+t5D@D{t# zeBthn?sVio!_6X9tL>8Dq zpH}PIMe?COPb1%2+XSsou~ImhPaO`3r`?}ygy;$VaLm<*ANnF&xX+jS3D&BVpSwQz z@Z^N;t<_oZ(^GiNG1d6776`e<|M8ZxZVzvV&A!9Oo#Da`y-@@g*vu!jvuXZ@n`Qb^ zn)&VO>teOqEaeBh=H`EyUv;-|p=|52o4wifj}Qv_UAF`#cZ=oc`OWWN;HkjN{!70; z%-`R3!(4A18@{m7`p@nWu2pp}@LctNKHT|?@xlSHG<^3Ep*2^xH1sO}?CyvCcC&{o zRtm`8*Hqk%l8crG3%SBK{x)9SGrzq75l@>hJ*wo&$8Y0JJOwi0x35$&>fIOD za4!#(x$XAf*314LZVDbg!VRSRVFS5kVgzur__o{i56jIn`1g2Ik8(cm*Y~kc%jaF} z(_Q}<`E&{QZr01l$Mel|49MBAju2hl4REvZTjc9GJR~3ewB1HNy?NYh?(c>em^TlL zVHN*;)o&gWU+)$P*0a|StIgu@F!ZaK;oo7TOcj{T7Wbc?_h}~LO6?Lj#Z2%FK6Ur} z5czzy|2%A8_K~k|_8&Jd5W^y$-^tHI)a~A40aP=>VD@^y93UzW4@1nCE_cJ@$5;iP zZGEOt+Q(}2{JxK=@$PQ3SuIxEj|*|z=~*=q9$cMyBH@VkSE4Ob0yq@P)zX3%H zZ2cB97(&NB0S5gEHbBn1zt3hD!uer@b?6^AyKgZ6hhb#D-!C3}IW9iG^kdVnvkvam zcKz37zda0a?e^vi*izzwHCZo~iF?yGe~6kj-$!TZtnaUV`rl?U2%cT3ZzK?Q_zuF; z{!x{9t4dgr%`Wqg4+j_wxgk?EMR9BzA%5sy4f$yg3W(BP7y6_o z82AY{ifKf@c=@!WK`>th3(TKpPVl=5@zX??P-X?-HLgl;1` zc30MNzqrQQ^t&BdYai6U*3vjm5< zvkSQDB_^8=d_T|?_ia<0Luz>9tqnNkK6aoD*Mc?OYo&rEI0WA=5U2%r+Jx&(j-A{h zjU0$}K0)YKfdF);dkUJ}E7JzN%{GMIDdED8&p;gGTU~5WMW;j?1u4^zde(PWx#*O@ z(ajwnv}zR&-w=McNI(T|Y!bA7pj4Y0a2bcR@XAm%)(g2)fUfo$wt+iL-#pxtnJx~6 z$rm{?F&TE@JPiG+_e>}O+5DhCQxu{jBOkSR?FvB*pp|R00%^Z z=-Z3#_NtQr;JmM5VGxjf(Y#6>39gw0+E*DhW)J;21a?beP?S1_X^yAyj688ia}VL5 zn2Hy}BP1+eeCyWzODBhFI#OYwSQ$5R#LB7Q85SfWds2$+1~DNTurY3|pzVjzQdH{w zLY?=3-UN;Wfwl1Lqn0SakH7n78NVr0j6V?d$&XYx_95dZEtIG^esonMUyx&W-#slp z!(qqG6CBFC@0MS>|2)G<4LIPmgT4BDJfSxJB1+(SweHYAZC9`ty@%ZSn{6+OF5sB} z2)%G>>7n0YQFv&eFNCc&o6pbNQGz~> zqOwdQh3r1y04n~0XG<4%t6t?3H)le-#rBJIlo9V1yS{sX(|~9>Aps$JKlJP6<{r*DX??owZg_$NfA}~26I=mZ2SyERX^ru> zvBcBn-uk1{xMX1&XHZ934Kv{@@|%T6eHONqpV8VK9w*jdpx*H}{NZWw?XFiCA2usl zHL^Msqf0DijIGc6z7qM#x4X^u)$9#4P8d`zOey^Pak*R#cQDj4zZ$>5whY5eOfLTp z-jn!LA2q_Z<>}# z!Bx!1^)rr2&tAJEcsLGD>*>+LSo9ovHz@@wLaGgI|9K7J^|L%ErIgDi5r%wx#`v&U z{^xnvK{UY=zqSEqS9nszGQ^kp1wg^ll*Qd<2hE)RwCh*>V&4-R+~(N8#$hT_*yt4) zVEEjxzP-X4&#e<_WeQ&DPhCt|MyTcgZgevTrERzgvzar{4u$28kwSmEK55;wD06}B zGlae`!|nh*ovy%ps5+b~>7S_qtn&0jgy`S5ht>W%nx_Eo^PJ0PDmL==)Afh$!|QH- z`=(1xm2qf)*?pLGGS&a_g(y`)oj1@VR>K-zIRfmn<;MlQ&9GYx2hdWz0zi~fSGtH& zFkFnByYh58iKJ1n-TpfIz(6aNjx1wT2AqzgOtsS;RmiB0aRrXNHcod;Nm{XZV~94c zOBnURX?)U`;BvrvvwMQG==!yk>zgICCK(%!F5^_nz{@b$M;}`s6jPxR?1}8*DK%yV zebOi9BQY`R2H9fjRib0KgwJ6TlXo5D1^KzOx|D7O^6Hgj*4t^SZQN}taC*2#3Dfc= zH&lSz!&K#>w9ZPURn%C%NO63jEQAyaJGFX+0kTNDnkbnsusBi{i8^V$!B`*uce1Ke z;!dg}w7l~EAioBcxRa`g57!lsQ1Vq&;!djKU$C8o5A}($_0>0QBNNt!`UzpDRxdpYYpdO9owGPn z=DP`qI%&Oe2G9+wjy-^GC=;L4o?1|8FYdIp$QVF3RFOS^ZYWcYV^xPSHGn>{VA#Ha zAP#9KhKFx>y5V^Xd;7RVlG=ERE=Z31x6S#IM1m*S=(41leTS&t4R(g;HsvQ;zIjCyusYZfe%wSC64iE!STUWFd%lJItUj z$8AhkZX=8z^rP~)L0@WzbGJHUa)Z89n?W%{lEvy}@of+1uHazu7j+2PW&IAy$?GzP zySa5}fu5a1$*09vU0SA7blJ^jP)eV{4~TpL^=Gf4%<~$ao`ck>V1y-HYI|=kwmG(9 z7Z#jjT02br>n)b5dVQ7F8M)+Q%A&{wEBx%T5+wRUp^HjfaFK_{v}jR2@H@H@C z5)(bK+Mv%62rnP6HtwxpgX+z2N>t8-KGMK%;SQj#hO;x< zxcZZ}Exg^Je}YOAy`Im6bdpx-u>%`_280aOA`v<4mh&4CY!Ssb7@JRWe@C(==K%sIGK3ymbgMOFR_>$t`X%xA@cI z(1|BA(DB9waI+Lc2)IcPmpPr?#%_SCM3<=o;;ejLYh%30YyIA$(z!!dl6a;&?V+anR3h@q}x16ui5c6`hZmr!r-uHhuCx@?L= zK?vY=H$W|p>c$CS8qfq_!Yg%rn8;LDL^IV#hOPAvaBgG-QC;}lG$iKuhKVcs1vJNp zeFyI%1smLe4@zIkMrn%c+q;lUNIa?rK{UcL02^{HO|cyHkMRx7G+Xd3!;8TWS|5yK zR+#q$#_SN=;l(pZegTOz@NpS?#m$t$K{?!&UhlEDgjpH(e0p|exIuKbxxIwAi5PBi zgBchc&eXPK0E`t4q6`)eLyZ-6_)5ImmY6!>>uhGM5U{CQJV46_N;}+b+q&fgz!ThA zwsu8?6|ig*APH)bBnqoCMdHW_*2P=(rVL)}$6-1|eOUb&@N`%=J2No?4vmFEiR@bB ztcO(D$P{MXX{Le3DLVofujuJwkaG>(H0K)3=;`+0O}rB*GR*9rA?uMIzAfc|c`U5f z5&pb`XJXGT)L26;)QF*=C;+Po5Kr{_jvONzN_aTDkP>8)6$v%=G<@;vxJnTEI?iC- zuYf7R8NbFk#c{ zVcPyjH{mF#a?Vwtnvodw*W`qhEm5_Y*&ECpX>#0Q20DAyIZ!|@&FZD0515qL=b}dh z{ukPB1io6a4_aD)w??Sz36u_$k}&m0_H{czTb-3qI#$t*V$&>E0~5JUY)%i_4y;k1y`?d{m&dJNrY%p*670G zeWP}8IlQ5)VV*>eJCKm&gadpm%47WwhuQFWHZ0HT(>Mv}R%xC9v1W*GdNbIQvOMN* z0X?krE%f({`OP>1&neFq@&vC7=Ln(YSwgS@K-zdXBLZUdP1A(K zx#d|xX6LYLAyzUm>);+Fk?sOFI@~O1u%8HPXY)Cvt&qK70#Oz7*?^h(5s&Xu*<9b%Yxtzg$swAh@#hk>TOLn@K z?BSUy$zy!Z9G0naJf>GWHr%l&kM#v43}=labwM86t0fI^UTFN-Z4D`E7Tq$rEEc#4$4SC?Zky;3tQR!?sgHtf?i z4v%CVcAPrA)FpY0FSyG~x*(73>>OWb5RdJ^SvnSQpM)ps0-u7xwE&hMlEOLcVQhLP zk3quejl@7of>EV&1R(zDa5CyTj*}U9_^j%E0diIahv}HwUX5)uvyx+!#e&&Y>`Tszx;{O#f+K{*8ndd`7s_tK zaHdr*BMbXXkH4JqJiQFjCwy^Ezhs*lMS2uvf;a3Io^3FdUZ76o6ZoIU;2F z5+RHcII)*#6Glv`KH*N!&-m_W_i?!IcX*5Z1w4VVd>th$5eu0J%EXR@EV1(FgS#EV zJ22r=8G-3pjyP;Tr)umQdXAhnGa9ylKli#aSXCj={+gXqfQE{Z6;dlm$Hk zmKZ#YgHjkCN{X^rnxJ9~cE(%;!ZNEN+Yg7fROR#rh4PL+XCjV60;p0p8T%y!&zH0J^2oMd7rv(!hUSTJXJ@kl)>*Dao5tLqcvoL88NB~Wi* zFr<*8EY`5=GJPCX6k@T{2{t(fp=Gm}^Za94Ee(Lnj|I{sO=LNRT{=$y4>mLthT9k% zIZRn_^Nh@Zn$3^}Qyj$AZF;|Wz_Fm1a87kWQ9_nj-CS9stimkz&~V|70a$D}$V-wt zI4`_EKnj7$@SY%LiPm7{O4#%;*DaqVL@!vR`YSB}%d(h*1vq?FF&0aWp|nz_=y@R2 zv%Eb2*3C_p&Jw_kC4~;Efz?ULQb?{RG6rTEn<2;(!*M$!g6+?SVq6tXwioRtvN%hF zx7*DY^7WY0FDd>xWnp@X$KBU(CIhaA&p*j^HF>$`{2i}3hj~oK!3Y`49I*1JpiYV* zMR}|tGb(d+5snz^qAb=Tk&-bs4sbiZ9(BZ4HN*3okm}~<1>Vnw23)xm)L%xs~ zSko}?yLX4hqjZGJ*OOMZEA#pW_`{7+5CB<`rv+SG@jOkg9Af4Y>n_?Px)>d+)(CMA z&1CaUUVzIHYSCp1g~d8?3EPMgUrt#T>wKMkuNni>1a;YOg8!ED)SYsB$+b z%3}?o1dq0JSdT^XSWj29=FG$VZu`9dC`WVCYbjP?7H)8lX||#ei=Bw`xXbVen`W|e z;9QN1kg-I{yt0g=&88rat+>O*VXF(W*y_0>wIJ0GJ5#c$lx!oaNHG`&qsYH^(y1a13Wtl;3BRpz)l^r=US6SE@sSv z4HIz@#Ta8wQk2J9T#ki%4by{7-DA*^Z1PCX)&BVoZ>%WZ+_F5}#w1`F#_5RBF0wpN zh%pXRh*O>?Kk3AfroF7X;R^Q;=r;(8=2!|ahWfy{Re#rR0D!Om*tkR1N! z!&Z!s1iN(MCtr`4q+=;|)p1TL73d)u9|`tfurGoeFwpV9r0?By(x`j|JP|BalI4D#)@t=3*$oWKPQRn4iU#gP8&+=y@P!l^`bQc_3g=jRRE}Rl`FH%&PQAc%oof zoh!;GF`~w_mi>QVgr6Gi%;$G9KYSn?n^@0+cJ+&!x3d-32w-T>iCg0w#J`3RH({$G z-yU}R_+nr((JUx8W*x>sT{B)IxD^lF1}5Y1!iat^L~LOD=vpxw;Z>&U4Op-#NAh@6 z!V%>bF+L0hk2pOI>%|I^v+3Nv$zjb%Y4Y-LDc9Q#KL3Qdd0#!RSHtIi_3c%wth1?R zXV0|tS&f>*Ei&X~ot$vqQ%Y10?y;zQITle$MMH!6Z0+}+5|T)wS~d;dmNCg{`Jxcu zAQN%%iNvXbF|{n8Fq-w1t)`vJl}#2%&Pr*gs!$~rf({jKt>zF~NmRLnUSd=(p`Y$@ zVm^sUIL!@!qe%g?Nhz?g!?vEU3xb7IqZJ$w2xJZAb8r)SvQ6Y&FXd9gC?7IjvP+Bn znC#MGIUA-AIsDZmB14AESji^P*(kXLGT9`%v}d8@4#9Z)jS5JT=nRl-0&*pcO=IrQ zu_?yPjaF?ZRnaLK`4lvRA{*M68<9<7(jKxYNIpY0jg_X5Perp2vZ=@*+7Gt73kI1#OkxnZBda6{$&r?$l_G;K4Rh@AqZ|>JPnD&Zo8k*i znS<1&&sNoCkXe^5d7koif~;)aZSs88ZTeizZR$M51Bs)uwGZ&fH$i|&t{nm_@{Hl9 zS<1GB&m!LxK95{W_#CnfVKM*->=}W_5b8Wo{7C1LW(6NZ42CL$TSMrC$<%-X7M{Xs zk}N}IaDW{Vz1u=N<`f6HV3(XHCc8jQZ>J)%6yVd334-$Adr5*U`P1r$68B^wm`c2p z#mr5llSLd#io<2#X1d|hfgju1ZrJ9>Wj~d`hD{TmAEYWdJey8Y1n0Ra0)I}FDjk}u zr^-jB>M4@olqf|In5LzQo7^k>dK;6aa2+cveTW_Elh`GaiA713hXoH0uyu^&7~$%x zCx3XWg-Zi7%#70J4swnNO~f-;1{!l_LFDRSt8B=4fS?ACr)>`Bc&Z9+WorlVhkz-6 z2N#JRy2+I+2e27~90EDZsl72+N<%HAUQK=JKF`8=EuAOWw#-Hy@{_CMAhszVBKKlsy1FuB^3Ni znB;;gwFaNTcx|*uJ{Sqk1!tWwu#>Lv#nd>3j!WuRGyAe#jwP|{ z&9#PggN>_=nZ~MHY;2H(5P-tXv|>uvGgCtoA~mbeH*~zn2H_KHpg<=!|5Br|1svHv z`T|q7qva@&)raNRarFqWK%}~`b3C*8H)^=D=&c%dYli*>JS{g?P2FN)L!P>c!e->( zM#(UNcfG`-ZGT@R!M@Ek3}Uu+*C#Bd&2BwdT$|og5V!S*tKo6I+4b{5UJyRa&hHlc zVF|CE$jcos5JhH7cw(VD>=wfTZidX~BaOb4@z*V3H2#>$4H1prboP4JFQ0e&;Y;6P zcHgew?KV4kCRAUP`0)26flLED$yUEsK3VWJp4!D56#v6U%Px()JA4>rv3taSb<6E@ z_W;j!Jj1+N7y*C0-*k_w&D~+y8ERX|X7AO5^u!i%h!4i%`j{c!&Fs@pxU55rRr z>Eq#jB7Ca#r&ss=-ScDjuvIIwWB0$QO9Nr2i@%@8Nm9JPez)Ln}+%%O%0! zH_#vLLNQ9B!|q%38(hrs$;PHIjms^|U>C;YGGZY!_JF%#_g%kUZtna0?#p8J+(VVZ zt2+>1pnmYb@Vud2;HR$t3T^(-cgu%Gx7}@E?d9nwSqb-$67>Om>QOvJ4k=OL*`S}g z@UPD=(%0aDV|aq5gM6wZXZUIPx!Y`i>Y&u~7Owe#G-skI^@hghn~D^bb;8S;7GuYM7APSp7T<{p3dYfBgX z4Fkvjv)N!X3YvHPo!WK$^?kp87C}c-i@&?g4j$@)D6_%*eCpG0B6OIF8$bL49s|7} z2BL@izI$G4uU1d}!Os}kATYnIKlTf7E%`V8d%r=0376X3ed?D7d>5(z`cr5Bg}xPN z4x*L78}3i4xCDUKUiGr+z(2ub#EXMJb&v3*)^ZrX{M0SsCD{G&@J)+@Fup@2px3~V zFaNa~*6_#+^gD~)@}v3<6@41MLemw>T4Gc|sliYB0wV-^XdD-BE`REt7Q4?q{&ju@ zO{K%e1LFylEbKIZGl~GPS6Pk08qmg|{MqFNG_Cc%@7D^k=-#6A5$68S{x47`fI@^3 zi6vPi@KHq|=*{{BK3A1sVdU<9fZvo~Vk_%@|7{MH4_i5hel3OuFg`nMR`*Cctf4!6 z>eq+vw=)b2z{~jo$iT!fhEN3r@%7>pz~JR|oVN z^=&x2)BnUT`rq5l%O(ALwf{Ign6JyHtuA;!JUr-s-<=t@_aj>XxirNE;s*pQM;>1u zwvb%-K!4e<^-o{Mf9mo$Vj8LOFVt=P<$l<$)#yYD*kDfKK@64T57?hsKVaPJtzULM zb^z8-hkmuPegKE~EPjB-io>K0ya4wNn#|oE>hom@{to|vf5H#&&!_$ghhB&i%0pdg zlkF_K;@EukU;S-A+HkjkK1l!mv^YTfR`rLsGwxtOum4`GAxIcMfS@TqfXMsdQ3fzY z`LNYd1!0YUk-FGImkZMH`|e9;`PAC|0qX$+o$1vIJ*K5-aKH> z^|^b3Nd+{W`=RTU919ok#?0_o0SC@+NlL+HEQ ztX42yf;k`*GA#P91J<-)2hH>Wy;|7l+CZ7T4u~dA_}?E;nM*Nkz)nmX94)41qL#%% zvr>T*)Oh>U@Aoj2!Ktc(308sMhs|CMKB2C!Y5iTUpP#Inh6)fGP>*=43tj84qkdD>OrmmaBmFiu%p}^zHOzwLCM){2O~Kc z0pMI&sDMAhq*nXM^WovGu)_6k=tkJL{66gVhu?=)|MUNOD@c9?615EQ02PWk9zq6( zE3>BDxLg1kY>~<6Po_S^@6gDMf4;-|-opZfnBaz76xt-%L}9;PYR?Y<#K!f1Tf4U9 zHgY8EFPbnZOSa$IEo*zU;db|0ayw#PDkxqgPE({-q%3>Ce$L560*NZ1Sfuu4V%n0b zfJ9zSo=jx5ZRlNdMbOK&#KS*<#ITU-)%F7;-7oS%1->Qj7=nPV6^&A)<4%37K&QNN zs+=e^ZJ+Wh+I~r}Dk%4!x7+&%sMlh_BQI%Awt;YDC5d#T!Fd4uN40PBecqp`(~E{A zPG`2cpDfo-$@s5mhZW*z)1xvXKx*W8`xAih-aUdxpbh}?G6%pt6vbBBoavXdqEqt) z_CT;#ScAE$XeHDTb3W>)OJ1k=QkBdl639m;+Vj~*17E)Y%Ld89|HWv+FoFmXCF&gZ z+mP}y+7!>(Yw~!x)*YV@6n?M0(gB}N=}O?CcyXFU?1+b^g!=Vf9dT2SI!Lrih zPYz%N6?oVbc6x!3^5^4b3Qw+5#KuE-I&CiRA697KziO7VhKD9nz%Q8ZOpe+G0H~L^ z8;bx9V&4Lc)7;eIcYxUCuRnM5>&-WOQu`c~=PzS9K`-<-mtZ2h;n91wQzEO!}M^+#TL`3!9-;w8P z@^#Kskcl7On$Ge?#+FHDHD4%J@Xjd3g_e6(Lu6n~2M0%Q_<~!80zPboMBI|d5#r(U z!*xp~2LviSOT>=c#C9K4T|W{h=paV-@NeF#j*fx?YsMQmG*DdP{Yl>;K#!>l?_Ov{90P6vyF(N z1bzPF8K0#7N7Gzh|4ts~D>^*SH~06;-3KA6@sodz-!?mB_T&?vVLnKfZ<~)Jaz*&H zKjmXQS;FHI6)vb4bxSJ^NEtF1k05nMkY;j&7fzTsAuCZe7A-?cchjJz1A+()n5-TN z?)2g3m)$pUhuCoL<}nC{ZXT*Pe_Tpn3eL;S)dIT)tuJ3cUtb~x{PXJK>&HKuFBhNw zIBz~(UR_%r2&wV}P8F-@VG-?>c1C5wf=ZS-y^uZ`8tng(SBW|mvFfv7)#S{_XS4bD z<#gT?-FqfQz0u)a=DiJV?Xw1_LIo!hlZjSB)N(g5>%GcN3wZ@hYYqet;y&=*Q4~~j zu!r{V=3Ev+o7jIboa$#g&mZyskbho*BL&kerX@>dKCh#9EuM!Ll@5mHe(REa8Q6b4OSA3VlR^#_5Yp>Vy)dQH0<&TnJ^pA~I zq({cf+I&pRO6gdJg_JxcJr%GR>?R)B$yr$5WPwPt+dP4~tFj9a6vCVdN^*Ss&*&^< z`pG{9EoB%NEiHv{{-B*oht=UHXAQk^7pC;~rs@R?P74)Pi~YTy{6o@#}-wQ5m(+% ztKvg6-_WOlV%U{biT;qHQy_K-RJwm6E62`K-lTz+GJsN%4Lw?IqU2~fV?_sn+>@8w zPyUTk9Eds8rM9e=GS`%&CA}5$Gdfz!h{kWoBtD?3Rx;Qg9z#)cT94aZK_;s2r2h<$ zEKvUo`3?3`!#W9~y!Vlh5hdd1!wfI3n7UUd^61*|3%EvxQacb6I!J(-)}of;U_F4e zXJ)KV;TPl}*x<2Y--K);D8y(S5fs#{1fQ-Z0Dc!O)RJp}HbMGB z{_(Hna^Fi{#F8@> zP|s47X`>+z#rI0K*b*a$YT5r}!!O}T&t%veFeOkjjkV`yA zvm#fv{Z5`U>7w^>FJpv*Lk|964-b}6U1$XnWp_dEOc)##%)>!<>r32X#AQ}KOJZ~a zo~$13CMvE@d;_>cUHLu=b4(&7z;6dA)p2_r^QfW~X+@jacajO;$Mlhfp5nIJ^qKm| zG82kKRYf-hR~ECL5)t5u^G$H<&$t8>-(~CH!jAZUjjK40BkI1U-!$yRnF(eH@6IZr z1#=EEIKP@<;nLTu(4Yg{&80(kMyt&&x_+%7A}$idSE2#Igu5CCcrfHnw{yXj^2-p* z^iC^|a^M~3Am@S~^(HD-+4ibcJ!yyJ69S8S>uT_hKGr1wRV{V#J_c1uOh?$qO9wCaJG=GgpZ{)liH0jQ7H;+Gq zK6~MJ-gg*y)hBvc9_v)~N#{`27Q!kSEDFEz_qqmK($8QDSr%mQG(F46w$eMz@4OV8 znw`XSfiF9(2}1F{MFW?=;$%b2)S26Q?n~n@vu+226odwGZ?8$|5y~v3X~eTB?;e)u zF1lg8uo|SiW|hr4w$7yBcXG0w|L+sJ5RoB2`nr$p&v4UV>5Qv2R2Vu_#&;myW9=EHE`=o_GI^?lP9&0P z1mKp4kIqoSM?YHL*y9Pd5x`%k>4?AJ%QH-*zuzylQB_n6u5YmsmD{tp7Sk7Sfjfn; zTeWTXu%rEXjLk_5C$CLAVQ)7`zzTadM{hM><^zd5t*EH{(Q^A70~R(AGmrNqgoV#l z+oVe?>7TQr0qdkTPtTa^8I_>Hqyn@y$&w2iBi3oVoX~&6|PQJH3cuirbb;A z&EzdZnBC}shj6kSK%xJYIWOk?!ZzfNOAhL4HJ?8?Qo5TVSyYXuYf*Lh zcy2EK*=Qf`uNS{veollgTMf_IG983M6)6QLP4h)K-dv8>kEoE4voz9Pe%xAdmTPF& zbF6a9Q^@|p{FhXLDrzKxE9T76B%>#>Z< zAhbJ7o!u#I7Hamvky8AkUI5S15ngA%ta00;l9VqnP7cSx@vva?0;P`;06Clpc*c8i zplq|)QR|`UCNc=EGMlBI^il>I`+rw1c%}|LkKdzHv8O^a_k#{fK05tVR7KJ9LM6+6 z4sbL0Yut)J{?KS=j52mvPF>>EQ+!*J7MAD{Non(Wap|e>MjZpauvu>;>Rb0HS_!6q zLnlx-N2Cq;t9KfuHK>2#wzTU0;HvIBxiCB9{j30=+jv0D3hT+JN!?e($>vV=>PDcN zK~(I}+YqR1Ji+#xJxNy=pQNC+!=k2iwGRM*9Z3X^HagScRU#48&H-bwwvDP^MmUw= zDuqeWK@~+?oQsQgc;#z4S7?%2e`7h1|M2B2uBuW) zGsEo3r9>ks;I;N|5ES%ryF?9H1ykP&7LYy>%P;267UNRdF?u`S;-fjdOX#nj(9dLg zs0I|31#d{Fa~S26@syW#GcI6`w6K#CQtAVGE9#Px3ltECckadzRSwffw!;3M*;7SI z=tu<>4&QQd*Cq_$H(P0YpK~z|JRKEAMSXxz^zLC+=?p%iR0390h5!p&UhD#l$z^UD%m*Xqqk=gVz5YcYGezyH}*M`233oh{;Kt*<&&F0uCJ zhM-3a?|~DlX$viFK+xtR`y63VRL$MtdD+o%ta*78^>sVQ zq|%J4x(X!-o8GH1*o}h#K~avRLv6WGwKhRwKXMRn2$>@hxt>s^+#>m!DiM}2fQ!81 z?t+lA$Xn79;bz4;yI|cV|7X3Or-<@PBNaqRilVa2FXWcPdW>ahi)Zw9&g~#Ar(&Mv z&LATNd(0;{iIPlI;Sz2i*yqDC#hfBn+hc2SWl)_RJc`vabh$@4Y0;!KNCpYt3*`GK}gb&4Ut-ezJ zhOA1+ihoMuzvFT2P(o%_QNR)lqFBD$0@bdw0=AzEGTvR^@p|s%EL_5XP9(Q|*s(_5 zCg8YX)~mj*j}{9BTL*!9`-fFsv5i<=3TJ%jvxDN`r+6%`4Bduzj2t8-k7QEdS|U{- z;w^dMk`<~_5$NQnn;4$lqsAmM!k-f`Q}PrBD&GzxY6o0%f(^&X5J`eH_sRN3WcP%a674;r<3=an!g` zxciCA>rCr6k02SMSc_n?bdQ4@Z#6{ZjipCNYwsLU9QZyjHv!@V>j)$Eo9A3~Px8mq z{0XBfRO0(GKdMjR*c~l#3_|6;kxD`^ zDVT%NBn}hehj5a=Z)Q)cxjN_%;-RyaM@E!KOf4fm2d^D_iCJf@)Q$q@(@HEvY}ARq zoEHyG#=MzYr*K3el|)!<1vQ-?i$@H)3h!?bG}hFVY-M0cTpo@%W!TQA9(x+ne9koB zqJ(k+FKiZh#beTPjcTep{LJ4$6=RysHDk*>i=`leW2`E8i5u z`e9%cr*1|7EJ_`aD4I~zVUM$P<@41x-Dn9^xRHNOX_JBk#xe`xZkzU88t6MB-6^i$ zW{@pilN^fNlD8Q5P8^9|>vV^~jRHbMWJT6mc#=aJG6PbOzbgb4uvrO9Kp>$$x6D#Q z1}qAtQ;QTXGJ4paP8it`5@ZAot!5=2^TeiArlx_ExbM3*M<)>5ti(Vq0-@FgLfeNR zMgMJARSKKps=PrYbrlmVrD7$QCz&Q4d5WQRe+JF#qmZKaanEYg@}bhiNEqRLD&bJK zT~c}2PAC#Q*P2sOKMEPLsKMd43bDc)ShTyiC!HPw}&#wlm|)7qkDmSfTBO! zq=~6##X98q{usAfF7W|nE#Vd4Vv96mmIZ4hHOUqZO{7tmI(^{yU(Wx11ckgwY=%Xc2(034jvZdit zN{!wf=s>5A*>ci&jHik>gexp|PIn=g8J#rMWETXyyn6{o#PG7FI+tP7xlT}24yx)z zbu*6qWayMvj?9~A^+leEqeTTNOb|`UjZ_1$@T@qBcWe4@#cDGYK_-agm#3Z>f8aTU=7zQZMR z-0_+OJT!1Xx6~9X<{k4N`Xm;P1tm0Tl9@Vk5xT3$PF4ubdi?MmanT}euesn~>qGL& zeS8^?OWq>|J1?cTPhdV^&sYkKj$=ANDSpDp)elDNB?!|$MozS*JsRm8jgdCo@}WTq z{`(Sr60%F?BjeJnuIQ9}wIcb$E=rJylA2Mw*RGR5D}BL`h$ZrEivj`+?$3r!$+Vx? z7m}iz<9S3BJlAL$UE5$z--1-T zC?$&FJUq&qnCsraJjnp+EKa={s=fw#F|_?a9?k6I9%GHgj}I->WO;NOD=I{hhZmU~ zAr$rkQY+2cH9nc0YGal3YI*X^8SNyQmVq7}g;3I~5hfgQsXzm9z^}7 zaQO|$pu`o6Ko9`kM9l>U(Z0eIFMPy%o36x$JFDV}GJ{e)6ag<`Q@5}kpf7&t0xLIL z#C&Bb_T{zG-~{si7VmTF>9;FASNK6XuZrhXLY6BrGb&e&60)ec<_xB$ z-BGfYE2Zl9LaYt5ak&un-}q984h>W;4~<@&HYJOM>yDLXmk=J=n@QcaggRJny$uE1 z$p!%Cl68tGL8hdL#UP`a(7I>9NXKb=g|={LrPV}*5$Mib+_`>4bs!lZ9coHmEfNAL z;c0Z)$s%idl(k;%y_=qpv}-z&Y%0qWe#OzeT+jbt%P;qPmy_a2Q8>UV8=&C?PrXN_etgdiZ6rzGfN%i_-NKxUGboMo;b1 z^^sE^RGTof)!AJb_>GZZl6z}&6r*6~q%(q=tpPHjPo1jKCAY_n8bo8VM^1R}2&2Do zO)a4_c#U|KN8aeOy%$n#e(i?^G_E95v13=_54@6Z1b9jTAHGPrHQ%ZrlvEwW&92~` z_13&-kt^hX%x31cf12Gk&;51=eOz`fgmX#3Bjd~uA8@k04z4XUDm7!R2050prw^q4jRym zIO+`7*k6^GdswX^3FOiNWSL<5=Uv<&w7MKo?S_pX$2(-LY?PAaf{K}=ir&}m49QbE z9;ehDQ!*JoHY+XV!_oxGm*zXkTe}&Stdg)tiYGS2$%%OogKaP$#f;8JTq@*lYu-*~ zTW@bS8yhI2L5Y)?6KkQ(0^PXNVi042K@pEUi64@%Js-;%$lpu2N)7zsyAS%K2>AzI z2l|AuOrLZ@o%Seb>@Ah}OGA%L`fS@uw*UsFTJEUFJxOXFcAR8GD6wlY0o4~KC&?3% z$i+dGAQ6AinJj2<#y#Zl&~FdXggab4vtot-IJuzWoCa}{an7UBWcKf;t?xdE-S{e& z9Da6%4%`v55L03cVd->tMi#uxYCc2*b<{e`4Q@?!=Rg+WdXFK1Q*PwbB#HjD1KN` zn^0NF0)W%eBdx*bQ+0c7ivwL|_zL4Aj#`Fj$R~VS*K|1iKY4HsOPE;|zEE-Ax{>26 zj$I-v)ySiEzFpuWTA_y(pM{*bcjf5gXc6h2;fKN#4u`8zK?n0*3`Ff_R;kA3`^LOU zSsnB?OIeU2+4Mg;bbGDcbEAr65j`CI2}hRg6*%?^|P z*SN1CMQa^~!JQSNhw|kqdI*><-|iFl9{oVn`q6}M*kXv4wdQ!B2C+< zOPFRDOxNn9?4gNZF%68CCY(h8*sRI+&I(H3v^8jZmgD{`sP>ky0Q@bC~#i&}G% zI~|&b1-oFAa$be6iFuv_XIrn4gGQh@d;E#yQaUPz4c@c)V!}^GVgNi_1t{gP2&97c zQ655H?}^UH)_7M`N}=>QcHOL*-u=DZ;oEni41^~}z|uiUt|B>|FLl5U&LJLLjHj5Z zXde~>T1t;}I&!c(6QF-&&DdNkmXfCmoUSDP5ACEf;4cn(gGOhZ7XCEWAx5wDo2cw$ z3{DCMQ4kkZJL{*WCF9$;KQN?i2iKbohG6U{JKxydkuvN3NI>c$M>|Y2N{eRk%2F+w z`9!+^^G+^ddq=Ie94$a|pi-TjZ;mkf?iNYIsO_#rn1y8A9lb=%)?JYz{bPiqVqR!~ zjYF#1)^&<6Z9m+F^Uh9{EjN{qd|;71S(__Jm|luDQZ1(qo005wF5}oU7((_z9wdw! zEp)Tj&rponG6~UveNjNMK{%tRpTmJW$>QOgBgnMo((!&Zi>}N&LXBGv&3F&hB_s}Z zRC)O_EqCu13cHT-aPaFqT?{s(*I)-Q=YF|ZstFi@+H)C9IEW7aj1^{7ZwnZbb2f-0 zmD|BBG}MB0u6tVX{CORV=Tu<@D(RTP=-_zX8C(x(kOZ3vi>_*Ziic@ zN2vGek zTrm)JO;~>9+*@c!sP#99V?=j_-6KhfjNk?7sC1EgmmWO6nGYqY(_e zzQ(g~-m{;Kf(HA>w4SKNBdK&mnG|PF+U(}zgO94oz)EKuON<)yPb@|Y0FFmqKtuRL zLqjx`cpb+0PtB!(@9K8?DA2LYz3k;2;xtxZ)6+cMKa&}YEt{kFhYn#g$`5DK zLj-?e9_K=s&(5MbxahsNaWp5Vl7?tARr1O)ZtT5mfYl`juH#|tgrnh&&dSqo$|j-l zw4A7hpJm%@Q=iU?`@Jp6R~ zfG;7bMLCV{sV{o?)At>^@&&!>0mJ*`x9#-PmfAKh@<@J7+{Nb5#&POdr}iMqSp9UM zswxsc?cf!h;UkB*Od_*R|8*sAnXT{zPkf6QU(-Y-Yl}}L>eqb~f|}#qN5?AMF)OsX z=wVJ2OPAeEZt(Ff{Kq*bGRJ{n(Z}pxiWsDHVB#?8P>?Oi{56aD?(+<4rS^kprRr Date: Tue, 30 Aug 2022 17:04:00 +1000 Subject: [PATCH 5/8] Cleanup of experimental FolderPicker code used for testing and diagnosing the issue with DSE. FolderBrowserDialog works fine. --- VG Music Studio/UI/MainForm.cs | 206 +-------------------------------- 1 file changed, 5 insertions(+), 201 deletions(-) diff --git a/VG Music Studio/UI/MainForm.cs b/VG Music Studio/UI/MainForm.cs index 8a8d0bbc..71e452e1 100644 --- a/VG Music Studio/UI/MainForm.cs +++ b/VG Music Studio/UI/MainForm.cs @@ -53,203 +53,6 @@ internal class MainForm : ThemedForm #endregion - #region Folder Picker - - // Originally made by: Simon Mourier - // Source: https://stackoverflow.com/questions/11624298/how-do-i-use-openfiledialog-to-select-a-folder/66187224#66187224 - - public class FolderPicker - { - public virtual string ResultPath { get; protected set; } - public virtual string ResultName { get; protected set; } - public virtual string InputPath { get; set; } - public virtual bool ForceFileSystem { get; set; } - public virtual string Title { get; set; } - public virtual string OkButtonLabel { get; set; } - public virtual string FileNameLabel { get; set; } - - protected virtual int SetOptions(int options) - { - if (ForceFileSystem) - { - options |= (int)FOS.FOS_FORCEFILESYSTEM; - } - return options; - } - - // for all .NET - public virtual bool? ShowDialog(IntPtr owner, bool throwOnError = false) - { - var dialog = (IFileOpenDialog)new FileOpenDialog(); - if (!string.IsNullOrEmpty(InputPath)) - { - if (CheckHr(SHCreateItemFromParsingName(InputPath, null, typeof(IShellItem).GUID, out var item), throwOnError) != 0) - return null; - - dialog.SetFolder(item); - } - - var options = FOS.FOS_PICKFOLDERS; - options = (FOS)SetOptions((int)options); - dialog.SetOptions(options); - - if (Title != null) - { - dialog.SetTitle(Title); - } - - if (OkButtonLabel != null) - { - dialog.SetOkButtonLabel(OkButtonLabel); - } - - if (FileNameLabel != null) - { - dialog.SetFileName(FileNameLabel); - } - - if (owner == IntPtr.Zero) - { - owner = Process.GetCurrentProcess().MainWindowHandle; - if (owner == IntPtr.Zero) - { - owner = GetDesktopWindow(); - } - } - - var hr = dialog.Show(owner); - if (hr == ERROR_CANCELLED) - return null; - - if (CheckHr(hr, throwOnError) != 0) - return null; - - if (CheckHr(dialog.GetResult(out var result), throwOnError) != 0) - return null; - - if (CheckHr(result.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING, out var path), throwOnError) != 0) - return null; - - ResultPath = path; - - if (CheckHr(result.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEEDITING, out path), false) == 0) - { - ResultName = path; - } - return true; - } - - private static int CheckHr(int hr, bool throwOnError) - { - if (hr != 0) - { - if (throwOnError) - Marshal.ThrowExceptionForHR(hr); - } - return hr; - } - - [DllImport("shell32")] - private static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem ppv); - - [DllImport("user32")] - private static extern IntPtr GetDesktopWindow(); - -#pragma warning disable IDE1006 // Naming Styles - private const int ERROR_CANCELLED = unchecked((int)0x800704C7); -#pragma warning restore IDE1006 // Naming Styles - - [ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")] // CLSID_FileOpenDialog - private class FileOpenDialog - { - } - - [ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - private interface IFileOpenDialog - { - [PreserveSig] int Show(IntPtr parent); // IModalWindow - [PreserveSig] int SetFileTypes(); // not fully defined - [PreserveSig] int SetFileTypeIndex(int iFileType); - [PreserveSig] int GetFileTypeIndex(out int piFileType); - [PreserveSig] int Advise(); // not fully defined - [PreserveSig] int Unadvise(); - [PreserveSig] int SetOptions(FOS fos); - [PreserveSig] int GetOptions(out FOS pfos); - [PreserveSig] int SetDefaultFolder(IShellItem psi); - [PreserveSig] int SetFolder(IShellItem psi); - [PreserveSig] int GetFolder(out IShellItem ppsi); - [PreserveSig] int GetCurrentSelection(out IShellItem ppsi); - [PreserveSig] int SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); - [PreserveSig] int GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); - [PreserveSig] int SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); - [PreserveSig] int SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); - [PreserveSig] int SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); - [PreserveSig] int GetResult(out IShellItem ppsi); - [PreserveSig] int AddPlace(IShellItem psi, int alignment); - [PreserveSig] int SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); - [PreserveSig] int Close(int hr); - [PreserveSig] int SetClientGuid(); // not fully defined - [PreserveSig] int ClearClientData(); - [PreserveSig] int SetFilter([MarshalAs(UnmanagedType.IUnknown)] object pFilter); - [PreserveSig] int GetResults([MarshalAs(UnmanagedType.IUnknown)] out object ppenum); - [PreserveSig] int GetSelectedItems([MarshalAs(UnmanagedType.IUnknown)] out object ppsai); - } - - [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - private interface IShellItem - { - [PreserveSig] int BindToHandler(); // not fully defined - [PreserveSig] int GetParent(); // not fully defined - [PreserveSig] int GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); - [PreserveSig] int GetAttributes(); // not fully defined - [PreserveSig] int Compare(); // not fully defined - } - -#pragma warning disable CA1712 // Do not prefix enum values with type name - private enum SIGDN : uint - { - SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000, - SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000, - SIGDN_FILESYSPATH = 0x80058000, - SIGDN_NORMALDISPLAY = 0, - SIGDN_PARENTRELATIVE = 0x80080001, - SIGDN_PARENTRELATIVEEDITING = 0x80031001, - SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001, - SIGDN_PARENTRELATIVEPARSING = 0x80018001, - SIGDN_URL = 0x80068000 - } - - [Flags] - private enum FOS - { - FOS_OVERWRITEPROMPT = 0x2, - FOS_STRICTFILETYPES = 0x4, - FOS_NOCHANGEDIR = 0x8, - FOS_PICKFOLDERS = 0x20, - FOS_FORCEFILESYSTEM = 0x40, - FOS_ALLNONSTORAGEITEMS = 0x80, - FOS_NOVALIDATE = 0x100, - FOS_ALLOWMULTISELECT = 0x200, - FOS_PATHMUSTEXIST = 0x800, - FOS_FILEMUSTEXIST = 0x1000, - FOS_CREATEPROMPT = 0x2000, - FOS_SHAREAWARE = 0x4000, - FOS_NOREADONLYRETURN = 0x8000, - FOS_NOTESTFILECREATE = 0x10000, - FOS_HIDEMRUPLACES = 0x20000, - FOS_HIDEPINNEDPLACES = 0x40000, - FOS_NODEREFERENCELINKS = 0x100000, - FOS_OKBUTTONNEEDSINTERACTION = 0x200000, - FOS_DONTADDTORECENT = 0x2000000, - FOS_FORCESHOWHIDDEN = 0x10000000, - FOS_DEFAULTNOMINIMODE = 0x20000000, - FOS_FORCEPREVIEWPANEON = 0x40000000, - FOS_SUPPORTSTREAMABLEITEMS = unchecked((int)0x80000000) - } -#pragma warning restore CA1712 // Do not prefix enum values with type name - } - #endregion - protected override void Dispose(bool disposing) { if (disposing) @@ -526,17 +329,18 @@ private void EndCurrentPlaylist(object sender, EventArgs e) private void OpenDSE(object sender, EventArgs e) { - var d = new FolderPicker + var d = new FolderBrowserDialog { - Title = Strings.MenuOpenDSE, + Description = Strings.MenuOpenDSE, + UseDescriptionForTitle = true }; //var f = File.OpenRead(d.SelectedPath); - if (d.ShowDialog(Handle) == true) + if (d.ShowDialog() == DialogResult.OK) { DisposeEngine(); bool success; - new Engine(Engine.EngineType.NDS_DSE, d.ResultPath); + new Engine(Engine.EngineType.NDS_DSE, d.SelectedPath); success = true; if (success != true) { From ef4f7a32a52bbe365ea0f0f3b8568d22e8b2fdd9 Mon Sep 17 00:00:00 2001 From: Davin Date: Tue, 30 Aug 2022 23:49:29 +1000 Subject: [PATCH 6/8] Filled in missing XML comments for Sanford.Multimedia.Midi.Core --- .../Generic/Deque/GenericDeque.cs | 3 + .../Generic/UndoableList/UndoableList.Test.cs | 3 + .../Generic/UndoableList/UndoableList.cs | 120 +++ .../Sanford.Collections/PriorityQueue.cs | 18 + .../Sanford.Multimedia.Midi/Clocks/IClock.cs | 3 + .../Clocks/MidiInternalClock.cs | 31 +- .../Clocks/PpqnClock.cs | 71 +- .../InputDevice.Construction.cs | 3 + .../InputDevice Class/InputDevice.Events.cs | 51 + .../InputDevice.Messaging.cs | 5 +- .../InputDevice.Properties.cs | 9 + .../InputDevice.PublicMethods.cs | 23 +- .../InputDevice Class/InputDevice.cs | 3 + .../Device Classes/MidiDevice.cs | 11 +- .../Device Classes/MidiDeviceException.cs | 27 + .../OutputDevice Classes/NoOpEventArgs.cs | 8 +- .../OutputDevice Classes/OutputDevice.cs | 16 +- .../OutputDevice Classes/OutputDeviceBase.cs | 118 ++- .../OutputDevice Classes/OutputStream.cs | 50 +- .../Sanford.Multimedia.Midi/GeneralMidi.cs | 515 +++++++++- .../EventArgs/ChannelMessageEventArgs.cs | 9 + .../EventArgs/InvalidShortMessageEventArgs.cs | 9 + .../EventArgs/InvalidSysExMessageEventArgs.cs | 9 + .../EventArgs/MetaMessageEventArgs.cs | 9 + .../Messages/EventArgs/MidiEventArgs.cs | 6 + .../EventArgs/ShortMessageEventArgs.cs | 21 + .../EventArgs/SysCommonMessageEventArgs.cs | 9 + .../EventArgs/SysExMessageEventArgs.cs | 9 + .../EventArgs/SysRealtimeMessageEventArgs.cs | 27 + .../Messages/IMidiMessage.cs | 18 + .../Message Builders/MetaTextBuilder.cs | 3 + .../Message Builders/TempoChangeBuilder.cs | 28 +- .../Messages/MessageDispatcher.cs | 38 +- .../MidiEvents/InputDeviceMidiEvents.cs | 11 +- .../Messages/MidiEvents/MergeMidiEvents.cs | 30 + .../MidiEvents/OutputDeviceEventSink.cs | 9 + .../Messages/ShortMessage.cs | 36 + .../Messages/SysExMessage.cs | 20 +- .../Processing/ChannelChaser.cs | 21 + .../Processing/ChannelStopper.cs | 21 + .../Processing/ChasedEventArgs.cs | 9 + .../Processing/StoppedEventArgs.cs | 9 + .../Sequencing/MidiEvent.cs | 12 + .../Sequencing/MidiFileProperties.cs | 34 +- .../Sequencing/RecordingSession.cs | 21 + .../Sequencing/Sequence.cs | 72 ++ .../Sequencing/Sequencer.cs | 60 ++ .../Track Classes/Track.Iterators.cs | 11 +- .../Sequencing/Track Classes/Track.Test.cs | 3 + .../Sequencing/Track Classes/Track.cs | 96 +- .../Sanford.Multimedia.Timers/ITimer.cs | 3 + .../Sanford.Multimedia.Timers/Time.cs | 86 +- .../Sanford.Multimedia.Timers/Timer.cs | 7 +- .../Sanford.Multimedia/ErrorEventArgs.cs | 12 + .../Sanford.Multimedia/Key.cs | 121 ++- .../Sanford.Threading/AsyncResult.cs | 12 + .../DelegateQueue/DelegateQueue.cs | 24 +- .../DelegateQueue/PostCompletedEventArgs.cs | 9 + .../DelegateScheduler/DelegateScheduler.cs | 27 +- .../DelegateScheduler/Task.cs | 25 + .../InvokeCompletedEventArgs.cs | 24 + WinForms/Properties/Icon48.png | Bin 0 -> 870 bytes WinForms/Properties/Icon528.png | Bin 0 -> 10133 bytes WinForms/Properties/Next.ico | Bin 0 -> 140062 bytes WinForms/Properties/Next.png | Bin 0 -> 4026 bytes WinForms/Properties/Pause.ico | Bin 0 -> 118062 bytes WinForms/Properties/Pause.png | Bin 0 -> 1450 bytes WinForms/Properties/Play.ico | Bin 0 -> 140062 bytes WinForms/Properties/Play.png | Bin 0 -> 2298 bytes WinForms/Properties/Playlist.png | Bin 0 -> 3045 bytes WinForms/Properties/Previous.ico | Bin 0 -> 140062 bytes WinForms/Properties/Previous.png | Bin 0 -> 3708 bytes WinForms/Properties/Resources.Designer.cs | 133 +++ WinForms/Properties/Resources.resx | 142 +++ WinForms/Properties/Settings.Designer.cs | 26 + WinForms/Properties/Settings.settings | 7 + WinForms/Properties/Song.png | Bin 0 -> 2328 bytes WinForms/Properties/Strings.Designer.cs | 919 ++++++++++++++++++ WinForms/Properties/Strings.es.resx | 348 +++++++ WinForms/Properties/Strings.it.resx | 348 +++++++ WinForms/Properties/Strings.resx | 380 ++++++++ WinForms/UI/ColorSlider.cs | 485 +++++++++ WinForms/UI/FlexibleMessageBox.cs | 697 +++++++++++++ WinForms/UI/ImageComboBox.cs | 62 ++ WinForms/UI/MainForm.cs | 789 +++++++++++++++ WinForms/UI/PianoControl.cs | 183 ++++ WinForms/UI/SongInfoControl.cs | 354 +++++++ WinForms/UI/Theme.cs | 165 ++++ WinForms/UI/TrackViewer.cs | 113 +++ WinForms/UI/ValueTextBox.cs | 104 ++ WinForms/Util/HSLColor.cs | 161 +++ WinForms/WinForms.csproj | 57 ++ 92 files changed, 7459 insertions(+), 122 deletions(-) create mode 100644 WinForms/Properties/Icon48.png create mode 100644 WinForms/Properties/Icon528.png create mode 100644 WinForms/Properties/Next.ico create mode 100644 WinForms/Properties/Next.png create mode 100644 WinForms/Properties/Pause.ico create mode 100644 WinForms/Properties/Pause.png create mode 100644 WinForms/Properties/Play.ico create mode 100644 WinForms/Properties/Play.png create mode 100644 WinForms/Properties/Playlist.png create mode 100644 WinForms/Properties/Previous.ico create mode 100644 WinForms/Properties/Previous.png create mode 100644 WinForms/Properties/Resources.Designer.cs create mode 100644 WinForms/Properties/Resources.resx create mode 100644 WinForms/Properties/Settings.Designer.cs create mode 100644 WinForms/Properties/Settings.settings create mode 100644 WinForms/Properties/Song.png create mode 100644 WinForms/Properties/Strings.Designer.cs create mode 100644 WinForms/Properties/Strings.es.resx create mode 100644 WinForms/Properties/Strings.it.resx create mode 100644 WinForms/Properties/Strings.resx create mode 100644 WinForms/UI/ColorSlider.cs create mode 100644 WinForms/UI/FlexibleMessageBox.cs create mode 100644 WinForms/UI/ImageComboBox.cs create mode 100644 WinForms/UI/MainForm.cs create mode 100644 WinForms/UI/PianoControl.cs create mode 100644 WinForms/UI/SongInfoControl.cs create mode 100644 WinForms/UI/Theme.cs create mode 100644 WinForms/UI/TrackViewer.cs create mode 100644 WinForms/UI/ValueTextBox.cs create mode 100644 WinForms/Util/HSLColor.cs create mode 100644 WinForms/WinForms.csproj diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.cs index a472f87e..686ea28a 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/Deque/GenericDeque.cs @@ -591,6 +591,9 @@ public virtual object Clone() #region IEnumerable Members + ///

+ /// Gets and returns the Enumerator. + /// public virtual IEnumerator GetEnumerator() { return new Enumerator(this); diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Test.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Test.cs index 48c79f12..e8167caa 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Test.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.Test.cs @@ -40,6 +40,9 @@ namespace Sanford.Collections.Generic { public partial class UndoableList : IList { + /// + /// This is the main command that will test the UndoableList. + /// [Conditional("DEBUG")] public static void Test() { diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.cs index 8cecb32f..866b9072 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/Generic/UndoableList/UndoableList.cs @@ -58,16 +58,25 @@ public partial class UndoableList : IList #region Construction + /// + /// The undoable construction list. + /// public UndoableList() { theList = new List(); } + /// + /// The collection list of undoables. + /// public UndoableList(IEnumerable collection) { theList = new List(collection); } + /// + /// The capacity list of undoables. + /// public UndoableList(int capacity) { theList = new List(capacity); @@ -111,111 +120,177 @@ public void ClearHistory() #region List Wrappers + /// + /// Searches the entire list for an element using the default comparer. + /// public int BinarySearch(T item) { return theList.BinarySearch(item); } + /// + /// Searches the entire list for an element using a specified comparer. + /// public int BinarySearch(T item, IComparer comparer) { return theList.BinarySearch(item, comparer); } + /// + /// Searches a range of elements in the sorted list for an element using a specified comparer. + /// public int BinarySearch(int index, int count, T item, IComparer comparer) { return theList.BinarySearch(index, count, item, comparer); } + /// + /// Determines whenever the list contains the undo/redo option. + /// public bool Contains(T item) { return theList.Contains(item); } + /// + /// Converts all the data that is being read into the option chosen. + /// public List ConvertAll(Converter converter) { return theList.ConvertAll(converter); } + /// + /// If the data exists and matches, it returns the value as true. + /// public bool Exists(Predicate match) { return theList.Exists(match); } + /// + /// Initiates trying to find the data that matches. + /// public T Find(Predicate match) { return theList.Find(match); } + /// + /// Initiates trying to find all the data results that match. + /// public List FindAll(Predicate match) { return theList.FindAll(match); } + /// + /// Finds the index to the data. + /// public int FindIndex(Predicate match) { return theList.FindIndex(match); } + /// + /// Finds the index to the data based on the start of the index. + /// public int FindIndex(int startIndex, Predicate match) { return theList.FindIndex(startIndex, match); } + /// + /// Finds the index to the data based on the start of the index and the count. + /// public int FindIndex(int startIndex, int count, Predicate match) { return theList.FindIndex(startIndex, count, match); } + /// + /// Searches for an element that matches the conditions defined by the T in Predicate, then returns the zero-based index of the last occurrence that matches the data if found, otherwise will be -1. + /// public int FindLastIndex(Predicate match) { return theList.FindLastIndex(match); } + /// + /// Searches for an element that matches the conditions defined by the T in Predicate and extended from the start of the index, then returns the zero-based index of the last occurrence that matches the data if found, otherwise will be -1. + /// public int FindLastIndex(int startIndex, Predicate match) { return theList.FindLastIndex(startIndex, match); } + /// + /// Searches for an element that matches the conditions defined by the T in Predicate and extended from the start of the index and to show a specific number of options, then returns the zero-based index of the last occurrence that matches the data if found, otherwise will be -1. + /// public int FindLastIndex(int startIndex, int count, Predicate match) { return theList.FindLastIndex(startIndex, count, match); } + /// + /// Searches for an element that matches the conditions defined by T, then returns the last element that matches, otherwise it will be T by default. + /// public T FindLast(Predicate match) { return theList.FindLast(match); } + /// + /// Searches for the item, then returns the last occurrence of the item in the list. + /// public int LastIndexOf(T item) { return theList.LastIndexOf(item); } + /// + /// Searches for the item, then returns the last occurrence of the item within the range of elements in the list. + /// public int LastIndexOf(T item, int index) { return theList.LastIndexOf(item, index); } + /// + /// Searches for the item, then returns the last occurrence of the item within the range of elements in the list and contains a specified number of elements and ends at the specific index. + /// public int LastIndexOf(T item, int index, int count) { return theList.LastIndexOf(item, index, count); } + /// + /// Determines whenever every element in the list matches the conditions set by the Predicate. + /// public bool TrueForAll(Predicate match) { return theList.TrueForAll(match); } + /// + /// Copies the elements of the list to a new array. + /// public T[] ToArray() { return theList.ToArray(); } + /// + /// Sets the capacity to the actual number of elements in the list, if the number is less than a threshold value. + /// public void TrimExcess() { theList.TrimExcess(); } + /// + /// Adds a range of elements to insert from the list with the number of elements. + /// public void AddRange(IEnumerable collection) { InsertRangeCommand command = new InsertRangeCommand(theList, theList.Count, collection); @@ -223,6 +298,9 @@ public void AddRange(IEnumerable collection) undoManager.Execute(command); } + /// + /// Inserts the range of elements from the list index into the undo/redo manager. + /// public void InsertRange(int index, IEnumerable collection) { InsertRangeCommand command = new InsertRangeCommand(theList, index, collection); @@ -230,6 +308,9 @@ public void InsertRange(int index, IEnumerable collection) undoManager.Execute(command); } + /// + /// Removes a range of elements to insert from the list with the number of elements. + /// public void RemoveRange(int index, int count) { RemoveRangeCommand command = new RemoveRangeCommand(theList, index, count); @@ -237,6 +318,9 @@ public void RemoveRange(int index, int count) undoManager.Execute(command); } + /// + /// Reverts any added element or any removed element from the list. + /// public void Reverse() { ReverseCommand command = new ReverseCommand(theList); @@ -244,6 +328,9 @@ public void Reverse() undoManager.Execute(command); } + /// + /// Reverts any added element or any removed element from the list and shows the number of elements. + /// public void Reverse(int index, int count) { ReverseCommand command = new ReverseCommand(theList, index, count); @@ -285,11 +372,17 @@ public int RedoCount #region IList Members + /// + /// Searches for a list of undo/redo functions via an index. + /// public int IndexOf(T item) { return theList.IndexOf(item); } + /// + /// Inserts the undo/redo listed options from the index. + /// public void Insert(int index, T item) { InsertCommand command = new InsertCommand(theList, index, item); @@ -297,6 +390,9 @@ public void Insert(int index, T item) undoManager.Execute(command); } + /// + /// Allows to remove the undo/redo options listed by command. + /// public void RemoveAt(int index) { RemoveAtCommand command = new RemoveAtCommand(theList, index); @@ -304,6 +400,9 @@ public void RemoveAt(int index) undoManager.Execute(command); } + /// + /// Gets or sets the undo/redo options from the list. + /// public T this[int index] { get @@ -322,6 +421,9 @@ public T this[int index] #region ICollection Members + /// + /// Adds an undo/redo option to the list of undo/redo options. + /// public void Add(T item) { InsertCommand command = new InsertCommand(theList, Count, item); @@ -329,6 +431,9 @@ public void Add(T item) undoManager.Execute(command); } + /// + /// Clears an undo/redo option from the list of undo/redo options. + /// public void Clear() { #region Guard @@ -345,11 +450,17 @@ public void Clear() undoManager.Execute(command); } + /// + /// Copies an undo/redo option from the list to an array. + /// public void CopyTo(T[] array, int arrayIndex) { theList.CopyTo(array, arrayIndex); } + /// + /// Counts the list of undo/redo options from the list. + /// public int Count { get @@ -358,6 +469,9 @@ public int Count } } + /// + /// Checks if the list is read only, and returns if it is false. + /// public bool IsReadOnly { get @@ -366,6 +480,9 @@ public bool IsReadOnly } } + /// + /// Removes an undo/redo option from the list. + /// public bool Remove(T item) { int index = IndexOf(item); @@ -391,6 +508,9 @@ public bool Remove(T item) #region IEnumerable Members + /// + /// Gets an enumerator and returns an enumerator that iterates through the list. + /// public IEnumerator GetEnumerator() { return theList.GetEnumerator(); diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Collections/PriorityQueue.cs b/Sanford.Multimedia.Midi.Core/Sanford.Collections/PriorityQueue.cs index 65411587..0661ad04 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Collections/PriorityQueue.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Collections/PriorityQueue.cs @@ -483,6 +483,9 @@ private void AssertValid() } } + /// + /// Tests the methods in the Priority Queue. + /// [Conditional("DEBUG")] public static void Test() { @@ -807,6 +810,9 @@ public bool MoveNext() #region ICollection Members + /// + /// Gets a value indicating whenever PriorityQueue is synchronized. + /// public virtual bool IsSynchronized { get @@ -815,6 +821,9 @@ public virtual bool IsSynchronized } } + /// + /// Gets the number of elements contained in PriorityQueue. + /// public virtual int Count { get @@ -823,6 +832,9 @@ public virtual int Count } } + /// + /// Copies the elements of the PriorityQueue to an array, starting at a particular array index. + /// public virtual void CopyTo(Array array, int index) { #region Require @@ -865,6 +877,9 @@ public virtual void CopyTo(Array array, int index) } } + /// + /// Gets an object that can be used to synchronize access to the PriorityQueue. + /// public virtual object SyncRoot { get @@ -877,6 +892,9 @@ public virtual object SyncRoot #region IEnumerable Members + /// + /// Gets the enumerator for the Priority Queue, then returns the enumerator through a collection. + /// public virtual IEnumerator GetEnumerator() { return new PriorityQueueEnumerator(this); diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/IClock.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/IClock.cs index 049fc53d..bec9a87d 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/IClock.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/IClock.cs @@ -79,6 +79,9 @@ bool IsRunning get; } + /// + /// Determines the number of ticks. + /// int Ticks { get; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/MidiInternalClock.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/MidiInternalClock.cs index a39c6373..3f1b4b83 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/MidiInternalClock.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/MidiInternalClock.cs @@ -74,6 +74,12 @@ public MidiInternalClock() { } + /// + /// Initializes a new instance of MidiInternalClock class with a specified base and a newly named integer. + /// + /// + /// The timer period in which the MidiInternalClock will use to determine the amount of time. + /// public MidiInternalClock(int timerPeriod) : base(timerPeriod) { timer = TimerFactory.Create(); @@ -91,9 +97,7 @@ public MidiInternalClock(int timerPeriod) : base(timerPeriod) public MidiInternalClock(IContainer container) : this() { - /// - /// Required for Windows.Forms Class Composition Designer support - /// + // Required for Windows.Forms Class Composition Designer support container.Add(this); } @@ -203,6 +207,9 @@ public void Stop() OnStopped(EventArgs.Empty); } + /// + /// Sets the amount of ticks determined by the integer. + /// public void SetTicks(int ticks) { #region Require @@ -224,6 +231,9 @@ public void SetTicks(int ticks) Reset(); } + /// + /// Processes with the meta message determined, along with the tempo. + /// public void Process(MetaMessage message) { #region Require @@ -252,6 +262,9 @@ public void Process(MetaMessage message) #region Event Raiser Methods + /// + /// Disposes of the MidiInternalClock when closed. + /// protected virtual void OnDisposed(EventArgs e) { EventHandler handler = Disposed; @@ -318,6 +331,9 @@ public int Tempo } } + /// + /// Gets the ticks in microseconds per beat. + /// public override int Ticks { get @@ -332,8 +348,14 @@ public override int Ticks #region IComponent Members + /// + /// Initializes the Disposed event. + /// public event EventHandler Disposed; + /// + /// Initializes the Site functionality using ISite. + /// public ISite Site { get @@ -350,6 +372,9 @@ public ISite Site #region IDisposable Members + /// + /// Performs the main Dispose functionality for when the application is closed. + /// public void Dispose() { #region Guard diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/PpqnClock.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/PpqnClock.cs index 5a809cea..2ecc5fd2 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/PpqnClock.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Clocks/PpqnClock.cs @@ -77,14 +77,22 @@ public abstract class PpqnClock : IClock // The timer period. private readonly int timerPeriod; - - // Indicates whether the clock is running. + + /// + /// Indicates whether the clock is running. + /// protected bool running = false; #endregion #region Construction + /// + /// The PpqnClock determines how many ticks and timer period is used for the PPQN format. + /// + /// + /// The timerPeriod integer determines the amount of time there is. + /// protected PpqnClock(int timerPeriod) { #region Require @@ -107,11 +115,17 @@ protected PpqnClock(int timerPeriod) #region Methods + /// + /// Gets the tempos per beat. + /// protected int GetTempo() { return tempo; - } + } + /// + /// Sets the tempos to be used per beat. + /// protected void SetTempo(int tempo) { #region Require @@ -127,11 +141,17 @@ protected void SetTempo(int tempo) this.tempo = tempo; } + /// + /// Resets the amount of ticks. + /// protected void Reset() { fractionalTicks = 0; } + /// + /// Generates the amount of ticks. + /// protected int GenerateTicks() { int ticks = (fractionalTicks + periodResolution) / tempo; @@ -140,16 +160,25 @@ protected int GenerateTicks() return ticks; } + /// + /// Calculates the amount of time that the timer will have. + /// private void CalculatePeriodResolution() { periodResolution = ppqn * timerPeriod * MicrosecondsPerMillisecond; } + /// + /// Calculates the amount of ticks per clock. + /// private void CalculateTicksPerClock() { ticksPerClock = ppqn / PpqnMinValue; } + /// + /// An event that handles the ticks. + /// protected virtual void OnTick(EventArgs e) { EventHandler handler = Tick; @@ -160,6 +189,9 @@ protected virtual void OnTick(EventArgs e) } } + /// + /// An event that starts the PPQN Clock. + /// protected virtual void OnStarted(EventArgs e) { EventHandler handler = Started; @@ -170,6 +202,9 @@ protected virtual void OnStarted(EventArgs e) } } + /// + /// An event that stops the PPQN Clock. + /// protected virtual void OnStopped(EventArgs e) { EventHandler handler = Stopped; @@ -180,6 +215,9 @@ protected virtual void OnStopped(EventArgs e) } } + /// + /// An event that continues the PPQN Clock. + /// protected virtual void OnContinued(EventArgs e) { EventHandler handler = Continued; @@ -194,6 +232,9 @@ protected virtual void OnContinued(EventArgs e) #region Properties + /// + /// An integer that gets and sets the PPQN Clock value. + /// public int Ppqn { get @@ -219,11 +260,20 @@ public int Ppqn } } + /// + /// An abstract integer that gets the amount of ticks. + /// public abstract int Ticks { get; } + /// + /// An integer that determines the ticks per clock. + /// + /// + /// The amount of ticks per clock. + /// public int TicksPerClock { get @@ -238,14 +288,29 @@ public int TicksPerClock #region IClock Members + /// + /// This event occurs when PPQN Clock generates a tick. + /// public event System.EventHandler Tick; + /// + /// This event occurs when PPQN Clock is started and starts generating ticks. + /// public event System.EventHandler Started; + /// + /// This event occurs when PPQN Clock continues generating ticks. + /// public event System.EventHandler Continued; + /// + /// This event occurs when PPQN Clock has stopped. + /// public event System.EventHandler Stopped; + /// + /// Checks if PPQN Clock is running. + /// public bool IsRunning { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Construction.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Construction.cs index 7f1daa38..714c992c 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Construction.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Construction.cs @@ -65,6 +65,9 @@ public InputDevice(int deviceID, bool postEventsOnCreationContext = true, bool p PostDriverCallbackToDelegateQueue = postDriverCallbackToDelegateQueue; } + /// + /// The Input Device handler. + /// ~InputDevice() { if (!IsDisposed) diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Events.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Events.cs index 8fb1294c..27ee277b 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Events.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Events.cs @@ -2,6 +2,12 @@ namespace Sanford.Multimedia.Midi { + /// + /// Handles the MIDI Message Events. + /// + /// + /// This provides the basic functionality for all MIDI messages. + /// public delegate void MidiMessageEventHandler(IMidiMessage message); public partial class InputDevice @@ -25,20 +31,44 @@ public bool PostEventsOnCreationContext /// public event MidiMessageEventHandler MessageReceived; + /// + /// Occurs when a short message was received. + /// public event EventHandler ShortMessageReceived; + /// + /// Occurs when a channel message was received. + /// public event EventHandler ChannelMessageReceived; + /// + /// Occurs when a system ex message was received. + /// public event EventHandler SysExMessageReceived; + /// + /// Occurs when a system common message was received. + /// public event EventHandler SysCommonMessageReceived; + /// + /// Occurs when a system realtime message was received. + /// public event EventHandler SysRealtimeMessageReceived; + /// + /// Occurs when a invalid short message was received. + /// public event EventHandler InvalidShortMessageReceived; + /// + /// Occurs when a invalid system ex message message was received. + /// public event EventHandler InvalidSysExMessageReceived; + /// + /// Occurs when a short message was sent. + /// protected virtual void OnShortMessage(ShortMessageEventArgs e) { EventHandler handler = ShortMessageReceived; @@ -59,6 +89,9 @@ protected virtual void OnShortMessage(ShortMessageEventArgs e) } } + /// + /// Occurs when a message was received. + /// protected void OnMessageReceived(IMidiMessage message) { MidiMessageEventHandler handler = MessageReceived; @@ -79,6 +112,9 @@ protected void OnMessageReceived(IMidiMessage message) } } + /// + /// Occurs when a channel message is received. + /// protected virtual void OnChannelMessageReceived(ChannelMessageEventArgs e) { EventHandler handler = ChannelMessageReceived; @@ -99,6 +135,9 @@ protected virtual void OnChannelMessageReceived(ChannelMessageEventArgs e) } } + /// + /// Occurs when a system ex message is received. + /// protected virtual void OnSysExMessageReceived(SysExMessageEventArgs e) { EventHandler handler = SysExMessageReceived; @@ -119,6 +158,9 @@ protected virtual void OnSysExMessageReceived(SysExMessageEventArgs e) } } + /// + /// Occurs when a system common message is received. + /// protected virtual void OnSysCommonMessageReceived(SysCommonMessageEventArgs e) { EventHandler handler = SysCommonMessageReceived; @@ -139,6 +181,9 @@ protected virtual void OnSysCommonMessageReceived(SysCommonMessageEventArgs e) } } + /// + /// Occurs when a system realtime message is received. + /// protected virtual void OnSysRealtimeMessageReceived(SysRealtimeMessageEventArgs e) { EventHandler handler = SysRealtimeMessageReceived; @@ -159,6 +204,9 @@ protected virtual void OnSysRealtimeMessageReceived(SysRealtimeMessageEventArgs } } + /// + /// Occurs when an invalid short message is received. + /// protected virtual void OnInvalidShortMessageReceived(InvalidShortMessageEventArgs e) { EventHandler handler = InvalidShortMessageReceived; @@ -179,6 +227,9 @@ protected virtual void OnInvalidShortMessageReceived(InvalidShortMessageEventArg } } + /// + /// Occurs when an invalid system ex message is received. + /// protected virtual void OnInvalidSysExMessageReceived(InvalidSysExMessageEventArgs e) { EventHandler handler = InvalidSysExMessageReceived; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Messaging.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Messaging.cs index f4a48dfb..d05c1eda 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Messaging.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Messaging.cs @@ -66,7 +66,7 @@ public bool PostDriverCallbackToDelegateQueue set; } - int FLastParam2; + //int FLastParam2; private void HandleMessage(IntPtr hnd, int msg, IntPtr instance, IntPtr param1, IntPtr param2) { @@ -289,6 +289,9 @@ private void ReleaseBuffer(IntPtr headerPtr) Monitor.Pulse(lockObject); } + /// + /// Creates a system ex buffer for MIDI headers. + /// public int AddSysExBuffer() { int result; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Properties.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Properties.cs index e1b38e7e..0ac6e3a4 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Properties.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.Properties.cs @@ -39,6 +39,9 @@ namespace Sanford.Multimedia.Midi { public partial class InputDevice { + /// + /// Gets the Input Device handle. + /// public override IntPtr Handle { get @@ -47,6 +50,9 @@ public override IntPtr Handle } } + /// + /// Gets and sets the system ex buffer size. + /// public int SysExBufferSize { get @@ -68,6 +74,9 @@ public int SysExBufferSize } } + /// + /// Determines how many Input Devices there are. + /// public static int DeviceCount { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.PublicMethods.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.PublicMethods.cs index f89683ef..2c206881 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.PublicMethods.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.PublicMethods.cs @@ -40,6 +40,9 @@ namespace Sanford.Multimedia.Midi { public partial class InputDevice { + /// + /// Closes the MIDI input device. + /// public override void Close() { #region Guard @@ -54,6 +57,9 @@ public override void Close() Dispose(true); } + /// + /// Starts recording from the MIDI input device. + /// public void StartRecording() { #region Require @@ -109,6 +115,9 @@ public void StartRecording() } } + /// + /// Stops recording from the MIDI input device. + /// public void StopRecording() { #region Require @@ -144,6 +153,9 @@ public void StopRecording() } } + /// + /// Resets the MIDI input device. + /// public override void Reset() { #region Require @@ -180,7 +192,13 @@ public override void Reset() } } } - + + /// + /// Initializes the MIDI input device capabilities. + /// + /// + /// This will show the device ID for the MIDI input device. + /// public static MidiInCaps GetDeviceCapabilities(int deviceID) { int result; @@ -197,6 +215,9 @@ public static MidiInCaps GetDeviceCapabilities(int deviceID) return caps; } + /// + /// When closed, all connections to the MIDI input device are disposed. + /// public override void Dispose() { #region Guard diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.cs index 1170395b..2e1737f5 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/InputDevice Class/InputDevice.cs @@ -44,6 +44,9 @@ namespace Sanford.Multimedia.Midi /// public partial class InputDevice : MidiDevice { + /// + /// Disposes the data when closed. + /// protected override void Dispose(bool disposing) { if(disposing) diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDevice.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDevice.cs index 03d27445..6a952ca3 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDevice.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDevice.cs @@ -40,7 +40,7 @@ namespace Sanford.Multimedia.Midi { /// - /// The base class for all MIDI devices. + /// The base abstract class for all MIDI devices. /// public abstract class MidiDevice : Device { @@ -52,11 +52,13 @@ public abstract class MidiDevice : Device private static extern int midiConnect(IntPtr handleA, IntPtr handleB, IntPtr reserved); [DllImport("winmm.dll")] - private static extern int midiDisconnect(IntPtr handleA, IntPtr handleB, IntPtr reserved); + private static extern int midiDisconnect(IntPtr handleA, IntPtr handleB, IntPtr reserved); #endregion - // Size of the MidiHeader structure. + /// + /// Size of the MidiHeader structure. + /// protected static readonly int SizeOfMidiHeader; static MidiDevice() @@ -64,6 +66,9 @@ static MidiDevice() SizeOfMidiHeader = Marshal.SizeOf(typeof(MidiHeader)); } + /// + /// The main function for all MIDI devices. + /// public MidiDevice(int deviceID) : base(deviceID) { } diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDeviceException.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDeviceException.cs index 3b80ce2f..a153198d 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDeviceException.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/MidiDeviceException.cs @@ -43,14 +43,41 @@ public class MidiDeviceException : DeviceException { #region Error Codes + /// + /// This error occurs when the header is not prepared. + /// public const int MIDIERR_UNPREPARED = 64; /* header not prepared */ + /// + /// This error occurs when the MIDI player is still playing something. + /// public const int MIDIERR_STILLPLAYING = 65; /* still something playing */ + /// + /// This error occurs when there are no configured instruments. + /// public const int MIDIERR_NOMAP = 66; /* no configured instruments */ + /// + /// This error occurs when the hardware is still busy. + /// public const int MIDIERR_NOTREADY = 67; /* hardware is still busy */ + /// + /// This error occurs when the port is no longer connected. + /// public const int MIDIERR_NODEVICE = 68; /* port no longer connected */ + /// + /// This error occurs when there is an invalid MIF. + /// public const int MIDIERR_INVALIDSETUP = 69; /* invalid MIF */ + /// + /// This error occurs when the operation is unsupported with open mode. + /// public const int MIDIERR_BADOPENMODE = 70; /* operation unsupported w/ open mode */ + /// + /// This error occurs when the through device is eating up a message. + /// public const int MIDIERR_DONT_CONTINUE = 71; /* thru device 'eating' a message */ + /// + /// This error is the last error in range. + /// public const int MIDIERR_LASTERROR = 71; /* last error in range */ #endregion diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/NoOpEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/NoOpEventArgs.cs index ceed7af8..18674d64 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/NoOpEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/NoOpEventArgs.cs @@ -37,17 +37,23 @@ namespace Sanford.Multimedia.Midi { /// - /// + /// The event args class for no operations. /// public class NoOpEventArgs : EventArgs { private int data; + /// + /// The function for the no operation events. + /// public NoOpEventArgs(int data) { this.data = data; } + /// + /// Gets and returns the data. + /// public int Data { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDevice.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDevice.cs index 38e43d75..906b2ef2 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDevice.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDevice.cs @@ -79,8 +79,11 @@ public OutputDevice(int deviceID) : base(deviceID) } } - #endregion - + #endregion + + /// + /// When closed, disposes of the MIDI output device. + /// protected override void Dispose(bool disposing) { if(disposing) @@ -147,6 +150,9 @@ public override void Reset() base.Reset(); } + /// + /// Sends the MIDI output channel device message. + /// public override void Send(ChannelMessage message) { #region Require @@ -188,6 +194,9 @@ public override void Send(ChannelMessage message) } } + /// + /// Sends a system ex MIDI output device message. + /// public override void Send(SysExMessage message) { // System exclusive cancels running status. @@ -196,6 +205,9 @@ public override void Send(SysExMessage message) base.Send(message); } + /// + /// Sends a system common MIDI output device message. + /// public override void Send(SysCommonMessage message) { #region Require diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDeviceBase.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDeviceBase.cs index a76e997c..af7b650e 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDeviceBase.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputDeviceBase.cs @@ -41,65 +41,128 @@ namespace Sanford.Multimedia.Midi { + /// + /// This is an abstract class for MIDI output devices. + /// public abstract class OutputDeviceBase : MidiDevice { + /// + /// Handles resetting the MIDI output device. + /// [DllImport("winmm.dll")] protected static extern int midiOutReset(IntPtr DeviceHandle); + /// + /// Handles the MIDI output device short messages. + /// [DllImport("winmm.dll")] protected static extern int midiOutShortMsg(IntPtr DeviceHandle, int message); + /// + /// Handles preparing the headers for the MIDI output device. + /// [DllImport("winmm.dll")] protected static extern int midiOutPrepareHeader(IntPtr DeviceHandle, IntPtr headerPtr, int sizeOfMidiHeader); + /// + /// Handles unpreparing the headers for the MIDI output device. + /// [DllImport("winmm.dll")] protected static extern int midiOutUnprepareHeader(IntPtr DeviceHandle, IntPtr headerPtr, int sizeOfMidiHeader); + /// + /// Handles the MIDI output device long message. + /// [DllImport("winmm.dll")] protected static extern int midiOutLongMsg(IntPtr DeviceHandle, IntPtr headerPtr, int sizeOfMidiHeader); + /// + /// Obtains the MIDI output device caps. + /// [DllImport("winmm.dll")] protected static extern int midiOutGetDevCaps(IntPtr deviceID, ref MidiOutCaps caps, int sizeOfMidiOutCaps); + /// + /// Obtains the number of MIDI output devices. + /// [DllImport("winmm.dll")] protected static extern int midiOutGetNumDevs(); + /// + /// A construct integer that tells the compiler that hexadecimal value 0x3C7 means MOM_OPEN. + /// protected const int MOM_OPEN = 0x3C7; + + /// + /// A construct integer that tells the compiler that hexadecimal value 0x3C8 means MOM_CLOSE. + /// protected const int MOM_CLOSE = 0x3C8; + + /// + /// A construct integer that tells the compiler that hexadecimal value 0x3C9 means MOM_DONE. + /// protected const int MOM_DONE = 0x3C9; + /// + /// This delegate is a generic delegate for the MIDI output devices. + /// protected delegate void GenericDelegate(T args); - // Represents the method that handles messages from Windows. + /// + /// Represents the method that handles messages from Windows. + /// protected delegate void MidiOutProc(IntPtr hnd, int msg, IntPtr instance, IntPtr param1, IntPtr param2); - // For releasing buffers. + /// + /// For releasing buffers. + /// protected DelegateQueue delegateQueue = new DelegateQueue(); - + + /// + /// This object remains locked in place. + /// protected readonly object lockObject = new object(); - // The number of buffers still in the queue. + /// + /// The number of buffers still in the queue. + /// protected int bufferCount = 0; - // Builds MidiHeader structures for sending system exclusive messages. + /// + /// Builds MidiHeader structures for sending system exclusive messages. + /// private MidiHeaderBuilder headerBuilder = new MidiHeaderBuilder(); - // The device handle. + /// + /// The device handle. + /// protected IntPtr DeviceHandle = IntPtr.Zero; + /// + /// Base class for output devices with an integer. + /// + /// + /// Device ID is used here. + /// public OutputDeviceBase(int deviceID) : base(deviceID) { } + /// + /// Disposes when it has been closed. + /// ~OutputDeviceBase() { Dispose(false); } + /// + /// This dispose function will dispose all delegates that are queued when closed. + /// protected override void Dispose(bool disposing) { if(disposing) @@ -110,6 +173,9 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + /// + /// Sends the MIDI output channel device message. + /// public virtual void Send(ChannelMessage message) { #region Require @@ -124,6 +190,9 @@ public virtual void Send(ChannelMessage message) Send(message.Message); } + /// + /// Sends a short MIDI output channel device message. + /// public virtual void SendShort(int message) { #region Require @@ -138,6 +207,9 @@ public virtual void SendShort(int message) Send(message); } + /// + /// Sends a system ex MIDI output channel device message. + /// public virtual void Send(SysExMessage message) { #region Require @@ -188,6 +260,9 @@ public virtual void Send(SysExMessage message) } } + /// + /// Sends a system common MIDI output device message. + /// public virtual void Send(SysCommonMessage message) { #region Require @@ -202,6 +277,9 @@ public virtual void Send(SysCommonMessage message) Send(message.Message); } + /// + /// Sends a system realtime MIDI output device message. + /// public virtual void Send(SysRealtimeMessage message) { #region Require @@ -216,6 +294,9 @@ public virtual void Send(SysRealtimeMessage message) Send(message.Message); } + /// + /// Resets the MIDI output device. + /// public override void Reset() { #region Require @@ -245,8 +326,11 @@ public override void Reset() throw new OutputDeviceException(result); } } - } + } + /// + /// Sends a MIDI output device message. + /// protected void Send(int message) { lock(lockObject) @@ -260,6 +344,9 @@ protected void Send(int message) } } + /// + /// Initializes the MIDI output device capabilities. + /// public static MidiOutCaps GetDeviceCapabilities(int deviceID) { MidiOutCaps caps = new MidiOutCaps(); @@ -278,7 +365,9 @@ public static MidiOutCaps GetDeviceCapabilities(int deviceID) return caps; } - // Handles Windows messages. + /// + /// Handles Windows messages. + /// protected virtual void HandleMessage(IntPtr hnd, int msg, IntPtr instance, IntPtr param1, IntPtr param2) { if(msg == MOM_OPEN) @@ -293,7 +382,9 @@ protected virtual void HandleMessage(IntPtr hnd, int msg, IntPtr instance, IntPt } } - // Releases buffers. + /// + /// Releases buffers. + /// private void ReleaseBuffer(object state) { lock(lockObject) @@ -321,6 +412,9 @@ private void ReleaseBuffer(object state) } } + /// + /// When closed, disposes the object that is locked in place. + /// public override void Dispose() { #region Guard @@ -338,6 +432,9 @@ public override void Dispose() } } + /// + /// Handles the MIDI output device pointer. + /// public override IntPtr Handle { get @@ -346,6 +443,9 @@ public override IntPtr Handle } } + /// + /// Counts the number of MIDI output devices. + /// public static int DeviceCount { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputStream.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputStream.cs index b1c5947b..fd5dfae7 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputStream.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Device Classes/OutputDevice Classes/OutputStream.cs @@ -41,6 +41,9 @@ namespace Sanford.Multimedia.Midi { + /// + /// Sealed class stream for MIDI output devices. + /// public sealed class OutputStream : OutputDeviceBase { [DllImport("winmm.dll")] @@ -107,8 +110,14 @@ private struct Property private MidiHeaderBuilder headerBuilder = new MidiHeaderBuilder(); + /// + /// Handles the event for no operations. + /// public event EventHandler NoOpOccurred; + /// + /// Stream for MIDI output devices. + /// public OutputStream(int deviceID) : base(deviceID) { midiOutProc = HandleMessage; @@ -121,6 +130,9 @@ public OutputStream(int deviceID) : base(deviceID) } } + /// + /// Disposes the streams when closed. + /// protected override void Dispose(bool disposing) { if(disposing) @@ -144,8 +156,11 @@ protected override void Dispose(bool disposing) } base.Dispose(disposing); - } + } + /// + /// When the application is closed, this will dispose of any streams. + /// public override void Close() { #region Guard @@ -160,6 +175,9 @@ public override void Close() Dispose(true); } + /// + /// Starts playing the stream. + /// public void StartPlaying() { #region Require @@ -182,6 +200,9 @@ public void StartPlaying() } } + /// + /// Pauses playing the stream. + /// public void PausePlaying() { #region Require @@ -204,6 +225,9 @@ public void PausePlaying() } } + /// + /// Stops playing the stream. + /// public void StopPlaying() { #region Require @@ -226,6 +250,9 @@ public void StopPlaying() } } + /// + /// Resets the MIDI output device playing the stream. + /// public override void Reset() { #region Require @@ -243,6 +270,9 @@ public override void Reset() base.Reset(); } + /// + /// Writes to the MIDI output device stream. + /// public void Write(MidiEvent e) { switch(e.MidiMessage.MessageType) @@ -355,6 +385,9 @@ private void Write(int deltaTicks, MetaMessage message) } } + /// + /// Writes the no operation for MIDI output device streams. + /// public void WriteNoOp(int deltaTicks, int data) { // Delta time. @@ -371,6 +404,9 @@ public void WriteNoOp(int deltaTicks, int data) offsetTicks = 0; } + /// + /// Clears out all the MIDI output device streams when done. + /// public void Flush() { #region Require @@ -415,6 +451,9 @@ public void Flush() } } + /// + /// Initializes the amount of time for the MIDI output device stream. + /// public Time GetTime(TimeType type) { #region Require @@ -453,6 +492,9 @@ private void OnNoOpOccurred(NoOpEventArgs e) } } + /// + /// Handles the messages for the MIDI output device streams. + /// protected override void HandleMessage(IntPtr hnd, int msg, IntPtr instance, IntPtr param1, IntPtr param2) { if(msg == MOM_POSITIONCB) @@ -492,6 +534,9 @@ private void HandleNoOp(object state) } } + /// + /// Gets the size of the MIDI output device stream and sets the amount to be divided. + /// public int Division { get @@ -554,6 +599,9 @@ public int Division } } + /// + /// Gets the amount of tempo, then sets the tempo for the MIDI output device stream. + /// public int Tempo { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/GeneralMidi.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/GeneralMidi.cs index b5da3374..7dfe1918 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/GeneralMidi.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/GeneralMidi.cs @@ -39,133 +39,644 @@ namespace Sanford.Multimedia.Midi /// public enum GeneralMidiInstrument { + /// + /// Instrument sample: Acoustic Grand Piano. + /// AcousticGrandPiano, + + /// + /// Instrument sample: Bright Acoustic Piano. + /// BrightAcousticPiano, + + /// + /// Instrument sample: Electric Grand Piano. + /// ElectricGrandPiano, + + /// + /// Instrument sample: Honky Tonk Piano. + /// HonkyTonkPiano, + + /// + /// Instrument sample: Electric Piano 1. + /// ElectricPiano1, + + /// + /// Instrument sample: Electric Piano 2. + /// ElectricPiano2, + + /// + /// Instrument sample: Harpsichord. + /// Harpsichord, + + /// + /// Instrument sample: Clavinet. + /// Clavinet, + + /// + /// Instrument sample: Celesta. + /// Celesta, + + /// + /// Instrument sample: Glockenspiel. + /// Glockenspiel, + + /// + /// Instrument sample: Music Box. + /// MusicBox, + + /// + /// Instrument sample: Vibraphone. + /// Vibraphone, + + /// + /// Instrument sample: Marimba. + /// Marimba, + + /// + /// Instrument sample: Xylophone. + /// Xylophone, + + /// + /// Instrument sample: Tubular Bells. + /// TubularBells, + + /// + /// Instrument sample: Dulcimer. + /// Dulcimer, + + /// + /// Instrument sample: Drawbar Organ. + /// DrawbarOrgan, + + /// + /// Instrument sample: Percussive Organ. + /// PercussiveOrgan, + + /// + /// Instrument sample: Rock Organ. + /// RockOrgan, + + /// + /// Instrument sample: Church Organ. + /// ChurchOrgan, + + /// + /// Instrument sample: Reed Organ. + /// ReedOrgan, + + /// + /// Instrument sample: Accordion. + /// Accordion, + + /// + /// Instrument sample: Harmonica. + /// Harmonica, - TangoAccordion, - AcousticGuitarNylon, + + /// + /// Instrument sample: Tango Accordion. + /// + TangoAccordion, + + /// + /// Instrument sample: Acoustic Guitar Nylon. + /// + AcousticGuitarNylon, + + /// + /// Instrument sample: Acoustic Guitar Steel. + /// AcousticGuitarSteel, + + /// + /// Instrument sample: Electric Guitar Jazz. + /// ElectricGuitarJazz, + + /// + /// Instrument sample: Electric Guitar Clean. + /// ElectricGuitarClean, + + /// + /// Instrument sample: Electric Guitar Muted. + /// ElectricGuitarMuted, + + /// + /// Instrument sample: Overdriven Guitar. + /// OverdrivenGuitar, + + /// + /// Instrument sample: Distortion Guitar. + /// DistortionGuitar, + + /// + /// Instrument sample: Guitar Harmonics. + /// GuitarHarmonics, + + /// + /// Instrument sample: Acoustic Bass. + /// AcousticBass, + + /// + /// Instrument sample: Electric Bass Finger. + /// ElectricBassFinger, + + /// + /// Instrument sample: Electric Bass Pick. + /// ElectricBassPick, + + /// + /// Instrument sample: Fretless Bass. + /// FretlessBass, + + /// + /// Instrument sample: Slap Bass 1. + /// SlapBass1, + + /// + /// Instrument sample: Slap Bass 2. + /// SlapBass2, + + /// + /// Instrument sample: Synth Bass 1. + /// SynthBass1, + + /// + /// Instrument sample: Synth Bass 2. + /// SynthBass2, + + /// + /// Instrument sample: Violin. + /// Violin, + + /// + /// Instrument sample: Viola. + /// Viola, + + /// + /// Instrument sample: Cello. + /// Cello, + + /// + /// Instrument sample: Contrabass. + /// Contrabass, + + /// + /// Instrument sample: Tremolo Strings. + /// TremoloStrings, + + /// + /// Instrument sample: Pizzicato Strings. + /// PizzicatoStrings, + + /// + /// Instrument sample: Orchestral Harp. + /// OrchestralHarp, + + /// + /// Instrument sample: Timpani. + /// Timpani, + + /// + /// Instrument sample: String Ensemble 1. + /// StringEnsemble1, + + /// + /// Instrument sample: String Ensemble 2. + /// StringEnsemble2, + + /// + /// Instrument sample: Synth Strings 1. + /// SynthStrings1, + + /// + /// Instrument sample: Synth Strings 2. + /// SynthStrings2, + + /// + /// Instrument sample: Aah (Choir). + /// ChoirAahs, + + /// + /// Instrument sample: Ooh (Voice). + /// VoiceOohs, + + /// + /// Instrument sample: Synth Voice. + /// SynthVoice, + + /// + /// Instrument sample: Orchestra Hit. + /// OrchestraHit, + + /// + /// Instrument sample: Trumpet. + /// Trumpet, + + /// + /// Instrument sample: Trombone. + /// Trombone, + + /// + /// Instrument sample: Tuba. + /// Tuba, + + /// + /// Instrument sample: Muted Trumpet. + /// MutedTrumpet, + + /// + /// Instrument sample: French Horn. + /// FrenchHorn, + + /// + /// Instrument sample: Brass Section. + /// BrassSection, + + /// + /// Instrument sample: Synth Brass 1. + /// SynthBrass1, + + /// + /// Instrument sample: Synth Brass 2. + /// SynthBrass2, + + /// + /// Instrument sample: Soprano Saxophone. + /// SopranoSax, + + /// + /// Instrument sample: Alto Saxophone. + /// AltoSax, + + /// + /// Instrument sample: Tenor Saxophone. + /// TenorSax, + + /// + /// Instrument sample: Baritone Saxophone. + /// BaritoneSax, + + /// + /// Instrument sample: Oboe. + /// Oboe, + + /// + /// Instrument sample: English Horn. + /// EnglishHorn, + + /// + /// Instrument sample: Bassoon. + /// Bassoon, + + /// + /// Instrument sample: Clarinet. + /// Clarinet, + + /// + /// Instrument sample: Piccolo. + /// Piccolo, + + /// + /// Instrument sample: Flute. + /// Flute, + + /// + /// Instrument sample: Recorder. + /// Recorder, + + /// + /// Instrument sample: Pan Flute. + /// PanFlute, + + /// + /// Instrument sample: Blown Bottle. + /// BlownBottle, + + /// + /// Instrument sample: Shakuhachi. + /// Shakuhachi, + + /// + /// Instrument sample: Whistle. + /// Whistle, + + /// + /// Instrument sample: Ocarina. + /// Ocarina, + + /// + /// Instrument sample: Lead 1 (Square). + /// Lead1Square, + + /// + /// Instrument sample: Lead 2 (Sawtooth). + /// Lead2Sawtooth, + + /// + /// Instrument sample: Lead 3 (Calliope). + /// Lead3Calliope, + + /// + /// Instrument sample: Lead 4 (Chiff). + /// Lead4Chiff, + + /// + /// Instrument sample: Lead 5 (Charang). + /// Lead5Charang, + + /// + /// Instrument sample: Lead 6 (Voice). + /// Lead6Voice, + + /// + /// Instrument sample: Lead 7 (Fifths). + /// Lead7Fifths, + + /// + /// Instrument sample: Lead 8 (Bass And Lead). + /// Lead8BassAndLead, + + /// + /// Instrument sample: Pad 1 (New Age). + /// Pad1NewAge, + + /// + /// Instrument sample: Pad 2 (Warm). + /// Pad2Warm, + + /// + /// Instrument sample: Pad 3 (Polysynth). + /// Pad3Polysynth, + + /// + /// Instrument sample: Pad 4 (Choir). + /// Pad4Choir, + + /// + /// Instrument sample: Pad 5 (Bowed). + /// Pad5Bowed, + + /// + /// Instrument sample: Pad 6 (Metallic). + /// Pad6Metallic, + + /// + /// Instrument sample: Pad 7 (Halo). + /// Pad7Halo, + + /// + /// Instrument sample: Pad 8 (Sweep). + /// Pad8Sweep, + + /// + /// Instrument sample: Fx 1 (Rain). + /// Fx1Rain, + + /// + /// Instrument sample: Fx 2 (Soundtrack). + /// Fx2Soundtrack, + + /// + /// Instrument sample: Fx 3 (Crystal). + /// Fx3Crystal, + + /// + /// Instrument sample: Fx 4 (Atmosphere). + /// Fx4Atmosphere, + + /// + /// Instrument sample: Fx 5 (Brightness). + /// Fx5Brightness, + + /// + /// Instrument sample: Fx 6 (Goblins). + /// Fx6Goblins, + + /// + /// Instrument sample: Fx 7 (Echoes). + /// Fx7Echoes, + + /// + /// Instrument sample: Fx 8 (Sci-Fi). + /// Fx8SciFi, + + /// + /// Instrument sample: Sitar. + /// Sitar, + + /// + /// Instrument sample: Banjo. + /// Banjo, + + /// + /// Instrument sample: Shamisen. + /// Shamisen, + + /// + /// Instrument sample: Koto. + /// Koto, + + /// + /// Instrument sample: Kalimba. + /// Kalimba, + + /// + /// Instrument sample: Bag Pipe. + /// BagPipe, + + /// + /// Instrument sample: Fiddle. + /// Fiddle, + + /// + /// Instrument sample: Shanai. + /// Shanai, + + /// + /// Instrument sample: Tinkle Bell. + /// TinkleBell, + + /// + /// Instrument sample: Agogo. + /// Agogo, + + /// + /// Instrument sample: Steel Drums. + /// SteelDrums, + + /// + /// Instrument sample: Woodblock. + /// Woodblock, + + /// + /// Instrument sample: Taiko Drum. + /// TaikoDrum, + + /// + /// Instrument sample: Melodic Tom. + /// MelodicTom, + + /// + /// Instrument sample: Synth Drum. + /// SynthDrum, + + /// + /// Instrument sample: Reverse Cymbal. + /// ReverseCymbal, + + /// + /// Instrument sample: Guitar Fret Noise. + /// GuitarFretNoise, + + /// + /// Instrument sample: Breath Noise. + /// BreathNoise, + + /// + /// Instrument sample: Seashore. + /// Seashore, + + /// + /// Instrument sample: Bird Tweet. + /// BirdTweet, + + /// + /// Instrument sample: Telephone Ring. + /// TelephoneRing, + + /// + /// Instrument sample: Helicopter. + /// Helicopter, + + /// + /// Instrument sample: Applause. + /// Applause, + + /// + /// Instrument sample: Gunshot. + /// Gunshot } } \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ChannelMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ChannelMessageEventArgs.cs index f5ce09b6..fe5030a1 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ChannelMessageEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ChannelMessageEventArgs.cs @@ -4,16 +4,25 @@ namespace Sanford.Multimedia.Midi { + /// + /// The class that contains events for channel messages. + /// public class ChannelMessageEventArgs : MidiEventArgs { private ChannelMessage message; + /// + /// The function that contains events for channel messages. + /// public ChannelMessageEventArgs(ChannelMessage message, int absoluteTicks = -1) { this.message = message; this.AbsoluteTicks = absoluteTicks; } + /// + /// Gets the channel messages. + /// public ChannelMessage Message { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidShortMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidShortMessageEventArgs.cs index 6b87f2ec..086a3e08 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidShortMessageEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidShortMessageEventArgs.cs @@ -4,16 +4,25 @@ namespace Sanford.Multimedia.Midi { + /// + /// This class declares invalid short message events. + /// public class InvalidShortMessageEventArgs : MidiEventArgs { private int message; + /// + /// Main function for when the invalid short message event is declared. + /// public InvalidShortMessageEventArgs(int message, int absoluteTicks = -1) { this.message = message; this.AbsoluteTicks = absoluteTicks; } + /// + /// Gets and returns the message. + /// public int Message { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidSysExMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidSysExMessageEventArgs.cs index 81b297ec..34acfda3 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidSysExMessageEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/InvalidSysExMessageEventArgs.cs @@ -4,16 +4,25 @@ namespace Sanford.Multimedia.Midi { + /// + /// This class declares invalid exclusive system message events. + /// public class InvalidSysExMessageEventArgs : MidiEventArgs { private byte[] messageData; + /// + /// Main function for declared invalid exclusive system message events. + /// public InvalidSysExMessageEventArgs(byte[] messageData, int absoluteTicks = -1) { this.messageData = messageData; this.AbsoluteTicks = absoluteTicks; } + /// + /// Gets and returns the message data. + /// public ICollection MessageData { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MetaMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MetaMessageEventArgs.cs index d935b7cb..a7cb2b3c 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MetaMessageEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MetaMessageEventArgs.cs @@ -4,16 +4,25 @@ namespace Sanford.Multimedia.Midi { + /// + /// Class for declaring metadata message events. + /// public class MetaMessageEventArgs : MidiEventArgs { private MetaMessage message; + /// + /// Main function for declaring metadata message events. + /// public MetaMessageEventArgs(MetaMessage message, int absoluteTicks = -1) { this.message = message; this.AbsoluteTicks = absoluteTicks; } + /// + /// Gets and returns the message. + /// public MetaMessage Message { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MidiEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MidiEventArgs.cs index a1e0921c..cffd73c3 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MidiEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/MidiEventArgs.cs @@ -4,8 +4,14 @@ namespace Sanford.Multimedia.Midi { + /// + /// Class for MIDI events. + /// public class MidiEventArgs : EventArgs { + /// + /// Gets and sets the ticks for the MIDI events. + /// public int AbsoluteTicks { get; set; } } } diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ShortMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ShortMessageEventArgs.cs index 60f26ee9..873eb9c9 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ShortMessageEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/ShortMessageEventArgs.cs @@ -11,12 +11,18 @@ public class ShortMessageEventArgs : MidiEventArgs { ShortMessage message; + /// + /// A short message event that calculates the absolute ticks. + /// public ShortMessageEventArgs(ShortMessage message, int absoluteTicks = -1) { this.message = message; this.AbsoluteTicks = absoluteTicks; } + /// + /// A short message event that uses a timestamp and calculates the absolute ticks. + /// public ShortMessageEventArgs(int message, int timestamp = 0, int absoluteTicks = -1) { this.message = new ShortMessage(message); @@ -24,12 +30,18 @@ public ShortMessageEventArgs(int message, int timestamp = 0, int absoluteTicks = this.AbsoluteTicks = absoluteTicks; } + /// + /// A short message event that calculates the status byte, data 1 byte, data 2 byte, and absolute ticks. + /// public ShortMessageEventArgs(byte status, byte data1, byte data2, int absoluteTicks = -1) { this.message = new ShortMessage(status, data1, data2); this.AbsoluteTicks = absoluteTicks; } + /// + /// Gets and returns the message. + /// public ShortMessage Message { get @@ -38,16 +50,25 @@ public ShortMessage Message } } + /// + /// Returns the channel message event. + /// public static ShortMessageEventArgs FromChannelMessage(ChannelMessageEventArgs arg) { return new ShortMessageEventArgs(arg.Message); } + /// + /// Returns the common system message event. + /// public static ShortMessageEventArgs FromSysCommonMessage(SysCommonMessageEventArgs arg) { return new ShortMessageEventArgs(arg.Message); } + /// + /// Returns the realtime system message event. + /// public static ShortMessageEventArgs FromSysRealtimeMessage(SysRealtimeMessageEventArgs arg) { return new ShortMessageEventArgs(arg.Message); diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysCommonMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysCommonMessageEventArgs.cs index b7a8f4bb..21b670a2 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysCommonMessageEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysCommonMessageEventArgs.cs @@ -4,16 +4,25 @@ namespace Sanford.Multimedia.Midi { + /// + /// Class for system common message events. + /// public class SysCommonMessageEventArgs : MidiEventArgs { private SysCommonMessage message; + /// + /// Main function for system common message events. + /// public SysCommonMessageEventArgs(SysCommonMessage message, int absoluteTicks = -1) { this.message = message; this.AbsoluteTicks = absoluteTicks; } + /// + /// Gets and returns the message. + /// public SysCommonMessage Message { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysExMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysExMessageEventArgs.cs index 31288048..bcfa1786 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysExMessageEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysExMessageEventArgs.cs @@ -4,16 +4,25 @@ namespace Sanford.Multimedia.Midi { + /// + /// Class for exclusive system message events. + /// public class SysExMessageEventArgs : MidiEventArgs { private SysExMessage message; + /// + /// Main function for exclusive system message events. + /// public SysExMessageEventArgs(SysExMessage message, int absoluteTicks = -1) { this.message = message; this.AbsoluteTicks = absoluteTicks; } + /// + /// Gets and returns the message. + /// public SysExMessage Message { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysRealtimeMessageEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysRealtimeMessageEventArgs.cs index 32ef7269..146a0f63 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysRealtimeMessageEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/EventArgs/SysRealtimeMessageEventArgs.cs @@ -4,20 +4,44 @@ namespace Sanford.Multimedia.Midi { + /// + /// Class for system realtime message events. + /// public class SysRealtimeMessageEventArgs : EventArgs { + /// + /// Requests the start for the system realtime message event. + /// public static readonly SysRealtimeMessageEventArgs Start = new SysRealtimeMessageEventArgs(SysRealtimeMessage.StartMessage); + /// + /// Requests to continue for the system realtime message event. + /// public static readonly SysRealtimeMessageEventArgs Continue = new SysRealtimeMessageEventArgs(SysRealtimeMessage.ContinueMessage); + /// + /// Requests to stop for the system realtime message event. + /// public static readonly SysRealtimeMessageEventArgs Stop = new SysRealtimeMessageEventArgs(SysRealtimeMessage.StopMessage); + /// + /// Requests the clock for the system realtime message event. + /// public static readonly SysRealtimeMessageEventArgs Clock = new SysRealtimeMessageEventArgs(SysRealtimeMessage.ClockMessage); + /// + /// Requests the ticks for the system realtime message event. + /// public static readonly SysRealtimeMessageEventArgs Tick = new SysRealtimeMessageEventArgs(SysRealtimeMessage.TickMessage); + /// + /// Requests the active sense for the system realtime message event. + /// public static readonly SysRealtimeMessageEventArgs ActiveSense = new SysRealtimeMessageEventArgs(SysRealtimeMessage.ActiveSenseMessage); + /// + /// Requests to restart for the system realtime message event. + /// public static readonly SysRealtimeMessageEventArgs Reset = new SysRealtimeMessageEventArgs(SysRealtimeMessage.ResetMessage); private SysRealtimeMessage message; @@ -27,6 +51,9 @@ private SysRealtimeMessageEventArgs(SysRealtimeMessage message) this.message = message; } + /// + /// Gets and returns the message. + /// public SysRealtimeMessage Message { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/IMidiMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/IMidiMessage.cs index a41134d1..286311f8 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/IMidiMessage.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/IMidiMessage.cs @@ -41,16 +41,34 @@ namespace Sanford.Multimedia.Midi /// public enum MessageType { + /// + /// Channel messages. + /// Channel, + /// + /// Exclusive system messages. + /// SystemExclusive, + /// + /// Common system messages. + /// SystemCommon, + /// + /// Realtime system messages. + /// SystemRealtime, + /// + /// Metadata messages. + /// Meta, + /// + /// Short messages. + /// Short } diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/MetaTextBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/MetaTextBuilder.cs index e9809615..a468ad26 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/MetaTextBuilder.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/MetaTextBuilder.cs @@ -138,6 +138,9 @@ public MetaTextBuilder(MetaType type) /// /// The type of MetaMessage. /// + /// + /// The text string of MetaMessage. + /// /// /// If the MetaMessage type is not a text based type. /// diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TempoChangeBuilder.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TempoChangeBuilder.cs index 0c0cbc61..db5a3234 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TempoChangeBuilder.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/Message Builders/TempoChangeBuilder.cs @@ -77,7 +77,7 @@ public TempoChangeBuilder() /// Initialize a new instance of the TempoChangeBuilder class with the /// specified MetaMessage. /// - /// + /// /// The MetaMessage to use for initializing the TempoChangeBuilder class. /// /// @@ -87,9 +87,9 @@ public TempoChangeBuilder() /// The TempoChangeBuilder uses the specified MetaMessage to initialize /// its property values. /// - public TempoChangeBuilder(MetaMessage e) + public TempoChangeBuilder(MetaMessage m) { - Initialize(e); + Initialize(m); } #endregion @@ -99,23 +99,23 @@ public TempoChangeBuilder(MetaMessage e) /// /// Initializes the TempoChangeBuilder with the specified MetaMessage. /// - /// + /// /// The MetaMessage to use for initializing the TempoChangeBuilder. /// /// /// If the specified MetaMessage is not a tempo type. /// - public void Initialize(MetaMessage e) + public void Initialize(MetaMessage m) { #region Require - if(e == null) + if(m == null) { - throw new ArgumentNullException("e"); + throw new ArgumentNullException("m"); } - else if(e.MetaType != MetaType.Tempo) + else if(m.MetaType != MetaType.Tempo) { - throw new ArgumentException("Wrong meta message type.", "e"); + throw new ArgumentException("Wrong meta message type.", "m"); } #endregion @@ -125,12 +125,12 @@ public void Initialize(MetaMessage e) // If this platform uses little endian byte order. if(BitConverter.IsLittleEndian) { - int d = e.Length - 1; + int d = m.Length - 1; // Pack tempo. - for(int i = 0; i < e.Length; i++) + for(int i = 0; i < m.Length; i++) { - t |= e[d] << (Shift * i); + t |= m[d] << (Shift * i); d--; } } @@ -138,9 +138,9 @@ public void Initialize(MetaMessage e) else { // Pack tempo. - for(int i = 0; i < e.Length; i++) + for(int i = 0; i < m.Length; i++) { - t |= e[i] << (Shift * i); + t |= m[i] << (Shift * i); } } diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessageDispatcher.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessageDispatcher.cs index 92285c66..8f5cdeaa 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessageDispatcher.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MessageDispatcher.cs @@ -46,14 +46,29 @@ public class MessageDispatcher #region Events + /// + /// Handles dispatching the channel message. + /// public event EventHandler ChannelMessageDispatched; + /// + /// Handles dispatching the system ex message. + /// public event EventHandler SysExMessageDispatched; + /// + /// Handles dispatching the system common message. + /// public event EventHandler SysCommonMessageDispatched; + /// + /// Handles dispatching the system realtime message. + /// public event EventHandler SysRealtimeMessageDispatched; + /// + /// Handles dispatching the metadata message. + /// public event EventHandler MetaMessageDispatched; #endregion @@ -61,13 +76,17 @@ public class MessageDispatcher /// /// Dispatches IMidiMessages to their corresponding sink. /// - /// - /// The IMidiMessage to dispatch. + /// + /// The MidiEvent to dispatch. + /// + /// + /// The Track to dispatch. /// public void Dispatch(MidiEvent evt, Track track) { #region Require + // The IMidiMessage to dispatch. var message = evt.MidiMessage; if(message == null) @@ -131,6 +150,9 @@ public void Dispatch(MidiEvent evt, Track track) } } + /// + /// Dispatches the channel message. + /// protected virtual void OnChannelMessageDispatched(ChannelMessageEventArgs e, Track track) { EventHandler handler = ChannelMessageDispatched; @@ -141,6 +163,9 @@ protected virtual void OnChannelMessageDispatched(ChannelMessageEventArgs e, Tra } } + /// + /// Dispatches the system ex message. + /// protected virtual void OnSysExMessageDispatched(SysExMessageEventArgs e, Track track) { EventHandler handler = SysExMessageDispatched; @@ -151,6 +176,9 @@ protected virtual void OnSysExMessageDispatched(SysExMessageEventArgs e, Track t } } + /// + /// Dispatches the system common message. + /// protected virtual void OnSysCommonMessageDispatched(SysCommonMessageEventArgs e, Track track) { EventHandler handler = SysCommonMessageDispatched; @@ -161,6 +189,9 @@ protected virtual void OnSysCommonMessageDispatched(SysCommonMessageEventArgs e, } } + /// + /// Dispatches the system realtime message. + /// protected virtual void OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArgs e, Track track) { EventHandler handler = SysRealtimeMessageDispatched; @@ -171,6 +202,9 @@ protected virtual void OnSysRealtimeMessageDispatched(SysRealtimeMessageEventArg } } + /// + /// Dispatches the metadata message. + /// protected virtual void OnMetaMessageDispatched(MetaMessageEventArgs e, Track track) { EventHandler handler = MetaMessageDispatched; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/InputDeviceMidiEvents.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/InputDeviceMidiEvents.cs index a87a3abb..88ac6011 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/InputDeviceMidiEvents.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/InputDeviceMidiEvents.cs @@ -4,12 +4,15 @@ namespace Sanford.Multimedia.Midi { /// - /// MidiSignal provides all midi events from an input device + /// MidiSignal provides all midi events from an input device. /// public class InputDeviceMidiEvents : MidiEvents { readonly InputDevice FInDevice; + /// + /// Gets the device ID. + /// public int DeviceID { get { if (FInDevice != null) { @@ -31,11 +34,17 @@ public InputDeviceMidiEvents(InputDevice inDevice) FInDevice.StartRecording(); } + /// + /// Disposes the input device when closed. + /// public void Dispose() { FInDevice.Dispose(); } + /// + /// Initializes the MIDI events from device ID. + /// public static InputDeviceMidiEvents FromDeviceID(int deviceID) { var deviceCount = InputDevice.DeviceCount; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MergeMidiEvents.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MergeMidiEvents.cs index d9532853..ff0b7d9b 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MergeMidiEvents.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/MergeMidiEvents.cs @@ -8,6 +8,9 @@ namespace Sanford.Multimedia.Midi /// public class MergeMidiEvents : MidiEvents { + /// + /// Gets the device ID and returns with a value of -3. + /// public int DeviceID { get @@ -18,6 +21,9 @@ public int DeviceID readonly List FMidiEventsList = new List(); + /// + /// Merges the MIDI events. + /// public MergeMidiEvents(IEnumerable midiEvents) { foreach (var elem in midiEvents) @@ -27,6 +33,9 @@ public MergeMidiEvents(IEnumerable midiEvents) } } + /// + /// Gets and returns the MIDI event sources from the events list. + /// public IEnumerable EventSources { get @@ -35,10 +44,16 @@ public IEnumerable EventSources } } + /// + /// Disposes of the MergeMidiEvents when closed. + /// public void Dispose() { } + /// + /// Handles the event for when a MIDI message is received. + /// public event MidiMessageEventHandler MessageReceived { add @@ -57,6 +72,9 @@ public event MidiMessageEventHandler MessageReceived } } + /// + /// Handles the event for when a short message is received. + /// public event EventHandler ShortMessageReceived { add @@ -75,6 +93,9 @@ public event EventHandler ShortMessageReceived } } + /// + /// Handles the event for when a channel message is received. + /// public event EventHandler ChannelMessageReceived { add @@ -93,6 +114,9 @@ public event EventHandler ChannelMessageReceived } } + /// + /// Handles the event for when an exclusive system message is received. + /// public event EventHandler SysExMessageReceived { add @@ -111,6 +135,9 @@ public event EventHandler SysExMessageReceived } } + /// + /// Handles the event for when a common system message is received. + /// public event EventHandler SysCommonMessageReceived { add @@ -129,6 +156,9 @@ public event EventHandler SysCommonMessageReceived } } + /// + /// Handles the event for when a realtime system message is received. + /// public event EventHandler SysRealtimeMessageReceived { add diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/OutputDeviceEventSink.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/OutputDeviceEventSink.cs index 142c9931..d339df7b 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/OutputDeviceEventSink.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/MidiEvents/OutputDeviceEventSink.cs @@ -10,6 +10,9 @@ public class OutputDeviceEventSink : IDisposable readonly OutputDevice FOutDevice; readonly MidiEvents FEventSource; + /// + /// Gets the device ID and returns with a value of -1. + /// public int DeviceID { get @@ -25,6 +28,9 @@ public int DeviceID } } + /// + /// Initializes and registers the MIDI output device events. + /// public OutputDeviceEventSink(OutputDevice outDevice, MidiEvents eventSource) { FOutDevice = outDevice; @@ -104,6 +110,9 @@ public void Dispose() FOutDevice.Dispose(); } + /// + /// Sources and initializes the events for the MIDI output device. + /// public static OutputDeviceEventSink FromDeviceID(int deviceID, MidiEvents eventSource) { var deviceCount = OutputDevice.DeviceCount; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ShortMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ShortMessage.cs index 0abfdbd8..63acd74b 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ShortMessage.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/ShortMessage.cs @@ -36,6 +36,9 @@ namespace Sanford.Multimedia.Midi { + /// + /// Base abstract class for delta frames for MIDI messages. + /// public abstract class MidiMessageBase { /// @@ -65,8 +68,14 @@ public class ShortMessage : MidiMessageBase, IMidiMessage #region Constants + /// + /// The maximum value for data. + /// public const int DataMaxValue= 255; + /// + /// The maximum value for statuses. + /// public const int StatusMaxValue = 255; // @@ -74,6 +83,9 @@ public class ShortMessage : MidiMessageBase, IMidiMessage // private const int StatusMask = ~255; + /// + /// Bit manipulation constant for data mask. + /// protected const int DataMask = ~StatusMask; private const int Data1Mask = ~65280; private const int Data2Mask = ~Data1Mask + DataMask; @@ -81,6 +93,9 @@ public class ShortMessage : MidiMessageBase, IMidiMessage #endregion + /// + /// The message is set at 0. + /// protected int msg = 0; byte[] message; @@ -88,21 +103,33 @@ public class ShortMessage : MidiMessageBase, IMidiMessage #region Methods + /// + /// Gets and returns the bytes for the MIDI short message. + /// public byte[] GetBytes() { return Bytes; } + /// + /// Main function for MIDI short messages. + /// public ShortMessage() { //sub classes will fill the msg field } + /// + /// Initializes the message for the MIDI short message function. + /// public ShortMessage(int message) { this.msg = message; } + /// + /// Initializes the short message based on status and two bytes of data. + /// public ShortMessage(byte status, byte data1, byte data2) { this.message = new byte[] { status, data1, data2 }; @@ -233,6 +260,12 @@ public int Status } } + /// + /// Gets the bytes for the MIDI short message. + /// + /// + /// The message for the short message. + /// public byte[] Bytes { get @@ -246,6 +279,9 @@ public byte[] Bytes } } + /// + /// Gets the message type and returns the message type with a short message. + /// public virtual MessageType MessageType { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysExMessage.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysExMessage.cs index 115a9039..04b3e2e7 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysExMessage.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Messages/SysExMessage.cs @@ -109,12 +109,18 @@ public SysExMessage(byte[] data) this.data = new byte[data.Length]; data.CopyTo(this.data, 0); - } + } #endregion #region Methods + /// + /// Gets a byte array representation for the exclusive system message. + /// + /// + /// A clone of the byte array. + /// public byte[] GetBytes() { byte[] clone = new byte[data.Length]; @@ -124,11 +130,17 @@ public byte[] GetBytes() return clone; } + /// + /// Copies the data to a byte array buffer and index. + /// public void CopyTo(byte[] buffer, int index) { data.CopyTo(buffer, index); } + /// + /// Determines whenever the specified object is equal to the current object. + /// public override bool Equals(object obj) { #region Guard @@ -160,6 +172,9 @@ public override bool Equals(object obj) return equals; } + /// + /// Returns the hash code for the current object. + /// public override int GetHashCode() { return data.GetHashCode(); @@ -256,6 +271,9 @@ public MessageType MessageType #region IEnumerable Members + /// + /// Returns an enumerator for the exclusive system message. + /// public IEnumerator GetEnumerator() { return data.GetEnumerator(); diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelChaser.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelChaser.cs index feca5a68..d5f53d41 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelChaser.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelChaser.cs @@ -37,6 +37,9 @@ namespace Sanford.Multimedia.Midi { + /// + /// The class that contains the channel chaser functionality. + /// public class ChannelChaser { private ChannelMessage[,] controllerMessages; @@ -49,8 +52,14 @@ public class ChannelChaser private ChannelMessage[] polyPressureMessages; + /// + /// Handles the chased events. + /// public event EventHandler Chased; + /// + /// The main functions for ChannelChaser. + /// public ChannelChaser() { int c = ChannelMessage.MidiChannelMaxValue + 1; @@ -64,6 +73,9 @@ public ChannelChaser() polyPressureMessages = new ChannelMessage[c]; } + /// + /// For processing channel messages. + /// public void Process(ChannelMessage message) { switch(message.Command) @@ -90,6 +102,9 @@ public void Process(ChannelMessage message) } } + /// + /// Chases messages to an array so that it can determine between the MIDI channel value and data value, then detect the program change messages, pitch bend messages, channel pressure messages and poly pressure messages. + /// public void Chase() { ArrayList chasedMessages = new ArrayList(); @@ -138,6 +153,9 @@ public void Chase() OnChased(new ChasedEventArgs(chasedMessages)); } + /// + /// Resets all the channel chaser values. + /// public void Reset() { for(int c = 0; c <= ChannelMessage.MidiChannelMaxValue; c++) @@ -154,6 +172,9 @@ public void Reset() } } + /// + /// Handles the chased event. + /// protected virtual void OnChased(ChasedEventArgs e) { EventHandler handler = Chased; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelStopper.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelStopper.cs index a6635762..be04f65a 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelStopper.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChannelStopper.cs @@ -37,6 +37,9 @@ namespace Sanford.Multimedia.Midi { + /// + /// The ChannelStopper class, which provides pedal messages and sustenuto messages. + /// public class ChannelStopper { private ChannelMessage[,] noteOnMessage; @@ -49,8 +52,14 @@ public class ChannelStopper private ChannelMessageBuilder builder = new ChannelMessageBuilder(); + /// + /// Handles the stopped event. + /// public event EventHandler Stopped; + /// + /// This function contains the pedal messages and sustenuto messages. + /// public ChannelStopper() { int c = ChannelMessage.MidiChannelMaxValue + 1; @@ -63,6 +72,9 @@ public ChannelStopper() sustenutoMessage = new bool[c]; } + /// + /// Processes the channel message. + /// public void Process(ChannelMessage message) { switch(message.Command) @@ -122,6 +134,9 @@ public void Process(ChannelMessage message) } } + /// + /// Switches all sound off when stopped. + /// public void AllSoundOff() { ArrayList stoppedMessages = new ArrayList(); @@ -183,6 +198,9 @@ public void AllSoundOff() OnStopped(new StoppedEventArgs(stoppedMessages)); } + /// + /// Resets all the messages. + /// public void Reset() { for(int c = 0; c <= ChannelMessage.MidiChannelMaxValue; c++) @@ -198,6 +216,9 @@ public void Reset() } } + /// + /// Handles the event when the channels are stopped. + /// protected virtual void OnStopped(StoppedEventArgs e) { EventHandler handler = Stopped; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChasedEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChasedEventArgs.cs index 678d68b1..4acef681 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChasedEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/ChasedEventArgs.cs @@ -4,15 +4,24 @@ namespace Sanford.Multimedia.Midi { + /// + /// A class for chased events. + /// public class ChasedEventArgs : EventArgs { private ICollection messages; + /// + /// Main function for chased events. + /// public ChasedEventArgs(ICollection messages) { this.messages = messages; } + /// + /// Gets and returns messages. + /// public ICollection Messages { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/StoppedEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/StoppedEventArgs.cs index 07cbee74..d8e5c823 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/StoppedEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Processing/StoppedEventArgs.cs @@ -4,15 +4,24 @@ namespace Sanford.Multimedia.Midi { + /// + /// A class for stopped events. + /// public class StoppedEventArgs : EventArgs { private ICollection messages; + /// + /// Main function for stopped events. + /// public StoppedEventArgs(ICollection messages) { this.messages = messages; } + /// + /// Gets and returns messages. + /// public ICollection Messages { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiEvent.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiEvent.cs index cc3e84b9..3735ec34 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiEvent.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiEvent.cs @@ -36,6 +36,9 @@ namespace Sanford.Multimedia.Midi { + /// + /// A class for MIDI events. + /// public class MidiEvent { private object owner = null; @@ -86,6 +89,9 @@ internal object Owner } } + /// + /// Gets and returns the amount of absolute ticks. + /// public int AbsoluteTicks { get @@ -94,6 +100,9 @@ public int AbsoluteTicks } } + /// + /// Gets the amount of delta ticks from absolute ticks, subtracted from the previous absolute ticks, if the previous tick is not null; otherwise, obtains the amount of absolute ticks. + /// public int DeltaTicks { get @@ -113,6 +122,9 @@ public int DeltaTicks } } + /// + /// Gets and returns the MIDI message. + /// public IMidiMessage MidiMessage { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiFileProperties.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiFileProperties.cs index 0d00fd24..448c87b1 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiFileProperties.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/MidiFileProperties.cs @@ -43,10 +43,25 @@ namespace Sanford.Multimedia.Midi /// public enum SmpteFrameRate { - Smpte24 = 24, - Smpte25 = 25, + /// + /// 24 SMPTE Frames. + /// + Smpte24 = 24, + + /// + /// 25 SMPTE Frames. + /// + Smpte25 = 25, + + /// + /// 29 SMPTE Frames. + /// Smpte30Drop = 29, - Smpte30 = 30 + + /// + /// 30 SMPTE Frames. + /// + Smpte30 = 30 } /// @@ -54,7 +69,14 @@ public enum SmpteFrameRate /// public enum SequenceType { + /// + /// The PPQN Sequence Type. + /// Ppqn, + + /// + /// The SMPTE Sequence Type. + /// Smpte } @@ -375,8 +397,14 @@ public SequenceType SequenceType } } + /// + /// MIDI File Exception handles errors relating to the application being unable to read or write to a MIDI or Sequence. + /// public class MidiFileException : ApplicationException { + /// + /// The message that will display when an error occurs with a MIDI or Sequence format + /// public MidiFileException(string message) : base(message) { } diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/RecordingSession.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/RecordingSession.cs index 26c5f4f8..b952517b 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/RecordingSession.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/RecordingSession.cs @@ -4,6 +4,9 @@ namespace Sanford.Multimedia.Midi { + /// + /// This class initializes the recording sessions. + /// public class RecordingSession { private IClock clock; @@ -12,11 +15,17 @@ public class RecordingSession private Track result = new Track(); + /// + /// Main function for the recording sessions. + /// public RecordingSession(IClock clock) { this.clock = clock; } + /// + /// Builds the tracks, sorts and compares between a buffer and a timestamp, then creates a timestamped message with the amount of ticks. + /// public void Build() { result = new Track(); @@ -29,11 +38,17 @@ public void Build() } } + /// + /// Removes all elements from the list. + /// public void Clear() { buffer.Clear(); } + /// + /// Gets and returns the track result for the recording session. + /// public Track Result { get @@ -42,6 +57,9 @@ public Track Result } } + /// + /// Records a channel message if the clock is running. + /// public void Record(ChannelMessage message) { if(clock.IsRunning) @@ -50,6 +68,9 @@ public void Record(ChannelMessage message) } } + /// + /// Records an external system message if the clock is running. + /// public void Record(SysExMessage message) { if(clock.IsRunning) diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequence.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequence.cs index af6e534b..c325b095 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequence.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequence.cs @@ -68,12 +68,24 @@ public sealed class Sequence : IComponent, ICollection #region Events + /// + /// When the loading of the sequence is complete. + /// public event EventHandler LoadCompleted; + /// + /// When the loading of the sequence has changed. + /// public event ProgressChangedEventHandler LoadProgressChanged; + /// + /// When the sequence is saved. + /// public event EventHandler SaveCompleted; + /// + /// When the save progress for the sequence has changed. + /// public event ProgressChangedEventHandler SaveProgressChanged; #endregion @@ -250,6 +262,9 @@ public void Load(Stream fileStream) #endregion } + /// + /// Loads the sequence asynchronously. + /// public void LoadAsync(string fileName) { #region Require @@ -272,6 +287,9 @@ public void LoadAsync(string fileName) loadWorker.RunWorkerAsync(fileName); } + /// + /// Cancels loading the sequence asynchronously. + /// public void LoadAsyncCancel() { #region Require @@ -334,6 +352,9 @@ public void Save(Stream stream) } } + /// + /// Saves the sequence asynchronously. + /// public void SaveAsync(string fileName) { #region Require @@ -356,6 +377,9 @@ public void SaveAsync(string fileName) saveWorker.RunWorkerAsync(fileName); } + /// + /// Cancels saving the sequence asynchronously. + /// public void SaveAsyncCancel() { #region Require @@ -627,6 +651,9 @@ public SequenceType SequenceType } } + /// + /// If the loader is busy. + /// public bool IsBusy { get @@ -641,6 +668,9 @@ public bool IsBusy #region ICollection Members + /// + /// Adds an item to the sequence. + /// public void Add(Track item) { #region Require @@ -661,6 +691,9 @@ public void Add(Track item) properties.TrackCount = tracks.Count; } + /// + /// Removes all items from the sequence. + /// public void Clear() { #region Require @@ -677,6 +710,12 @@ public void Clear() properties.TrackCount = tracks.Count; } + /// + /// Determines whenever the sequence contains a specific value. + /// + /// + /// true, if the item is found in the sequence. Otherwise, it'll be false. + /// public bool Contains(Track item) { #region Require @@ -691,6 +730,9 @@ public bool Contains(Track item) return tracks.Contains(item); } + /// + /// Copies the elements of the sequence to an array, starting at a particular array index. + /// public void CopyTo(Track[] array, int arrayIndex) { #region Require @@ -705,6 +747,12 @@ public void CopyTo(Track[] array, int arrayIndex) tracks.CopyTo(array, arrayIndex); } + /// + /// Gets the number of elements contained in the sequence. + /// + /// + /// The number of elements in the sequence. + /// public int Count { get @@ -722,6 +770,12 @@ public int Count } } + /// + /// Gets a value indicating whenever the sequence is read-only. + /// + /// + /// true, if the sequence is read-only; otherwise, false. + /// public bool IsReadOnly { get @@ -739,6 +793,12 @@ public bool IsReadOnly } } + /// + /// Removes the first occurrence of a specific object from the sequence. + /// + /// + /// true, if the item was successfully removed from the sequence; otherwise false. This method also returns false if the item is not found in the original sequence. + /// public bool Remove(Track item) { #region Require @@ -764,6 +824,9 @@ public bool Remove(Track item) #region IEnumerable Members + /// + /// Returns an enumerator that iterates through the sequence. + /// public IEnumerator GetEnumerator() { #region Require @@ -800,8 +863,14 @@ IEnumerator IEnumerable.GetEnumerator() #region IComponent Members + /// + /// Handles disposing of the sequence when the application is closed. + /// public event EventHandler Disposed; + /// + /// Gets or sets the site associated with the sequence. + /// public ISite Site { get @@ -818,6 +887,9 @@ public ISite Site #region IDisposable Members + /// + /// Disposes the load when the application is closed. + /// public void Dispose() { #region Guard diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequencer.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequencer.cs index d19f33c0..0073d25e 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequencer.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Sequencer.cs @@ -4,6 +4,9 @@ namespace Sanford.Multimedia.Midi { + /// + /// This sequencer class allows for the sequencing of sequences. + /// public class Sequencer : IComponent { private Sequence sequence = null; @@ -30,8 +33,14 @@ public class Sequencer : IComponent #region Events + /// + /// Handles the event when the sequencer has finished playing the sequence. + /// public event EventHandler PlayingCompleted; + /// + /// Handles the event when a channel message is displayed when a sequence is played. + /// public event EventHandler ChannelMessagePlayed { add @@ -44,6 +53,9 @@ public event EventHandler ChannelMessagePlayed } } + /// + /// Handles the event when a system ex message is displayed when a sequence is played. + /// public event EventHandler SysExMessagePlayed { add @@ -56,6 +68,9 @@ public event EventHandler SysExMessagePlayed } } + /// + /// Handles the event when a metadata message is displayed when a sequence is played. + /// public event EventHandler MetaMessagePlayed { add @@ -68,6 +83,9 @@ public event EventHandler MetaMessagePlayed } } + /// + /// Handles the chased event in the sequencer. + /// public event EventHandler Chased { add @@ -80,6 +98,9 @@ public event EventHandler Chased } } + /// + /// Handles the event when sequencer stops playing. + /// public event EventHandler Stopped { add @@ -94,6 +115,9 @@ public event EventHandler Stopped #endregion + /// + /// The main sequencer function. + /// public Sequencer() { dispatcher.MetaMessageDispatched += delegate(object sender, MetaMessageEventArgs e) @@ -137,11 +161,17 @@ public Sequencer() }; } + /// + /// The function in which checks if the sequencer has been disposed. + /// ~Sequencer() { Dispose(false); } + /// + /// The method for disposing the sequencer when the application is closed. + /// protected virtual void Dispose(bool disposing) { if(disposing) @@ -159,6 +189,9 @@ protected virtual void Dispose(bool disposing) } } + /// + /// Starts the sequencer. + /// public void Start() { #region Require @@ -180,6 +213,9 @@ public void Start() } } + /// + /// Continues the sequencer. + /// public void Continue() { #region Require @@ -219,6 +255,9 @@ public void Continue() } } + /// + /// Stops the sequencer. + /// public void Stop() { #region Require @@ -247,6 +286,9 @@ public void Stop() } } + /// + /// Handles the event for when the sequencer is finished playing. + /// protected virtual void OnPlayingCompleted(EventArgs e) { EventHandler handler = PlayingCompleted; @@ -257,6 +299,9 @@ protected virtual void OnPlayingCompleted(EventArgs e) } } + /// + /// Handles the event for when the sequencer is disposed. + /// protected virtual void OnDisposed(EventArgs e) { EventHandler handler = Disposed; @@ -267,6 +312,9 @@ protected virtual void OnDisposed(EventArgs e) } } + /// + /// The sequencer's playing position of the sequence. + /// public int Position { get @@ -318,6 +366,9 @@ public int Position } } + /// + /// The loaded sequence that represents a series of tracks. + /// public Sequence Sequence { get @@ -349,8 +400,14 @@ public Sequence Sequence #region IComponent Members + /// + /// Handles the disposed event. + /// public event EventHandler Disposed; + /// + /// Gets the site and sets the site with a value. + /// public ISite Site { get @@ -367,6 +424,9 @@ public ISite Site #region IDisposable Members + /// + /// The dispose function for when the application is closed. + /// public void Dispose() { #region Guard diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Iterators.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Iterators.cs index 7115bb2c..02bfbb19 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Iterators.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Iterators.cs @@ -42,6 +42,9 @@ public sealed partial class Track { #region Iterators + /// + /// Main function for the track iterator. + /// public IEnumerable Iterator() { MidiEvent current = head; @@ -57,7 +60,10 @@ public IEnumerable Iterator() yield return current; } - + + /// + /// Dispatches the track iterator. + /// public IEnumerable DispatcherIterator(MessageDispatcher dispatcher) { IEnumerator enumerator = Iterator().GetEnumerator(); @@ -70,6 +76,9 @@ public IEnumerable DispatcherIterator(MessageDispatcher dispatcher) } } + /// + /// A track iterator for the amount of ticks. + /// public IEnumerable TickIterator(int startPosition, ChannelChaser chaser, MessageDispatcher dispatcher) { diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Test.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Test.cs index 57192eef..86f406ef 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Test.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.Test.cs @@ -5,6 +5,9 @@ namespace Sanford.Multimedia.Midi { public sealed partial class Track { + /// + /// Tests the tracks. + /// [Conditional("DEBUG")] public static void Test() { diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.cs index 0f7a8ad0..9b944992 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Midi/Sequencing/Track Classes/Track.cs @@ -68,6 +68,9 @@ public sealed partial class Track #region Construction + /// + /// Main function that represents the end of track MIDI event. + /// public Track() { endOfTrackMidiEvent = new MidiEvent(this, Length, MetaMessage.EndOfTrackMessage); @@ -91,12 +94,12 @@ public void Insert(int position, IMidiMessage message) { #region Require - if (position < 0) + if(position < 0) { throw new ArgumentOutOfRangeException("position", position, "IMidiMessage position out of range."); } - else if (message == null) + else if(message == null) { throw new ArgumentNullException("message"); } @@ -105,7 +108,7 @@ public void Insert(int position, IMidiMessage message) MidiEvent newMidiEvent = new MidiEvent(this, position, message); - if (head == null) + if(head == null) { head = newMidiEvent; tail = newMidiEvent; @@ -120,7 +123,7 @@ public void Insert(int position, IMidiMessage message) { newMidiEvent.Previous = tail; tail.Next = newMidiEvent; - tail = newMidiEvent; + tail = newMidiEvent; endOfTrackMidiEvent.SetAbsoluteTicks(Length); endOfTrackMidiEvent.Previous = tail; } @@ -173,7 +176,7 @@ public void Merge(Track trk) { #region Require - if (trk == null) + if(trk == null) { throw new ArgumentNullException("trk"); } @@ -182,11 +185,11 @@ public void Merge(Track trk) #region Guard - if (trk == this) + if(trk == this) { return; } - else if (trk.Count == 1) + else if(trk.Count == 1) { return; } @@ -205,7 +208,7 @@ public void Merge(Track trk) Debug.Assert(b != null); - if (a != null && a.AbsoluteTicks <= b.AbsoluteTicks) + if(a != null && a.AbsoluteTicks <= b.AbsoluteTicks) { current = new MidiEvent(this, a.AbsoluteTicks, a.MidiMessage); a = a.Next; @@ -218,9 +221,9 @@ public void Merge(Track trk) head = current; - while (a != null && b != null) + while(a != null && b != null) { - while (a != null && a.AbsoluteTicks <= b.AbsoluteTicks) + while(a != null && a.AbsoluteTicks <= b.AbsoluteTicks) { current.Next = new MidiEvent(this, a.AbsoluteTicks, a.MidiMessage); current.Next.Previous = current; @@ -228,9 +231,9 @@ public void Merge(Track trk) a = a.Next; } - if (a != null) + if(a != null) { - while (b != null && b.AbsoluteTicks <= a.AbsoluteTicks) + while(b != null && b.AbsoluteTicks <= a.AbsoluteTicks) { current.Next = new MidiEvent(this, b.AbsoluteTicks, b.MidiMessage); current.Next.Previous = current; @@ -240,7 +243,7 @@ public void Merge(Track trk) } } - while (a != null) + while(a != null) { current.Next = new MidiEvent(this, a.AbsoluteTicks, a.MidiMessage); current.Next.Previous = current; @@ -248,7 +251,7 @@ public void Merge(Track trk) a = a.Next; } - while (b != null) + while(b != null) { current.Next = new MidiEvent(this, b.AbsoluteTicks, b.MidiMessage); current.Next.Previous = current; @@ -284,11 +287,11 @@ public void RemoveAt(int index) { #region Require - if (index < 0) + if(index < 0) { throw new ArgumentOutOfRangeException("index", index, "Track index out of range."); } - else if (index == Count - 1) + else if(index == Count - 1) { throw new ArgumentException("Cannot remove the end of track event.", "index"); } @@ -297,7 +300,7 @@ public void RemoveAt(int index) MidiEvent current = GetMidiEvent(index); - if (current.Previous != null) + if(current.Previous != null) { current.Previous.Next = current.Next; } @@ -308,7 +311,7 @@ public void RemoveAt(int index) head = head.Next; } - if (current.Next != null) + if(current.Next != null) { current.Next.Previous = current.Previous; } @@ -346,7 +349,7 @@ public MidiEvent GetMidiEvent(int index) { #region Require - if (index < 0 || index >= Count) + if(index < 0 || index >= Count) { throw new ArgumentOutOfRangeException("index", index, "Track index out of range."); @@ -356,17 +359,17 @@ public MidiEvent GetMidiEvent(int index) MidiEvent result; - if (index == Count - 1) + if(index == Count - 1) { result = endOfTrackMidiEvent; } else { - if (index < Count / 2) + if(index < Count / 2) { result = head; - for (int i = 0; i < index; i++) + for(int i = 0; i < index; i++) { result = result.Next; } @@ -375,7 +378,7 @@ public MidiEvent GetMidiEvent(int index) { result = tail; - for (int i = Count - 2; i > index; i--) + for(int i = Count - 2; i > index; i--) { result = result.Previous; } @@ -385,7 +388,7 @@ public MidiEvent GetMidiEvent(int index) #region Ensure #if(DEBUG) - if (index == Count - 1) + if(index == Count - 1) { Debug.Assert(result.AbsoluteTicks == Length); Debug.Assert(result.MidiMessage == MetaMessage.EndOfTrackMessage); @@ -394,7 +397,7 @@ public MidiEvent GetMidiEvent(int index) { MidiEvent t = head; - for (int i = 0; i < index; i++) + for(int i = 0; i < index; i++) { t = t.Next; } @@ -408,19 +411,22 @@ public MidiEvent GetMidiEvent(int index) return result; } + /// + /// A MIDI event that moves the track. + /// public void Move(MidiEvent e, int newPosition) { #region Require - if (e.Owner != this) + if(e.Owner != this) { throw new ArgumentException("MidiEvent does not belong to this Track."); } - else if (newPosition < 0) + else if(newPosition < 0) { throw new ArgumentOutOfRangeException("newPosition"); } - else if (e == endOfTrackMidiEvent) + else if(e == endOfTrackMidiEvent) { throw new InvalidOperationException( "Cannot move end of track message. Use the EndOfTrackOffset property instead."); @@ -431,43 +437,43 @@ public void Move(MidiEvent e, int newPosition) MidiEvent previous = e.Previous; MidiEvent next = e.Next; - if (e.Previous != null && e.Previous.AbsoluteTicks > newPosition) + if(e.Previous != null && e.Previous.AbsoluteTicks > newPosition) { e.Previous.Next = e.Next; - if (e.Next != null) + if(e.Next != null) { e.Next.Previous = e.Previous; } - while (previous != null && previous.AbsoluteTicks > newPosition) + while(previous != null && previous.AbsoluteTicks > newPosition) { next = previous; previous = previous.Previous; - } + } } - else if (e.Next != null && e.Next.AbsoluteTicks < newPosition) + else if(e.Next != null && e.Next.AbsoluteTicks < newPosition) { e.Next.Previous = e.Previous; - if (e.Previous != null) + if(e.Previous != null) { e.Previous.Next = e.Next; } - while (next != null && next.AbsoluteTicks < newPosition) + while(next != null && next.AbsoluteTicks < newPosition) { previous = next; next = next.Next; } } - if (previous != null) + if(previous != null) { previous.Next = e; } - if (next != null) + if(next != null) { next.Previous = e; } @@ -476,14 +482,14 @@ public void Move(MidiEvent e, int newPosition) e.Next = next; e.SetAbsoluteTicks(newPosition); - if (newPosition < head.AbsoluteTicks) + if(newPosition < head.AbsoluteTicks) { head = e; } - if (newPosition > tail.AbsoluteTicks) + if(newPosition > tail.AbsoluteTicks) { - tail = e; + tail = e; } endOfTrackMidiEvent.SetAbsoluteTicks(Length); @@ -503,17 +509,17 @@ private void AssertValid() MidiEvent current = head; int ticks = 1; - while (current != null) + while(current != null) { ticks += current.DeltaTicks; - if (current.Previous != null) + if(current.Previous != null) { Debug.Assert(current.AbsoluteTicks >= current.Previous.AbsoluteTicks); Debug.Assert(current.DeltaTicks == current.AbsoluteTicks - current.Previous.AbsoluteTicks); } - if (current.Next == null) + if(current.Next == null) { Debug.Assert(tail == current); } @@ -575,7 +581,7 @@ public int EndOfTrackOffset { #region Require - if (value < 0) + if(value < 0) { throw new ArgumentOutOfRangeException("EndOfTrackOffset", value, "End of track offset out of range."); @@ -604,4 +610,4 @@ public object SyncRoot #endregion } -} \ No newline at end of file +} diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ITimer.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ITimer.cs index 5ab564f1..6804f33c 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ITimer.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/ITimer.cs @@ -28,6 +28,9 @@ namespace Sanford.Multimedia.Timers { + /// + /// This provides the functionality for the timer. + /// public interface ITimer : IComponent { /// diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Time.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Time.cs index 5621e7df..f802bf78 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Time.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Time.cs @@ -42,12 +42,30 @@ namespace Sanford.Multimedia.Timers /// public enum TimeType { + /// + /// Defined in milliseconds. + /// Milliseconds = 0x0001, - Samples = 0x0002, - Bytes = 0x0004, - Smpte = 0x0008, - Midi = 0x0010, - Ticks = 0x0020 + /// + /// Defined in samples. + /// + Samples = 0x0002, + /// + /// Defined in bytes. + /// + Bytes = 0x0004, + /// + /// Defined in SMPTE. + /// + Smpte = 0x0008, + /// + /// Defined in MIDI. + /// + Midi = 0x0010, + /// + /// Defined in ticks. + /// + Ticks = 0x0020 } /// @@ -56,18 +74,33 @@ public enum TimeType [StructLayout(LayoutKind.Explicit)] public struct Time { + /// + /// Type. + /// [FieldOffset(0)] public int type; + /// + /// Milliseconds. + /// [FieldOffset(4)] public int milliseconds; + /// + /// Samples. + /// [FieldOffset(4)] public int samples; + /// + /// Byte count. + /// [FieldOffset(4)] public int byteCount; + /// + /// Ticks. + /// [FieldOffset(4)] public int ticks; @@ -75,34 +108,61 @@ public struct Time // SMPTE // + /// + /// SMPTE hours. + /// [FieldOffset(4)] - public byte hours; + public byte hours; + /// + /// SMPTE minutes. + /// [FieldOffset(5)] - public byte minutes; + public byte minutes; + /// + /// SMPTE seconds. + /// [FieldOffset(6)] - public byte seconds; + public byte seconds; + /// + /// SMPTE frames. + /// [FieldOffset(7)] - public byte frames; + public byte frames; + /// + /// SMPTE frames per second. + /// [FieldOffset(8)] - public byte framesPerSecond; + public byte framesPerSecond; + /// + /// SMPTE dummy. + /// [FieldOffset(9)] - public byte dummy; + public byte dummy; + /// + /// SMPTE pad 1. + /// [FieldOffset(10)] - public byte pad1; + public byte pad1; + /// + /// SMPTE pad 2. + /// [FieldOffset(11)] public byte pad2; - + // // MIDI // + /// + /// MIDI song position pointer. + /// [FieldOffset(4)] public int songPositionPointer; } diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Timer.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Timer.cs index fc148e97..c09c4f9b 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Timer.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia.Timers/Timer.cs @@ -71,6 +71,9 @@ public struct TimerCaps /// public int periodMax; + /// + /// The default timer capabilities. + /// public static TimerCaps Default { get @@ -195,9 +198,7 @@ static Timer() /// public Timer(IContainer container) { - /// - /// Required for Windows.Forms Class Composition Designer support - /// + // Required for Windows.Forms Class Composition Designer support container.Add(this); Initialize(); diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/ErrorEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/ErrorEventArgs.cs index 8a1f32cb..b7fdd0ce 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/ErrorEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/ErrorEventArgs.cs @@ -4,15 +4,27 @@ namespace Sanford.Multimedia { + /// + /// This will handle any errors relating to Sanford.Multimedia. + /// public class ErrorEventArgs : EventArgs { private Exception ex; + /// + /// This represents the error itself. + /// public ErrorEventArgs(Exception ex) { this.ex = ex; } + /// + /// Displays the error. + /// + /// + /// The error that is associated with the issue. + /// public Exception Error { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Key.cs b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Key.cs index 5e526296..dfd313e9 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Key.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Multimedia/Key.cs @@ -1,4 +1,4 @@ -#region License +#region License /* Copyright (c) 2006 Leslie Sanford * @@ -39,35 +39,154 @@ namespace Sanford.Multimedia /// public enum Key { + /// + /// The A♭ (A-Flat) Minor sequenced note. + /// AFlatMinor, + + /// + /// The E♭ (E-Flat) Minor sequenced note. + /// EFlatMinor, + + /// + /// The B♭ (B-Flat) Minor sequenced note. + /// BFlatMinor, + + /// + /// The F Minor sequenced note. + /// FMinor, + + /// + /// The C Minor sequenced note. + /// CMinor, + + /// + /// The G Minor sequenced note. + /// GMinor, + + /// + /// The D Minor sequenced note. + /// DMinor, + + /// + /// The A Minor sequenced note. + /// AMinor, + + /// + /// The E Minor sequenced note. + /// EMinor, + + /// + /// The B Minor sequenced note. + /// BMinor, + + /// + /// The F♯ (F-Sharp) Minor sequenced note. + /// FSharpMinor, + + /// + /// The C♯ (C-Sharp) Minor sequenced note. + /// CSharpMinor, + + /// + /// The G♯ (G-Sharp) Minor sequenced note. + /// GSharpMinor, + + /// + /// The D♯ (D-Sharp) Minor sequenced note. + /// DSharpMinor, + + /// + /// The A♯ (A-Sharp) Minor sequenced note. + /// ASharpMinor, + + /// + /// The C♭ (C-Flat) Major sequenced note. + /// CFlatMajor, + + /// + /// The G♭ (G-Flat) Major sequenced note. + /// GFlatMajor, + + /// + /// The D♭ (D-Flat) Major sequenced note. + /// DFlatMajor, + + /// + /// The A♭ (A-Flat) Major sequenced note. + /// AFlatMajor, + + /// + /// The E♭ (E-Flat) Major sequenced note. + /// EFlatMajor, + + /// + /// The B♭ (B-Flat) Major sequenced note. + /// BFlatMajor, + + /// + /// The F Major sequenced note. + /// FMajor, + + /// + /// The C Major sequenced note. + /// CMajor, + + /// + /// The G Major sequenced note. + /// GMajor, + + /// + /// The D Major sequenced note. + /// DMajor, + + /// + /// The A Major sequenced note. + /// AMajor, + + /// + /// The E Major sequenced note. + /// EMajor, + + /// + /// The B Major sequenced note. + /// BMajor, + + /// + /// The F♯ (F-Sharp) Major sequenced note. + /// FSharpMajor, + + /// + /// The C♯ (C-Sharp) Major sequenced note. + /// CSharpMajor, } } \ No newline at end of file diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/AsyncResult.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/AsyncResult.cs index 17e120df..ef8dd84c 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Threading/AsyncResult.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/AsyncResult.cs @@ -140,6 +140,9 @@ public object Owner #region IAsyncResult Members + /// + /// This object provides the async state. + /// public object AsyncState { get @@ -148,6 +151,9 @@ public object AsyncState } } + /// + /// This handles the waiting time for the async. + /// public WaitHandle AsyncWaitHandle { get @@ -156,6 +162,9 @@ public WaitHandle AsyncWaitHandle } } + /// + /// Determines whenever the async completed synchronously or not. + /// public bool CompletedSynchronously { get @@ -164,6 +173,9 @@ public bool CompletedSynchronously } } + /// + /// Determines if the async has completed. + /// public bool IsCompleted { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.cs index 8c51bf27..3def8c6b 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/DelegateQueue.cs @@ -115,14 +115,15 @@ public DelegateQueue() /// public DelegateQueue(IContainer container) { - /// - /// Required for Windows.Forms Class Composition Designer support - /// + // Required for Windows.Forms Class Composition Designer support container.Add(this); InitializeDelegateQueue(); } + /// + /// Checks if DelegateQueue has been disposed. + /// ~DelegateQueue() { Dispose(false); @@ -156,6 +157,9 @@ private void InitializeDelegateQueue() #region Methods + /// + /// Disposes of DelegateQueue when closed. + /// protected virtual void Dispose(bool disposing) { if(disposing) @@ -503,8 +507,10 @@ private void DelegateProcedure() Debug.WriteLine(delegateThread.Name + " Finished"); } - - // Raises the InvokeCompleted event. + + /// + /// Raises the InvokeCompleted event. + /// protected virtual void OnInvokeCompleted(InvokeCompletedEventArgs e) { EventHandler handler = InvokeCompleted; @@ -518,7 +524,9 @@ protected virtual void OnInvokeCompleted(InvokeCompletedEventArgs e) } } - // Raises the PostCompleted event. + /// + /// Raises the PostCompleted event. + /// protected virtual void OnPostCompleted(PostCompletedEventArgs e) { EventHandler handler = PostCompleted; @@ -532,7 +540,9 @@ protected virtual void OnPostCompleted(PostCompletedEventArgs e) } } - // Raises the Disposed event. + /// + /// Raises the Disposed event. + /// protected virtual void OnDisposed(EventArgs e) { EventHandler handler = Disposed; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/PostCompletedEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/PostCompletedEventArgs.cs index 85504780..26e72726 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/PostCompletedEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateQueue/PostCompletedEventArgs.cs @@ -38,16 +38,25 @@ namespace Sanford.Threading { + /// + /// This class is used when the async events have been completed. + /// public class PostCompletedEventArgs : AsyncCompletedEventArgs { private SendOrPostCallback callback; + /// + /// Main function for post completed events. + /// public PostCompletedEventArgs(SendOrPostCallback callback, Exception error, object state) : base(error, false, state) { this.callback = callback; } + /// + /// Gets and returns the callback. + /// public SendOrPostCallback Callback { get diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/DelegateScheduler.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/DelegateScheduler.cs index f2973a51..85066579 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/DelegateScheduler.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/DelegateScheduler.cs @@ -103,9 +103,7 @@ public DelegateScheduler() /// public DelegateScheduler(IContainer container) { - /// - /// Required for Windows.Forms Class Composition Designer support - /// + // Required for Windows.Forms Class Composition Designer support container.Add(this); Initialize(); @@ -117,6 +115,9 @@ private void Initialize() timer.Elapsed += new ElapsedEventHandler(HandleElapsed); } + /// + /// Checks if the DelegateScheduler has been disposed. + /// ~DelegateScheduler() { Dispose(false); @@ -126,6 +127,9 @@ private void Initialize() #region Methods + /// + /// Disposes of DelegateScheduler when closed. + /// protected virtual void Dispose(bool disposing) { if(disposing) @@ -438,7 +442,9 @@ private void HandleElapsed(object sender, ElapsedEventArgs e) } } - // Raises the Disposed event. + /// + /// Raises the Disposed event. + /// protected virtual void OnDisposed(EventArgs e) { EventHandler handler = Disposed; @@ -449,7 +455,9 @@ protected virtual void OnDisposed(EventArgs e) } } - // Raises the InvokeCompleted event. + /// + /// Raises the InvokeCompleted event. + /// protected virtual void OnInvokeCompleted(InvokeCompletedEventArgs e) { EventHandler handler = InvokeCompleted; @@ -531,8 +539,14 @@ public ISynchronizeInvoke SynchronizingObject #region IComponent Members + /// + /// When the event has been disposed. + /// public event System.EventHandler Disposed; + /// + /// Gets and returns the site, sets the site with a value. + /// public ISite Site { get @@ -549,6 +563,9 @@ public ISite Site #region IDisposable Members + /// + /// The main dispose function that occurs when disposed. + /// public void Dispose() { #region Guard diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/Task.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/Task.cs index 19bc54eb..28af998b 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/Task.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/DelegateScheduler/Task.cs @@ -38,6 +38,9 @@ namespace Sanford.Threading { + /// + /// Indicates the tasks to be compared. + /// public class Task : IComparable { #region Task Members @@ -112,6 +115,9 @@ internal object Invoke(DateTime signalTime) return returnValue; } + /// + /// Initializes returns the arguments. + /// public object[] GetArgs() { return args; @@ -121,6 +127,9 @@ public object[] GetArgs() #region Properties + /// + /// Gets and returns the next timeout. + /// public DateTime NextTimeout { get @@ -129,6 +138,9 @@ public DateTime NextTimeout } } + /// + /// Gets and returns the count. + /// public int Count { get @@ -137,6 +149,9 @@ public int Count } } + /// + /// Gets and returns the method. + /// public Delegate Method { get @@ -145,6 +160,9 @@ public Delegate Method } } + /// + /// Gets and returns the timeout in milliseconds. + /// public int MillisecondsTimeout { get @@ -159,6 +177,13 @@ public int MillisecondsTimeout #region IComparable Members + + /// + /// Compares the current instance with another object of the same type and returns an integer indicates whenever the current instance precedes, follows, or occurs in the same position in the sort order as the other object. + /// + /// + /// Compares between the subtracted next timeout and the task. + /// public int CompareTo(object obj) { Task t = obj as Task; diff --git a/Sanford.Multimedia.Midi.Core/Sanford.Threading/InvokeCompletedEventArgs.cs b/Sanford.Multimedia.Midi.Core/Sanford.Threading/InvokeCompletedEventArgs.cs index 8e69cf3e..83e0a114 100644 --- a/Sanford.Multimedia.Midi.Core/Sanford.Threading/InvokeCompletedEventArgs.cs +++ b/Sanford.Multimedia.Midi.Core/Sanford.Threading/InvokeCompletedEventArgs.cs @@ -49,6 +49,21 @@ public class InvokeCompletedEventArgs : AsyncCompletedEventArgs private object result; + /// + /// Represents the delegate, objects and exceptions for the InvokeCompleted event. + /// + /// + /// Represents the delegate method used. + /// + /// + /// For any args to be used. + /// + /// + /// For any results that occur. + /// + /// + /// For any errors that may occur. + /// public InvokeCompletedEventArgs(Delegate method, object[] args, object result, Exception error) : base(error, false, null) { @@ -57,11 +72,17 @@ public InvokeCompletedEventArgs(Delegate method, object[] args, object result, E this.result = result; } + /// + /// Initializes the args as an object. + /// public object[] GetArgs() { return args; } + /// + /// Initializes method as a delegate. + /// public Delegate Method { get @@ -70,6 +91,9 @@ public Delegate Method } } + /// + /// Initializes result as an object. + /// public object Result { get diff --git a/WinForms/Properties/Icon48.png b/WinForms/Properties/Icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..4eb3e79cc24883b466fc38e31fe8dc910bbef28a GIT binary patch literal 870 zcmV-s1DX7ZP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y9E;jv63FrU-0@q1IK~!i%)s;(b)IbzPvz@=Ulgwz8Z$%O}M^(|9s26EyQAP zkiWmXA6kTn#oqka0g%4=d@}}U5h50QQUe?f5sN)Z0gi--#a>8WcmT$GLd0S(Y5)(R zF>|&AiN#)|0N??{+90vmgE{$2TnZA4J=hyS%!N2B^x&wxNFgSJUbG4THN@ngUZem= zLV1X@!&;L>KYjc#N^q~eHN1HM3A6|i4_$(Vl{!bL?+5GY`IH2)~CMqceHLx5ayq4Bxr6CuSm#i_4P?;-I^fGo84ru zxWhhmT+fSi=}UYmK;>nh zlarvHm){xy=6e5{L0-^zdEi1ya3yh(p1xio z`xGZ~Yq%)yAzX;-XaHc}rU8;X90}m^HNX*hm;^}Tq6UD6hldlj=cK3aftmzJ(&Fyj z-#^qOcZsE^PyL8Ii~*9gK%RSx04@(l^y>$x0H6@uB>*m%&^(a5gsHt+06<;=gbOBoE+paB z@(_!l1C!hwF3q|s%)O;N#G+6L2O-HhwI=b_!j)KbJcOiN6kJ{bdy9)$tmsJsa!4j& wr|!{{EjABN%T4^6cbP8a*t5dZ)H07*qoM6N<$g3Q5&p8x;= literal 0 HcmV?d00001 diff --git a/WinForms/Properties/Icon528.png b/WinForms/Properties/Icon528.png new file mode 100644 index 0000000000000000000000000000000000000000..75601de2ef2e68898be2f817784b750d79ffbeb2 GIT binary patch literal 10133 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 PydL+1rG@ihln?(8zw=kQ literal 0 HcmV?d00001 diff --git a/WinForms/Properties/Next.png b/WinForms/Properties/Next.png new file mode 100644 index 0000000000000000000000000000000000000000..dc839b50f18c9d65802cc276b8eaa1c7eeabce56 GIT binary patch literal 4026 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%0D$qRH; zyXtyZUEit5-_fJ&^J>Zk+;?M@Ps28yCFwT*4$p8uFn$-gRj>RKK}M5_qTQppzwt`b z9N0fuD%6E11`d4!NrL7hyD!T#fu^dJ^oKGfnFLdH(4~cTpQv7b+197fUcTZq*S13w z{uG@;u^0{`sMJFl!}NjZ&}TWsoe6RF0kWn|CFsC^`M}UPlCjMLT|@Av73bi9Piv5! Mwd1L(lYUqK2WetO7XSbN literal 0 HcmV?d00001 diff --git a/WinForms/Properties/Pause.ico b/WinForms/Properties/Pause.ico new file mode 100644 index 0000000000000000000000000000000000000000..30b143620803667ca703b20759d56c50743d8b20 GIT binary patch literal 118062 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 literal 0 HcmV?d00001 diff --git a/WinForms/Properties/Pause.png b/WinForms/Properties/Pause.png new file mode 100644 index 0000000000000000000000000000000000000000..9a736deda0138a1282a6d9c36f6f0b9190b9839c GIT binary patch literal 1450 zcmeAS@N?(olHy`uVBq!ia0vp^b3mAb4M=wFIOGeY7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0h7I;J!GcfQS24TkI`72U@f)XXJ5hcO-X(i=}MX3x0iJ5sNdU>fO3MP7n zdWOkbbG#WCSfxE(978JN-d=KCC!8+ens{PDYRg`OQ(ZHZZ!n*laW%EgMIk8em;3dg zov&YL=GmXJwNS0yq!E&PXHK}kzyB}$e?MMVmMmtQ&Lk4)YMxb~=`b;LsBu@ya`jwT zyY4}dwTSq_=-7ix%QW79Qg*j(7?&a-#1@!_X4 zQ}5;4<=H*?95>(Wy?MS_cHHyN&jVso%%`WXNne+~{{4dY`>sAUUtiwv{>7Ps%CwpK zv%j`a|5mp7+4h=$GF{zoo_*^1>ho(|^;)=!^Rwk==l|Su{CiE^-9P&xKTkTi7HHD- z_q&cGn`3YOn*aZk&vO5teSUv-Jy)LX?z-!?diyK)GS|4Lx1BaVEivcd*%P!P?!NdW z?^^%8r{|z7@4HX$m+qHtI4#ItUvcaK&pS8lbe`L>2Yqbc6L#2a?l~xV2dH(35WRZ; z1HY7?KOu4F;sd?^aldvm?vN?`(4Nk7?m=D&b~-JE=UqktKh2}z?1`Fte{R&Aer~(( z`RCWq!dbV^i{1ZPZu6oEZlKI zx$WM?nGb`k1;iKO%OP8x3k7#w6^-EvND#fd;^0yS*RYGdQb2CX5aX83i~GyIsW5h$ Uqzzvruqb8lboFyt=akR{092E?-2eap literal 0 HcmV?d00001 diff --git a/WinForms/Properties/Play.ico b/WinForms/Properties/Play.ico new file mode 100644 index 0000000000000000000000000000000000000000..e8be3d8757c7a4f9b418a3650f1cd6c69278d3db GIT binary patch literal 140062 zcmeI5U925d7036b?d2n_F;>L1@*=27BTCe0#0L_6z;Fv8 zg!)3_7r`{SiHYa~?Sm*0NFYR#!b2gUsli|%rXoTR`B2(I;rd^5X3beMXU{$No-=39 z?3w>g{&&w=XU<-G*8ZI}d-k1kj&oCPjXU#9hkDk1XSH)*cFt|s(0hF6>CQa@+w5%b z@$azT=RV-v=FREji=BJzLg((gFWvw7Dfi3II(G@$z^-VqXGi56+W9kHD{65$AIJ8= zF^=zRcIfYGD6&(T^2!y>Md-)E}DmCp!KO_TWpKY$v(Qj)RJeAsvOc$lE0E8 zL(b|zo=Fase}+atPD5Lu>7jd$wu9u?!_d#5kB-*FlCP5X{{ew^cLe$dwA@^bU56Ii zzW6u}9e}n%r$FPDV*BDl^iOCH^dV@RQr5oscn^94x&}HG8l^1Nz8=ee(eMIv;Rp`u zY=4sRnbi1BlFv!Kzb2UMhvrgYTWs>2 z>?8Z)HN|%1vCP9}q3w`dM`neOr%1$js{9`rAJqukARXNv1 zb3=>zm$}_BXg^fdTzIH+>|p)t{8Hvpw?J~KJL%%{Fa40Y;z8(3(5Q21ymV+O^RKaZ z6D{n6J^@V)ZEfW{4D+x3_#2x3J|y$6D$8>HOF!O${ses*!s==zWhMSK7XOClpFn3- z(1V!#*BSr1_^a#vS7-jIEC1Gc{;Bi)Tj%_vf)vIk7y(AW7Xhpll$RMibt*IkC94c` zsMkXlz_>rl2%aS_)k5DpebfDvE>7y(8g)(FVj z!6etFCLNcLz=7Pe6>`0?%47N%8v7UYBJ^V@#Cj#%R92+_FF~@dbrO0U+6BeJlP?6G+SBm^esmjW* z{_9x00*`k>qulROS!X>BEw}%29I~Ex2>L2?2GoSIlK$&h{0|-f4RkqF#r<02#9@^F z>v8-Yj(-H5H%m*aRBdKCHsG!2C=V?BSxJb#FF{ucB6Io9i!Sg#*ry?&2*{UrAFtJv2MG1X;Z z1Q-EEfDvE>7=ckE(7T05Za9)sL46`^=lFcDz2kFfjeD4+{o||BI=T4?kKKIFp4{Lh zkL6}3sj*{H_il(f?yzI>7(4cAd{j&J!wd5EbU%HawxjLNrR}DEwIA9q?I(6i+Qn{C z>v7>x^0=Z}?$eSFfn10GSm}LQ+n<^m_&k|7r5FP;KnBPF86X2>fDDiUGC&5%02v?y zWPl8i0Wv@a$N(8217v^fDDiUGC&5%02v?y zWPl8i0Wv@a$N(8217v^fDDiUGC&5%02v?y zWPl8i0Wv@a$N(8217v^>5DU18cR?QK7 zailE_!(gc@UVXXt8MJ?u0GpOp1YHodTq%91? z6XI2Kt;`Q}j_@+JZ-n%|8DUnpu^CSxn_<}W#Tk@LBEKrHTH9Rv7<_IsbViqJeQ~5M z48wtVEzS`n=iUXKn{pe~^%&Ud-TPTwlECW%&X>Gn`;lD zPdgwjjmx!UGYp%)I0K40^Q-bIb@3jW1=wa%Xl30?uE`ovd~u{L48s7RDZDDXGDi@0 z>$Vc7J=86Y%`SLN2u5gtZAw<@%#Yn>zb;z(Nu9e3Jpi2~5 z)WPNmWHSt#zBpr?W8qcvt-T(54gLQiB;Q#lMdk<-zlX*bN7}+LjPZ?)SLN2u5uU(t zdDdi2D2*6GQTF=^8W=5=al(1 z%da-4^8c|7=eEQ7mG`?eoK4NCw(cOW#p@~FPZ$xm9plxmrSg95h&a_8+Yw&v^%Or3 z6CtM^;k9_~=jRP0W>uJNaelSe{fBX!{5*H$thSU_$*FSwmp3E7FQWyF>Rh0OyxN?~ z?;~m%t1aTy-kWk1d6nPy)K?Gl@6&84tC90+&;9(q;ubS1oT`wIV*7l7 zJ@@nb;LTX-n6|l&h}Yt|{{Z^V-`gOsvJTWV*>gXC&!}UJ+Ve%j^Q+CN{JppibE?cX zm{*%qA4WgGXB@1r>PXONl=^4dCH?YUpB{qKb~njw!ar<%U=daK6&VPG7pULUaM z{%6qsRc5TC=2VY^`Lz}KRdTAXrM`{r+n`E+7h8F$yC$#E^J;VI1K{a0!&EDBsyQCc z2jZO%NIsRkDs$8qpxuxb;Z(1!)Ygic`8B%yTD+dR8z62-VT@`mb(A*{d5wx!d+tAo z@xIcG`D(M>+D-Br1+Vtp|0-a-2~s{=d+ztW>BwuNc`cs%_hSqO&Z` zEoR_4ckVaGo^6uXM(`^6RCDT4Y~PR#kaL|q_nR1Go8+~2UgfhWvgR+^2VDS3k@fy) z?n_O29N-Umt(DiaF~l+lcoLd3!_9NQ35MMyueI_zjj^~IYW(N?EHia(lh;~#wL{OG zYNJuIOfDDiUGC&5% z02v?yWPl8i0Wv@a$N(8217v^fDDiUGC&5% z02v?yWPl8i0Wv@a$N(8217v^fDDiUGC&5% z02v?yWPl8i0Wv@a$N(8=Czb6l`zj*E76T(qm>qJ7nTuct-3IxgDP@qj&bJYZkzZ{MYk z2ljUh$qNSUy-*L@&o9}3eTjY53hia>l=&^|=YrpFSscpZRu<=;+kXDs)BAP5vT-aM z_cBh(xJum?{i@rdy^M2pTijpW7VYY`V9(qZ?3vqwJ#$;IUy+QJ{K@mS%Wp5sOMSQb zcKUAf_P*P^z3(<>H*Rxw<2GkEZgcjs^MY|}_qT4XeR@8NPA+a`=QZorwqxBId(N$~ z=iC~5&aJWM+#37SruU}*x6b+PcjV0dcJ`LokCoW-=6*Z07b4Jmn7tnh&(hBLSR_W^ F;r}DLY(@Y8 literal 0 HcmV?d00001 diff --git a/WinForms/Properties/Play.png b/WinForms/Properties/Play.png new file mode 100644 index 0000000000000000000000000000000000000000..f386fd325480bec804b628fe9fb3016a349159db GIT binary patch literal 2298 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 literal 0 HcmV?d00001 diff --git a/WinForms/Properties/Previous.ico b/WinForms/Properties/Previous.ico new file mode 100644 index 0000000000000000000000000000000000000000..fe2ef73e77e4f7b2ae6c6c6f1701a2b5ffdaa4bc GIT binary patch literal 140062 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 literal 0 HcmV?d00001 diff --git a/WinForms/Properties/Previous.png b/WinForms/Properties/Previous.png new file mode 100644 index 0000000000000000000000000000000000000000..63496267cc5cabedf50440f1a3b095a96bef2eb6 GIT binary patch literal 3708 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

-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 y2S{H_$3dd9qdC6QL6S$6aNP{V$@my literal 0 HcmV?d00001 diff --git a/WinForms/Properties/Resources.Designer.cs b/WinForms/Properties/Resources.Designer.cs new file mode 100644 index 00000000..1aa1551a --- /dev/null +++ b/WinForms/Properties/Resources.Designer.cs @@ -0,0 +1,133 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Kermalis.VGMusicStudio.Properties { + using System; + + + ///

+ /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Icon { + get { + object obj = ResourceManager.GetObject("Icon", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconNext { + get { + object obj = ResourceManager.GetObject("IconNext", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconPause { + get { + object obj = ResourceManager.GetObject("IconPause", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconPlay { + get { + object obj = ResourceManager.GetObject("IconPlay", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap IconPlaylist { + get { + object obj = ResourceManager.GetObject("IconPlaylist", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconPrevious { + get { + object obj = ResourceManager.GetObject("IconPrevious", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap IconSong { + get { + object obj = ResourceManager.GetObject("IconSong", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/WinForms/Properties/Resources.resx b/WinForms/Properties/Resources.resx new file mode 100644 index 00000000..ad871cdc --- /dev/null +++ b/WinForms/Properties/Resources.resx @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Properties\Icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Next.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Pause.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Play.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Properties\Playlist.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Previous.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Properties\Song.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/WinForms/Properties/Settings.Designer.cs b/WinForms/Properties/Settings.Designer.cs new file mode 100644 index 00000000..875ce0f2 --- /dev/null +++ b/WinForms/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Kermalis.VGMusicStudio.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/WinForms/Properties/Settings.settings b/WinForms/Properties/Settings.settings new file mode 100644 index 00000000..39645652 --- /dev/null +++ b/WinForms/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/WinForms/Properties/Song.png b/WinForms/Properties/Song.png new file mode 100644 index 0000000000000000000000000000000000000000..62a132e5d49752cc1cccbe330aeb2d0148cbbbe5 GIT binary patch literal 2328 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fo{p?&#~tz_78O`%fY(0|PTd zfKP}kP<#}OhQNpofz1YAIzSnvB*-uL|HuXm!Qa_cfeC@Lz$3Dlfr0N32s4Umcr^e8 zB}-f*N`mv#O3D+9QW**oGxJLH@={9_O!N%&j0`_2WKIPZFtwg8jv*CsZzo2UNgML8 z)c*Ijo)pUWc+W*0mSvt&vG%eDopq-6pMMm=5Gls3a5{-GB~qtBr<-*`>PCi5I${pf zjxwFtM3!V<(vSKBCs^(FJ^U`f#`pY}dWf4rp4s;O3(Oi47w`Ndy{alr&>)XLh&SDQ z#p#U<*y)*W2kJlE{Ch_00r$SWQ&+OuJp6roM%)4Bd3O$TG3AABXlsxbU8D5CjP*6g zj?BxgEO!S!pGKNV$N@@~(PhBHgA zi%i(Dm32av#S-aTM?elJ|KdE~;565Xs5?v^rZIvIZh_ZCCv4dXQliANHYtK(Wt(); z1??7)fFj3PC6EAj(*>PFx0r;sZ3h~_-m);(el<78`+T5{*Yb13&VRW2PCftBY#-Hx z)pZ>F(pUVgX80ap=34$xGN8sObv`F+OxS_d`Wkx|Y&$)LX&3K@-_omgpV3@!nPLAw zr^@M`tTCn^YR=pLYmD0s41!&ayct_hZ($PRxsWlxlC_2R!n1}>RuQl2$xr&hXKx+F2TK_3_8pUAIw$eq^lBZY2LX)nvFVbGyV-s$ zX)k5j6MkU+?+DKW+nD}cIUU6$_u(gZmf$qu3B90j+3gtHM?Lyy!43W&mzlYEUlqNR zJQFy3&(BAp*NP7(=-ohPwmj#pOOgVT*ped}#h^^R(r|P7q#2Hq{47KA- zC+aK|xb8k_Vc%RYaqA>72Z&$mceb|_bNF`XSK$kFb&DBqg%>Q8;a0Ft{LPY~0o34k z!;Vqv6i}t`HvfjUBAtdeEhtLT#TdU_PG(Hee9j7m!dhW5<%;T#*D5wrKJTRFl(EI}sMiX +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Kermalis.VGMusicStudio.Properties +{ + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + + /// + /// Informs whenever an audio output device was removed. + /// + internal static string AudioDeviceRemoved + { + get + { + return ResourceManager.GetString("AudioDeviceRemoved", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} key. + /// + internal static string ConfigKeySubkey + { + get + { + return ResourceManager.GetString("ConfigKeySubkey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Would you like to stop playing the current playlist?. + /// + internal static string EndPlaylistBody + { + get + { + return ResourceManager.GetString("EndPlaylistBody", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid command in track {0} at 0x{1:X}: 0x{2:X}. + /// + internal static string ErrorAlphaDreamDSEMP2KSDATInvalidCommand + { + get + { + return ResourceManager.GetString("ErrorAlphaDreamDSEMP2KSDATInvalidCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot copy invalid game code "{0}". + /// + internal static string ErrorAlphaDreamMP2KCopyInvalidGameCode + { + get + { + return ResourceManager.GetString("ErrorAlphaDreamMP2KCopyInvalidGameCode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game code "{0}" is missing.. + /// + internal static string ErrorAlphaDreamMP2KMissingGameCode + { + get + { + return ResourceManager.GetString("ErrorAlphaDreamMP2KMissingGameCode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error parsing game code "{0}" in "{1}"{2}. + /// + internal static string ErrorAlphaDreamMP2KParseGameCode + { + get + { + return ResourceManager.GetString("ErrorAlphaDreamMP2KParseGameCode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Playlist "{0}" has song {1} defined more than once between decimal and hexadecimal.. + /// + internal static string ErrorAlphaDreamMP2KSongRepeated + { + get + { + return ResourceManager.GetString("ErrorAlphaDreamMP2KSongRepeated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" count must be the same as "{1}" count.. + /// + internal static string ErrorAlphaDreamMP2KSongTableCounts + { + get + { + return ResourceManager.GetString("ErrorAlphaDreamMP2KSongTableCounts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" must be True or False.. + /// + internal static string ErrorBoolParse + { + get + { + return ResourceManager.GetString("ErrorBoolParse", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Color {0} has an invalid key.. + /// + internal static string ErrorConfigColorInvalidKey + { + get + { + return ResourceManager.GetString("ErrorConfigColorInvalidKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Color {0} is not defined.. + /// + internal static string ErrorConfigColorMissing + { + get + { + return ResourceManager.GetString("ErrorConfigColorMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Color {0} is defined more than once between decimal and hexadecimal.. + /// + internal static string ErrorConfigColorRepeated + { + get + { + return ResourceManager.GetString("ErrorConfigColorRepeated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" is invalid.. + /// + internal static string ErrorConfigKeyInvalid + { + get + { + return ResourceManager.GetString("ErrorConfigKeyInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" is missing.. + /// + internal static string ErrorConfigKeyMissing + { + get + { + return ResourceManager.GetString("ErrorConfigKeyMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" must have at least one entry.. + /// + internal static string ErrorConfigKeyNoEntries + { + get + { + return ResourceManager.GetString("ErrorConfigKeyNoEntries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown header version: 0x{0:X}. + /// + internal static string ErrorDSEInvalidHeaderVersion + { + get + { + return ResourceManager.GetString("ErrorDSEInvalidHeaderVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid key in track {0} at 0x{1:X}: {2}. + /// + internal static string ErrorDSEInvalidKey + { + get + { + return ResourceManager.GetString("ErrorDSEInvalidKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There are no "bgm(NNNN).smd" files.. + /// + internal static string ErrorDSENoSequences + { + get + { + return ResourceManager.GetString("ErrorDSENoSequences", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading Global Config. + /// + internal static string ErrorGlobalConfig + { + get + { + return ResourceManager.GetString("ErrorGlobalConfig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading Song {0}. + /// + internal static string ErrorLoadSong + { + get + { + return ResourceManager.GetString("ErrorLoadSong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid running status command in track {0} at 0x{1:X}: 0x{2:X}. + /// + internal static string ErrorMP2KInvalidRunningStatusCommand + { + get + { + return ResourceManager.GetString("ErrorMP2KInvalidRunningStatusCommand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Too many nested call events in track {0}. + /// + internal static string ErrorMP2KSDATNestedCalls + { + get + { + return ResourceManager.GetString("ErrorMP2KSDATNestedCalls", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading GBA ROM (AlphaDream). + /// + internal static string ErrorOpenAlphaDream + { + get + { + return ResourceManager.GetString("ErrorOpenAlphaDream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading DSE Folder. + /// + internal static string ErrorOpenDSE + { + get + { + return ResourceManager.GetString("ErrorOpenDSE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading GBA ROM (MP2K). + /// + internal static string ErrorOpenMP2K + { + get + { + return ResourceManager.GetString("ErrorOpenMP2K", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Loading SDAT File. + /// + internal static string ErrorOpenSDAT + { + get + { + return ResourceManager.GetString("ErrorOpenSDAT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error parsing "{0}"{1}. + /// + internal static string ErrorParseConfig + { + get + { + return ResourceManager.GetString("ErrorParseConfig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Exporting DLS. + /// + internal static string ErrorSaveDLS + { + get + { + return ResourceManager.GetString("ErrorSaveDLS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Exporting MIDI. + /// + internal static string ErrorSaveMIDI + { + get + { + return ResourceManager.GetString("ErrorSaveMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Exporting SF2. + /// + internal static string ErrorSaveSF2 + { + get + { + return ResourceManager.GetString("ErrorSaveSF2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error Exporting WAV. + /// + internal static string ErrorSaveWAV + { + get + { + return ResourceManager.GetString("ErrorSaveWAV", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This SDAT archive has no sequences.. + /// + internal static string ErrorSDATNoSequences + { + get + { + return ResourceManager.GetString("ErrorSDATNoSequences", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" is not an integer value.. + /// + internal static string ErrorValueParse + { + get + { + return ResourceManager.GetString("ErrorValueParse", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "{0}" must be between {1} and {2}.. + /// + internal static string ErrorValueParseRanged + { + get + { + return ResourceManager.GetString("ErrorValueParseRanged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GBA Files. + /// + internal static string FilterOpenGBA + { + get + { + return ResourceManager.GetString("FilterOpenGBA", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SDAT Files. + /// + internal static string FilterOpenSDAT + { + get + { + return ResourceManager.GetString("FilterOpenSDAT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DLS Files. + /// + internal static string FilterSaveDLS + { + get + { + return ResourceManager.GetString("FilterSaveDLS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MIDI Files. + /// + internal static string FilterSaveMIDI + { + get + { + return ResourceManager.GetString("FilterSaveMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SF2 Files. + /// + internal static string FilterSaveSF2 + { + get + { + return ResourceManager.GetString("FilterSaveSF2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WAV Files. + /// + internal static string FilterSaveWAV + { + get + { + return ResourceManager.GetString("FilterSaveWAV", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Data. + /// + internal static string MenuData + { + get + { + return ResourceManager.GetString("MenuData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to End Current Playlist. + /// + internal static string MenuEndPlaylist + { + get + { + return ResourceManager.GetString("MenuEndPlaylist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File. + /// + internal static string MenuFile + { + get + { + return ResourceManager.GetString("MenuFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open GBA ROM (AlphaDream). + /// + internal static string MenuOpenAlphaDream + { + get + { + return ResourceManager.GetString("MenuOpenAlphaDream", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open DSE Folder. + /// + internal static string MenuOpenDSE + { + get + { + return ResourceManager.GetString("MenuOpenDSE", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open GBA ROM (MP2K). + /// + internal static string MenuOpenMP2K + { + get + { + return ResourceManager.GetString("MenuOpenMP2K", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open SDAT File. + /// + internal static string MenuOpenSDAT + { + get + { + return ResourceManager.GetString("MenuOpenSDAT", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Playlist. + /// + internal static string MenuPlaylist + { + get + { + return ResourceManager.GetString("MenuPlaylist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export VoiceTable as DLS. + /// + internal static string MenuSaveDLS + { + get + { + return ResourceManager.GetString("MenuSaveDLS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export Song as MIDI. + /// + internal static string MenuSaveMIDI + { + get + { + return ResourceManager.GetString("MenuSaveMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export VoiceTable as SF2. + /// + internal static string MenuSaveSF2 + { + get + { + return ResourceManager.GetString("MenuSaveSF2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export Song as WAV. + /// + internal static string MenuSaveWAV + { + get + { + return ResourceManager.GetString("MenuSaveWAV", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to C;C#;D;D#;E;F;F#;G;G#;A;A#;B. + /// + internal static string Notes + { + get + { + return ResourceManager.GetString("Notes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Next Song. + /// + internal static string PlayerNextSong + { + get + { + return ResourceManager.GetString("PlayerNextSong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Notes. + /// + internal static string PlayerNotes + { + get + { + return ResourceManager.GetString("PlayerNotes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pause. + /// + internal static string PlayerPause + { + get + { + return ResourceManager.GetString("PlayerPause", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Play. + /// + internal static string PlayerPlay + { + get + { + return ResourceManager.GetString("PlayerPlay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Position. + /// + internal static string PlayerPosition + { + get + { + return ResourceManager.GetString("PlayerPosition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Previous Song. + /// + internal static string PlayerPreviousSong + { + get + { + return ResourceManager.GetString("PlayerPreviousSong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rest. + /// + internal static string PlayerRest + { + get + { + return ResourceManager.GetString("PlayerRest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop. + /// + internal static string PlayerStop + { + get + { + return ResourceManager.GetString("PlayerStop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tempo. + /// + internal static string PlayerTempo + { + get + { + return ResourceManager.GetString("PlayerTempo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type. + /// + internal static string PlayerType + { + get + { + return ResourceManager.GetString("PlayerType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unpause. + /// + internal static string PlayerUnpause + { + get + { + return ResourceManager.GetString("PlayerUnpause", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Music. + /// + internal static string PlaylistMusic + { + get + { + return ResourceManager.GetString("PlaylistMusic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Would you like to play the following playlist?{0}. + /// + internal static string PlayPlaylistBody + { + get + { + return ResourceManager.GetString("PlayPlaylistBody", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to VoiceTable saved to {0}.. + /// + internal static string SuccessSaveDLS + { + get + { + return ResourceManager.GetString("SuccessSaveDLS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MIDI saved to {0}.. + /// + internal static string SuccessSaveMIDI + { + get + { + return ResourceManager.GetString("SuccessSaveMIDI", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to VoiceTable saved to {0}.. + /// + internal static string SuccessSaveSF2 + { + get + { + return ResourceManager.GetString("SuccessSaveSF2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to WAV saved to {0}.. + /// + internal static string SuccessSaveWAV + { + get + { + return ResourceManager.GetString("SuccessSaveWAV", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Arguments. + /// + internal static string TrackViewerArguments + { + get + { + return ResourceManager.GetString("TrackViewerArguments", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Event. + /// + internal static string TrackViewerEvent + { + get + { + return ResourceManager.GetString("TrackViewerEvent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Offset. + /// + internal static string TrackViewerOffset + { + get + { + return ResourceManager.GetString("TrackViewerOffset", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ticks. + /// + internal static string TrackViewerTicks + { + get + { + return ResourceManager.GetString("TrackViewerTicks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Track Viewer. + /// + internal static string TrackViewerTitle + { + get + { + return ResourceManager.GetString("TrackViewerTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Track {0}. + /// + internal static string TrackViewerTrackX + { + get + { + return ResourceManager.GetString("TrackViewerTrackX", resourceCulture); + } + } + } +} diff --git a/WinForms/Properties/Strings.es.resx b/WinForms/Properties/Strings.es.resx new file mode 100644 index 00000000..d98bb266 --- /dev/null +++ b/WinForms/Properties/Strings.es.resx @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Quisiera detener la Lista de Reproducción actual? + + + Error al Cargar Canción {0} + + + Error al Abrir Carpeta DSE + + + Error al Abrir GBA ROM (AlphaDream) + + + Error al Abrir GBA ROM (MP2K) + + + Error al Abrir Archivo SDAT + + + Error al Exportar MIDI + + + Archivo GBA + + + Archivo SDAT + + + Archivos MIDI + + + Datos + + + Detener Lista de Reproducción Actual + + + Archivo + + + Abrir Carpeta DSE + + + Abrir GBA ROM (AlphaDream) + + + Abrir GBA ROM (MP2K) + + + Abrir Archivo SDAT + + + Playlist + + + Exportar Canción como MIDI + + + Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si + + + Siguiente Canción + + + Notas + + + Pausar + + + Reproducir + + + Posición + + + Canción Anterior + + + Retraso + + + Detener + + + Tempo + + + Tipo + + + Resumir + + + Música + + + Quisiera reproducir la siguiente Lista de Reproducción? {0} + + + MIDI guardado en {0}. + + + Argumento + + + Evento + + + Offset + + + Ticks + + + Visor de Eventos + + + Pista {0} + + + {0} clave + + + "{0}" debe ser Verdadero o Falso. + + + El color {0} tiene una clave inválida. + + + El color {0} no está definido. + + + El color {0} está definido más de una vez entre decimal y hexadecimal. + + + "{0}" es inválido. + + + "{0}" no se encuentra. + + + "{0}" debe tener al menos una entrada. + + + Versión del encabezado desconocida: 0x{0:X} + + + Clave inválida en la pista {0} en 0x{1:X}: {2} + + + Comando inválido en la pista {0} en 0x{1:X}: 0x{2:X} + + + No hay ningún archivo "bgm(NNNN).smd". + + + Error al Cargar Configuración Global + + + No se puede copiar, el código del juego "{0}" es inválido. + + + No se encuentra el código del juego "{0}". + + + Error analizando el código del juego "{0}" en "{1}"{2} + + + La Lista de Reproducción "{0}" tiene a la canción {1} definida más de una vez entre decimal y hexadecimal. + + + El número de "{0}" debe ser igual al de "{1}". + + + Comando de estado en ejecución inválido en la pista {0} en 0x{1:X}: 0x{2:X} + + + Demasiadas llamadas a eventos anidadas en la pista {0} + + + Error analizando "{0}"{1} + + + Este archivo SDAT no tiene secuencias. + + + "{0}" no es un valor entero. + + + "{0}" debe estar entre {1} y {2}. + + + Error al Exportar WAV + + + Archivos WAV + + + Exportar Canción como WAV + + + WAV guardado en {0}. + + + Error al Exportar SF2 + + + Archivos SF2 + + + Exportar VoiceTable como SF2 + + + SF2 guardado en {0}. + + + Error al Exportar DLS + + + Archivos DLS + + + Exportar VoiceTable como DLS + + + DLS guardado en {0}. + + \ No newline at end of file diff --git a/WinForms/Properties/Strings.it.resx b/WinForms/Properties/Strings.it.resx new file mode 100644 index 00000000..935f17e8 --- /dev/null +++ b/WinForms/Properties/Strings.it.resx @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Errore durante il caricamento del Brano {0} + + + Errore durante il caricamento della ROM GBA (AlphaDream) + + + Errore Durante L'Esportazione in MIDI + + + File GBA + + + File MIDI + + + Dati + + + File + + + Apri ROM GBA (MP2K) + + + Esporta Brano in MIDI + + + Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si + + + Pausa + + + Note + + + Pausa + + + Play + + + Posizione + + + Stop + + + Tempo + + + Tipo + + + Riprendi + + + MIDI salvato in {0}. + + + Desideri riprodurre la seguente playlist?{0} + + + Brano Sucessivo + + + Brano Precedente + + + Desideri interrompere la riproduzione della playlist attuale? + + + Errore durante il caricamento della Cartella DSE + + + Errore durante il caricamento della ROM GBA (MP2K) + + + Errore durante il caricamento del File SDAT + + + File SDAT + + + Termina la Playlist Attuale + + + Apri Cartella DSE + + + Apri ROM GBA (AlphaDream) + + + Apri File SDAT + + + Playlist + + + Musica + + + Argomenti + + + Evento + + + Posizione + + + Tick + + + Visualizzatore Traccia + + + Traccia {0} + + + La Chiave {0} + + + "{0}" deve essere Vero o Falso. + + + Il colore {0} non ha una chiave valida. + + + Il colore {0} non è definito. + + + Il colore {0} è definito più di una volta tra decimale ed esadecimale. + + + "{0}" non è valido. + + + "{0}" è mancante. + + + "{0}" deve possedere almeno una voce. + + + Versione dell'header sconosciuta: 0x{0:X} + + + Tasto del pianoforte non valido nella traccia {0} a 0x{1:X}: {2} + + + Comando non valido nella traccia {0} a 0x{1:X}: 0x{2:X} + + + Non sono presenti file "bgm(NNNN).smd". + + + Errore nel Caricamento della Configurazione Globale + + + Non è possibile copiare il Codice Gioco invalido "{0}" + + + Il Codice Gioco "{0}" non è presente. + + + Errore nell'analisi del Codice Gioco "{0}" in "{1}"{2} + + + La Playlist "{0}" presenta il brano {1} più volte definito tra decimale e esadecimale. + + + Il numero di "{0}" deve essere uguale a quello di "{1}". + + + Comando di stato in esecuzione non valido nella traccia {0} a 0x{1:X}: 0x{2:X} + + + Troppi eventi di chiamata nidificati nella traccia {0} + + + Errore nell'analisi di "{0}"{1} + + + Questo archivio SDAT non ha sequenze. + + + "{0}" non è un valore intero. + + + "{0}" deve essere compreso tra {1} e {2}. + + + Errore Durante L'Esportazione in WAV + + + File WAV + + + Esporta Brano in WAV + + + WAV salvato in {0}. + + + Errore Durante L'Esportazione in SF2 + + + File SF2 + + + Esporta VoiceTable In SF2 + + + VoiceTable salvata in {0}. + + + Errore Durante L'Esportazione in DLS + + + File DLS + + + Esporta VoiceTable In DLS + + + VoiceTable salvata in {0}. + + \ No newline at end of file diff --git a/WinForms/Properties/Strings.resx b/WinForms/Properties/Strings.resx new file mode 100644 index 00000000..228bc168 --- /dev/null +++ b/WinForms/Properties/Strings.resx @@ -0,0 +1,380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Error Loading Song {0} + {0} is the song number. + + + Error Loading GBA ROM (AlphaDream) + + + Error Exporting MIDI + + + GBA Files + + + MIDI Files + + + Data + + + File + + + Open GBA ROM (MP2K) + + + Export Song as MIDI + + + C;C#;D;D#;E;F;F#;G;G#;A;A#;B + + + Rest + + + Notes + + + Pause + + + Play + + + Position + + + Stop + + + Tempo + + + Type + + + Unpause + + + MIDI saved to {0}. + {0} is the file name. + + + Would you like to play the following playlist?{0} + {0} is a newline character followed by the playlist name. + + + Next Song + + + Previous Song + + + Would you like to stop playing the current playlist? + + + Error Loading DSE Folder + + + Error Loading GBA ROM (MP2K) + + + Error Loading SDAT File + + + SDAT Files + + + End Current Playlist + + + Open DSE Folder + + + Open GBA ROM (AlphaDream) + + + Open SDAT File + + + Playlist + + + Music + + + Arguments + + + Event + + + Offset + + + Ticks + + + Track Viewer + + + Track {0} + {0} is the track number. + + + {0} key + {0} is the parent key name. + + + "{0}" must be True or False. + {0} is the value name. + + + Color {0} has an invalid key. + {0} is the color number. + + + Color {0} is not defined. + {0} is the color number. + + + Color {0} is defined more than once between decimal and hexadecimal. + {0} is the color number. + + + "{0}" is invalid. + {0} is the invalid key. + + + "{0}" is missing. + {0} is the missing key. + + + "{0}" must have at least one entry. + {0} is the key. + + + Unknown header version: 0x{0:X} + {0} is the header version. + + + Invalid key in track {0} at 0x{1:X}: {2} + {0} is the track number, {1} is the offset, {2} is the key. + + + Invalid command in track {0} at 0x{1:X}: 0x{2:X} + {0} is the track number, {1} is the offset, {2} is the command. + + + There are no "bgm(NNNN).smd" files. + + + Error Loading Global Config + + + Cannot copy invalid game code "{0}" + {0} is the invalid game code. + + + Game code "{0}" is missing. + {0} is the game code. + + + Error parsing game code "{0}" in "{1}"{2} + {0} is the game code, {1} is the config filename, {2} is a newline character followed by the error message. + + + Playlist "{0}" has song {1} defined more than once between decimal and hexadecimal. + {0} is the playlist name, {1} is the song index. + + + "{0}" count must be the same as "{1}" count. + {0} is key 1, {1} is key 2. + + + Invalid running status command in track {0} at 0x{1:X}: 0x{2:X} + {0} is the track number, {1} is the offset, {2} is the command. + + + Too many nested call events in track {0} + {0} is the track number. + + + Error parsing "{0}"{1} + {0} is the config filename, {1} is a newline character followed by the error message. + + + This SDAT archive has no sequences. + + + "{0}" is not an integer value. + {0} is the value name. + + + "{0}" must be between {1} and {2}. + {0} is the value name, {1} is the minimum allowed value, {2} is the maximum allowed value. + + + Error Exporting WAV + + + WAV Files + + + Export Song as WAV + + + WAV saved to {0}. + {0} is the file name. + + + Error Exporting SF2 + + + SF2 Files + + + Export VoiceTable as SF2 + + + VoiceTable saved to {0}. + {0} is the file name. + + + Error Exporting DLS + + + DLS Files + + + Export VoiceTable as DLS + + + VoiceTable saved to {0}. + {0} is the file name. + + + An audio output device was {0}. {1} was disconnected. + {0} is the status of the device. {1} is the device itself + + \ No newline at end of file diff --git a/WinForms/UI/ColorSlider.cs b/WinForms/UI/ColorSlider.cs new file mode 100644 index 00000000..b23df9ce --- /dev/null +++ b/WinForms/UI/ColorSlider.cs @@ -0,0 +1,485 @@ +#region License + +/* Copyright (c) 2017 Fabrice Lacharme + * This code is inspired from Michal Brylka + * https://www.codeproject.com/Articles/17395/Owner-drawn-trackbar-slider + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory(""), ToolboxBitmap(typeof(TrackBar))] + internal class ColorSlider : Control + { + private const int thumbSize = 14; + private Rectangle thumbRect; + + private long _value = 0L; + public long Value + { + get => _value; + set + { + if (value >= _minimum && value <= _maximum) + { + _value = value; + ValueChanged?.Invoke(this, new EventArgs()); + Invalidate(); + } + else + { + throw new ArgumentOutOfRangeException(nameof(Value), $"{nameof(Value)} must be between {nameof(Minimum)} and {nameof(Maximum)}."); + } + } + } + private long _minimum = 0L; + public long Minimum + { + get => _minimum; + set + { + if (value <= _maximum) + { + _minimum = value; + if (_value < _minimum) + { + _value = _minimum; + ValueChanged?.Invoke(this, new EventArgs()); + } + Invalidate(); + } + else + { + throw new ArgumentOutOfRangeException(nameof(Minimum), $"{nameof(Minimum)} cannot be higher than {nameof(Maximum)}."); + } + } + } + private long _maximum = 10L; + public long Maximum + { + get => _maximum; + set + { + if (value >= _minimum) + { + _maximum = value; + if (_value > _maximum) + { + _value = _maximum; + ValueChanged?.Invoke(this, new EventArgs()); + } + Invalidate(); + } + else + { + throw new ArgumentOutOfRangeException(nameof(Maximum), $"{nameof(Maximum)} cannot be lower than {nameof(Minimum)}."); + } + } + } + private long _smallChange = 1L; + public long SmallChange + { + get => _smallChange; + set + { + if (value >= 0) + { + _smallChange = value; + } + else + { + throw new ArgumentOutOfRangeException(nameof(SmallChange), $"{nameof(SmallChange)} must be greater than or equal to 0."); + } + } + } + private long _largeChange = 5L; + public long LargeChange + { + get => _largeChange; + set + { + if (value >= 0) + { + _largeChange = value; + } + else + { + throw new ArgumentOutOfRangeException(nameof(LargeChange), $"{nameof(LargeChange)} must be greater than or equal to 0."); + } + } + } + private bool _acceptKeys = true; + public bool AcceptKeys + { + get => _acceptKeys; + set + { + _acceptKeys = value; + SetStyle(ControlStyles.Selectable, value); + } + } + + public event EventHandler ValueChanged; + + private readonly Color _thumbOuterColor = Color.White; + private readonly Color _thumbInnerColor = Color.White; + private readonly Color _thumbPenColor = Color.FromArgb(125, 125, 125); + private readonly Color _barInnerColor = Theme.BackColorMouseOver; + private readonly Color _elapsedPenColorTop = Theme.ForeColor; + private readonly Color _elapsedPenColorBottom = Theme.ForeColor; + private readonly Color _barPenColorTop = Color.FromArgb(85, 90, 104); + private readonly Color _barPenColorBottom = Color.FromArgb(117, 124, 140); + private readonly Color _elapsedInnerColor = Theme.BorderColor; + private readonly Color _tickColor = Color.White; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + pen.Dispose(); + } + base.Dispose(disposing); + } + public ColorSlider() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.Selectable | + ControlStyles.SupportsTransparentBackColor | ControlStyles.UserMouse | + ControlStyles.UserPaint, true); + Size = new Size(200, 48); + } + + protected override void OnPaint(PaintEventArgs e) + { + if (!Enabled) + { + Color[] c = DesaturateColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, + _barInnerColor, + _elapsedPenColorTop, _elapsedPenColorBottom, + _barPenColorTop, _barPenColorBottom, + _elapsedInnerColor); + Draw(e, + c[0], c[1], c[2], + c[3], + c[4], c[5], + c[6], c[7], + c[8]); + } + else + { + if (mouseInRegion) + { + Color[] c = LightenColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, + _barInnerColor, + _elapsedPenColorTop, _elapsedPenColorBottom, + _barPenColorTop, _barPenColorBottom, + _elapsedInnerColor); + Draw(e, + c[0], c[1], c[2], + c[3], + c[4], c[5], + c[6], c[7], + c[8]); + } + else + { + Draw(e, + _thumbOuterColor, _thumbInnerColor, _thumbPenColor, + _barInnerColor, + _elapsedPenColorTop, _elapsedPenColorBottom, + _barPenColorTop, _barPenColorBottom, + _elapsedInnerColor); + } + } + } + private readonly Pen pen = new Pen(Color.Transparent); + private void Draw(PaintEventArgs e, + Color thumbOuterColorPaint, Color thumbInnerColorPaint, Color thumbPenColorPaint, + Color barInnerColorPaint, + Color elapsedTopPenColorPaint, Color elapsedBottomPenColorPaint, + Color barTopPenColorPaint, Color barBottomPenColorPaint, + Color elapsedInnerColorPaint) + { + if (Focused) + { + ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Color.FromArgb(50, elapsedTopPenColorPaint), ButtonBorderStyle.Dashed); + } + + long a = _maximum - _minimum; + long x = a == 0 ? 0 : (_value - _minimum) * (ClientRectangle.Width - thumbSize) / a; + thumbRect = new Rectangle((int)x, ClientRectangle.Y + (ClientRectangle.Height / 2) - (thumbSize / 2), thumbSize, thumbSize); + Rectangle barRect = ClientRectangle; + barRect.Inflate(-1, -barRect.Height / 3); + Rectangle elapsedRect = barRect; + elapsedRect.Width = thumbRect.Left + (thumbSize / 2); + + pen.Color = barInnerColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + (barRect.Height / 2)); + pen.Color = elapsedInnerColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + (barRect.Height / 2)); + pen.Color = elapsedTopPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2)); + pen.Color = elapsedBottomPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y + 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2)); + pen.Color = barTopPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2)); + pen.Color = barBottomPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); + pen.Color = barTopPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X, barRect.Y + (barRect.Height / 2) + 1); + pen.Color = barBottomPenColorPaint; + e.Graphics.DrawLine(pen, barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); + + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + Color newthumbOuterColorPaint = thumbOuterColorPaint, + newthumbInnerColorPaint = thumbInnerColorPaint; + if (busyMouse) + { + newthumbOuterColorPaint = Color.FromArgb(175, thumbOuterColorPaint); + newthumbInnerColorPaint = Color.FromArgb(175, thumbInnerColorPaint); + } + using (GraphicsPath thumbPath = CreateRoundRectPath(thumbRect, thumbSize)) + { + using (var lgbThumb = new LinearGradientBrush(thumbRect, newthumbOuterColorPaint, newthumbInnerColorPaint, LinearGradientMode.Vertical) { WrapMode = WrapMode.TileFlipXY }) + { + e.Graphics.FillPath(lgbThumb, thumbPath); + } + Color newThumbPenColor = thumbPenColorPaint; + if (busyMouse || mouseInThumbRegion) + { + newThumbPenColor = ControlPaint.Dark(newThumbPenColor); + } + pen.Color = newThumbPenColor; + e.Graphics.DrawPath(pen, thumbPath); + } + + const int numTicks = 1 + (10 * (5 + 1)); + int interval = 0; + int start = thumbRect.Width / 2; + int w = barRect.Width - thumbRect.Width; + int idx = 0; + pen.Color = _tickColor; + for (int i = 0; i <= 10; i++) + { + e.Graphics.DrawLine(pen, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height - 5); + if (i < 10) + { + for (int j = 0; j <= 5; j++) + { + idx++; + interval = idx * w / (numTicks - 1); + } + } + } + } + + private bool mouseInRegion = false; + private bool mouseInThumbRegion = false; + private bool busyMouse = false; + private void SetValueFromPoint(Point p) + { + int x = p.X; + int margin = thumbSize / 2; + x -= margin; + _value = (long)((x * ((_maximum - _minimum) / (ClientSize.Width - (2f * margin)))) + _minimum); + if (_value < _minimum) + { + _value = _minimum; + } + else if (_value > _maximum) + { + _value = _maximum; + } + ValueChanged?.Invoke(this, new EventArgs()); + } + protected override void OnEnabledChanged(EventArgs e) + { + base.OnEnabledChanged(e); + Invalidate(); + } + protected override void OnMouseEnter(EventArgs e) + { + base.OnMouseEnter(e); + mouseInRegion = true; + Invalidate(); + } + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + mouseInRegion = false; + mouseInThumbRegion = false; + Invalidate(); + } + protected override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); + busyMouse = (MouseButtons & MouseButtons.Left) != MouseButtons.None; + if (busyMouse) + { + SetValueFromPoint(e.Location); + } + Invalidate(); + } + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); + if (busyMouse) + { + SetValueFromPoint(e.Location); + } + Invalidate(); + } + protected override void OnMouseUp(MouseEventArgs e) + { + base.OnMouseUp(e); + mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); + bool old = busyMouse; + busyMouse = old && e.Button == MouseButtons.Left ? false : old; + Invalidate(); + } + protected override void OnGotFocus(EventArgs e) + { + base.OnGotFocus(e); + Invalidate(); + } + protected override void OnLostFocus(EventArgs e) + { + base.OnLostFocus(e); + Invalidate(); + } + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (_acceptKeys && !busyMouse) + { + switch (e.KeyCode) + { + case Keys.Down: + case Keys.Left: + { + long newVal = _value - _smallChange; + if (newVal < _minimum) + { + newVal = _minimum; + } + Value = newVal; + break; + } + case Keys.Up: + case Keys.Right: + { + long newVal = _value + _smallChange; + if (newVal > _maximum) + { + newVal = _maximum; + } + Value = newVal; + break; + } + case Keys.Home: + { + Value = _minimum; + break; + } + case Keys.End: + { + Value = _maximum; + break; + } + case Keys.PageDown: + { + long newVal = _value - _largeChange; + if (newVal < _minimum) + { + newVal = _minimum; + } + Value = newVal; + break; + } + case Keys.PageUp: + { + long newVal = _value + _largeChange; + if (newVal > _maximum) + { + newVal = _maximum; + } + Value = newVal; + break; + } + } + } + } + protected override bool ProcessDialogKey(Keys keyData) + { + return !_acceptKeys || keyData == Keys.Tab || ModifierKeys == Keys.Shift ? base.ProcessDialogKey(keyData) : false; + } + + private static GraphicsPath CreateRoundRectPath(Rectangle rect, int size) + { + var gp = new GraphicsPath(); + gp.AddLine(rect.Left + (size / 2), rect.Top, rect.Right - (size / 2), rect.Top); + gp.AddArc(rect.Right - size, rect.Top, size, size, 270, 90); + + gp.AddLine(rect.Right, rect.Top + (size / 2), rect.Right, rect.Bottom - (size / 2)); + gp.AddArc(rect.Right - size, rect.Bottom - size, size, size, 0, 90); + + gp.AddLine(rect.Right - (size / 2), rect.Bottom, rect.Left + (size / 2), rect.Bottom); + gp.AddArc(rect.Left, rect.Bottom - size, size, size, 90, 90); + + gp.AddLine(rect.Left, rect.Bottom - (size / 2), rect.Left, rect.Top + (size / 2)); + gp.AddArc(rect.Left, rect.Top, size, size, 180, 90); + return gp; + } + private static Color[] DesaturateColors(params Color[] colors) + { + var ret = new Color[colors.Length]; + for (int i = 0; i < colors.Length; i++) + { + int gray = (int)((colors[i].R * 0.3) + (colors[i].G * 0.6) + (colors[i].B * 0.1)); + ret[i] = Color.FromArgb((-0x010101 * (255 - gray)) - 1); + } + return ret; + } + private static Color[] LightenColors(params Color[] colors) + { + var ret = new Color[colors.Length]; + for (int i = 0; i < colors.Length; i++) + { + ret[i] = ControlPaint.Light(colors[i]); + } + return ret; + } + private static bool IsPointInRect(Point p, Rectangle rect) + { + return p.X > rect.Left & p.X < rect.Right & p.Y > rect.Top & p.Y < rect.Bottom; + } + } +} diff --git a/WinForms/UI/FlexibleMessageBox.cs b/WinForms/UI/FlexibleMessageBox.cs new file mode 100644 index 00000000..30d29ad1 --- /dev/null +++ b/WinForms/UI/FlexibleMessageBox.cs @@ -0,0 +1,697 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + /* FlexibleMessageBox – A flexible replacement for the .NET MessageBox + * + * Author: Jörg Reichert (public@jreichert.de) + * Contributors: Thanks to: David Hall, Roink + * Version: 1.3 + * Published at: http://www.codeproject.com/Articles/601900/FlexibleMessageBox + * + ************************************************************************************************************ + * Features: + * - It can be simply used instead of MessageBox since all important static "Show"-Functions are supported + * - It is small, only one source file, which could be added easily to each solution + * - It can be resized and the content is correctly word-wrapped + * - It tries to auto-size the width to show the longest text row + * - It never exceeds the current desktop working area + * - It displays a vertical scrollbar when needed + * - It does support hyperlinks in text + * + * Because the interface is identical to MessageBox, you can add this single source file to your project + * and use the FlexibleMessageBox almost everywhere you use a standard MessageBox. + * The goal was NOT to produce as many features as possible but to provide a simple replacement to fit my + * own needs. Feel free to add additional features on your own, but please left my credits in this class. + * + ************************************************************************************************************ + * Usage examples: + * + * FlexibleMessageBox.Show("Just a text"); + * + * FlexibleMessageBox.Show("A text", + * "A caption"); + * + * FlexibleMessageBox.Show("Some text with a link: www.google.com", + * "Some caption", + * MessageBoxButtons.AbortRetryIgnore, + * MessageBoxIcon.Information, + * MessageBoxDefaultButton.Button2); + * + * var dialogResult = FlexibleMessageBox.Show("Do you know the answer to life the universe and everything?", + * "One short question", + * MessageBoxButtons.YesNo); + * + ************************************************************************************************************ + * THE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS", WITHOUT WARRANTY + * OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHOR BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THIS + * SOFTWARE. + * + ************************************************************************************************************ + * History: + * Version 1.3 - 19.Dezember 2014 + * - Added refactoring function GetButtonText() + * - Used CurrentUICulture instead of InstalledUICulture + * - Added more button localizations. Supported languages are now: ENGLISH, GERMAN, SPANISH, ITALIAN + * - Added standard MessageBox handling for "copy to clipboard" with + and + + * - Tab handling is now corrected (only tabbing over the visible buttons) + * - Added standard MessageBox handling for ALT-Keyboard shortcuts + * - SetDialogSizes: Refactored completely: Corrected sizing and added caption driven sizing + * + * Version 1.2 - 10.August 2013 + * - Do not ShowInTaskbar anymore (original MessageBox is also hidden in taskbar) + * - Added handling for Escape-Button + * - Adapted top right close button (red X) to behave like MessageBox (but hidden instead of deactivated) + * + * Version 1.1 - 14.June 2013 + * - Some Refactoring + * - Added internal form class + * - Added missing code comments, etc. + * + * Version 1.0 - 15.April 2013 + * - Initial Version + */ + + internal class FlexibleMessageBox + { + #region Public statics + + /// + /// Defines the maximum width for all FlexibleMessageBox instances in percent of the working area. + /// + /// Allowed values are 0.2 - 1.0 where: + /// 0.2 means: The FlexibleMessageBox can be at most half as wide as the working area. + /// 1.0 means: The FlexibleMessageBox can be as wide as the working area. + /// + /// Default is: 70% of the working area width. + /// + public static double MAX_WIDTH_FACTOR = 0.7; + + /// + /// Defines the maximum height for all FlexibleMessageBox instances in percent of the working area. + /// + /// Allowed values are 0.2 - 1.0 where: + /// 0.2 means: The FlexibleMessageBox can be at most half as high as the working area. + /// 1.0 means: The FlexibleMessageBox can be as high as the working area. + /// + /// Default is: 90% of the working area height. + /// + public static double MAX_HEIGHT_FACTOR = 0.9; + + /// + /// Defines the font for all FlexibleMessageBox instances. + /// + /// Default is: Theme.Font + /// + public static Font FONT = Theme.Font; + + #endregion + + #region Public show functions + + public static DialogResult Show(string text) + { + return FlexibleMessageBoxForm.Show(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text) + { + return FlexibleMessageBoxForm.Show(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption) + { + return FlexibleMessageBoxForm.Show(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(Exception ex, string caption) + { + return FlexibleMessageBoxForm.Show(null, string.Format("Error Details:{1}{1}{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace), caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text, string caption) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons) + { + return FlexibleMessageBoxForm.Show(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) + { + return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); + } + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + { + return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, defaultButton); + } + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + { + return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, defaultButton); + } + + #endregion + + #region Internal form class + + class FlexibleMessageBoxForm : ThemedForm + { + IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + void InitializeComponent() + { + components = new Container(); + button1 = new ThemedButton(); + richTextBoxMessage = new ThemedRichTextBox(); + FlexibleMessageBoxFormBindingSource = new BindingSource(components); + panel1 = new ThemedPanel(); + pictureBoxForIcon = new PictureBox(); + button2 = new ThemedButton(); + button3 = new ThemedButton(); + ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).BeginInit(); + panel1.SuspendLayout(); + ((ISupportInitialize)(pictureBoxForIcon)).BeginInit(); + SuspendLayout(); + // + // button1 + // + button1.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + button1.AutoSize = true; + button1.DialogResult = DialogResult.OK; + button1.Location = new Point(11, 67); + button1.MinimumSize = new Size(0, 24); + button1.Name = "button1"; + button1.Size = new Size(75, 24); + button1.TabIndex = 2; + button1.Text = "OK"; + button1.UseVisualStyleBackColor = true; + button1.Visible = false; + // + // richTextBoxMessage + // + richTextBoxMessage.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) + | AnchorStyles.Left) + | AnchorStyles.Right); + richTextBoxMessage.BorderStyle = BorderStyle.None; + richTextBoxMessage.DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "MessageText", true, DataSourceUpdateMode.OnPropertyChanged)); + richTextBoxMessage.Font = new Font(Theme.Font.FontFamily, 9); + richTextBoxMessage.Location = new Point(50, 26); + richTextBoxMessage.Margin = new Padding(0); + richTextBoxMessage.Name = "richTextBoxMessage"; + richTextBoxMessage.ReadOnly = true; + richTextBoxMessage.ScrollBars = RichTextBoxScrollBars.Vertical; + richTextBoxMessage.Size = new Size(200, 20); + richTextBoxMessage.TabIndex = 0; + richTextBoxMessage.TabStop = false; + richTextBoxMessage.Text = ""; + richTextBoxMessage.LinkClicked += new LinkClickedEventHandler(LinkClicked); + // + // panel1 + // + panel1.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) + | AnchorStyles.Left) + | AnchorStyles.Right); + panel1.Controls.Add(pictureBoxForIcon); + panel1.Controls.Add(richTextBoxMessage); + panel1.Location = new Point(-3, -4); + panel1.Name = "panel1"; + panel1.Size = new Size(268, 59); + panel1.TabIndex = 1; + // + // pictureBoxForIcon + // + pictureBoxForIcon.BackColor = Color.Transparent; + pictureBoxForIcon.Location = new Point(15, 19); + pictureBoxForIcon.Name = "pictureBoxForIcon"; + pictureBoxForIcon.Size = new Size(32, 32); + pictureBoxForIcon.TabIndex = 8; + pictureBoxForIcon.TabStop = false; + // + // button2 + // + button2.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); + button2.DialogResult = DialogResult.OK; + button2.Location = new Point(92, 67); + button2.MinimumSize = new Size(0, 24); + button2.Name = "button2"; + button2.Size = new Size(75, 24); + button2.TabIndex = 3; + button2.Text = "OK"; + button2.UseVisualStyleBackColor = true; + button2.Visible = false; + // + // button3 + // + button3.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); + button3.AutoSize = true; + button3.DialogResult = DialogResult.OK; + button3.Location = new Point(173, 67); + button3.MinimumSize = new Size(0, 24); + button3.Name = "button3"; + button3.Size = new Size(75, 24); + button3.TabIndex = 0; + button3.Text = "OK"; + button3.UseVisualStyleBackColor = true; + button3.Visible = false; + // + // FlexibleMessageBoxForm + // + AutoScaleDimensions = new SizeF(6F, 13F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(260, 102); + Controls.Add(button3); + Controls.Add(button2); + Controls.Add(panel1); + Controls.Add(button1); + DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "CaptionText", true)); + Icon = Properties.Resources.Icon; + MaximizeBox = false; + MinimizeBox = false; + MinimumSize = new Size(276, 140); + Name = "FlexibleMessageBoxForm"; + SizeGripStyle = SizeGripStyle.Show; + StartPosition = FormStartPosition.CenterParent; + Text = ""; + Shown += new EventHandler(FlexibleMessageBoxForm_Shown); + ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).EndInit(); + panel1.ResumeLayout(false); + ((ISupportInitialize)(pictureBoxForIcon)).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + ThemedButton button1, button2, button3; + private BindingSource FlexibleMessageBoxFormBindingSource; + ThemedRichTextBox richTextBoxMessage; + ThemedPanel panel1; + private PictureBox pictureBoxForIcon; + + #region Private constants + + //These separators are used for the "copy to clipboard" standard operation, triggered by Ctrl + C (behavior and clipboard format is like in a standard MessageBox) + static readonly String STANDARD_MESSAGEBOX_SEPARATOR_LINES = "---------------------------\n"; + static readonly String STANDARD_MESSAGEBOX_SEPARATOR_SPACES = " "; + + //These are the possible buttons (in a standard MessageBox) + private enum ButtonID { OK = 0, CANCEL, YES, NO, ABORT, RETRY, IGNORE }; + + //These are the buttons texts for different languages. + //If you want to add a new language, add it here and in the GetButtonText-Function + private enum TwoLetterISOLanguageID { en, de, es, it }; + static readonly String[] BUTTON_TEXTS_ENGLISH_EN = { "OK", "Cancel", "&Yes", "&No", "&Abort", "&Retry", "&Ignore" }; //Note: This is also the fallback language + static readonly String[] BUTTON_TEXTS_GERMAN_DE = { "OK", "Abbrechen", "&Ja", "&Nein", "&Abbrechen", "&Wiederholen", "&Ignorieren" }; + static readonly String[] BUTTON_TEXTS_SPANISH_ES = { "Aceptar", "Cancelar", "&Sí", "&No", "&Abortar", "&Reintentar", "&Ignorar" }; + static readonly String[] BUTTON_TEXTS_ITALIAN_IT = { "OK", "Annulla", "&Sì", "&No", "&Interrompi", "&Riprova", "&Ignora" }; + + #endregion + + #region Private members + + MessageBoxDefaultButton defaultButton; + int visibleButtonsCount; + readonly TwoLetterISOLanguageID languageID = TwoLetterISOLanguageID.en; + + #endregion + + #region Private constructor + + private FlexibleMessageBoxForm() + { + InitializeComponent(); + + //Try to evaluate the language. If this fails, the fallback language English will be used + Enum.TryParse(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, out languageID); + + KeyPreview = true; + KeyUp += FlexibleMessageBoxForm_KeyUp; + } + + #endregion + + #region Private helper functions + + static string[] GetStringRows(string message) + { + if (string.IsNullOrEmpty(message)) + { + return null; + } + + var messageRows = message.Split(new char[] { '\n' }, StringSplitOptions.None); + return messageRows; + } + + string GetButtonText(ButtonID buttonID) + { + var buttonTextArrayIndex = Convert.ToInt32(buttonID); + + switch (languageID) + { + case TwoLetterISOLanguageID.de: return BUTTON_TEXTS_GERMAN_DE[buttonTextArrayIndex]; + case TwoLetterISOLanguageID.es: return BUTTON_TEXTS_SPANISH_ES[buttonTextArrayIndex]; + case TwoLetterISOLanguageID.it: return BUTTON_TEXTS_ITALIAN_IT[buttonTextArrayIndex]; + + default: return BUTTON_TEXTS_ENGLISH_EN[buttonTextArrayIndex]; + } + } + + static double GetCorrectedWorkingAreaFactor(double workingAreaFactor) + { + const double MIN_FACTOR = 0.2; + const double MAX_FACTOR = 1.0; + + if (workingAreaFactor < MIN_FACTOR) + { + return MIN_FACTOR; + } + + if (workingAreaFactor > MAX_FACTOR) + { + return MAX_FACTOR; + } + + return workingAreaFactor; + } + + static void SetDialogStartPosition(FlexibleMessageBoxForm flexibleMessageBoxForm, IWin32Window owner) + { + //If no owner given: Center on current screen + if (owner == null) + { + var screen = Screen.FromPoint(Cursor.Position); + flexibleMessageBoxForm.StartPosition = FormStartPosition.Manual; + flexibleMessageBoxForm.Left = screen.Bounds.Left + screen.Bounds.Width / 2 - flexibleMessageBoxForm.Width / 2; + flexibleMessageBoxForm.Top = screen.Bounds.Top + screen.Bounds.Height / 2 - flexibleMessageBoxForm.Height / 2; + } + } + + static void SetDialogSizes(FlexibleMessageBoxForm flexibleMessageBoxForm, string text, string caption) + { + //First set the bounds for the maximum dialog size + flexibleMessageBoxForm.MaximumSize = new Size(Convert.ToInt32(SystemInformation.WorkingArea.Width * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_WIDTH_FACTOR)), + Convert.ToInt32(SystemInformation.WorkingArea.Height * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_HEIGHT_FACTOR))); + + //Get rows. Exit if there are no rows to render... + var stringRows = GetStringRows(text); + if (stringRows == null) + { + return; + } + + //Calculate whole text height + var textHeight = TextRenderer.MeasureText(text, FONT).Height; + + //Calculate width for longest text line + const int SCROLLBAR_WIDTH_OFFSET = 15; + var longestTextRowWidth = stringRows.Max(textForRow => TextRenderer.MeasureText(textForRow, FONT).Width); + var captionWidth = TextRenderer.MeasureText(caption, SystemFonts.CaptionFont).Width; + var textWidth = Math.Max(longestTextRowWidth + SCROLLBAR_WIDTH_OFFSET, captionWidth); + + //Calculate margins + var marginWidth = flexibleMessageBoxForm.Width - flexibleMessageBoxForm.richTextBoxMessage.Width; + var marginHeight = flexibleMessageBoxForm.Height - flexibleMessageBoxForm.richTextBoxMessage.Height; + + //Set calculated dialog size (if the calculated values exceed the maximums, they were cut by windows forms automatically) + flexibleMessageBoxForm.Size = new Size(textWidth + marginWidth, + textHeight + marginHeight); + } + + static void SetDialogIcon(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxIcon icon) + { + switch (icon) + { + case MessageBoxIcon.Information: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Information.ToBitmap(); + break; + case MessageBoxIcon.Warning: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Warning.ToBitmap(); + break; + case MessageBoxIcon.Error: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Error.ToBitmap(); + break; + case MessageBoxIcon.Question: + flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Question.ToBitmap(); + break; + default: + //When no icon is used: Correct placement and width of rich text box. + flexibleMessageBoxForm.pictureBoxForIcon.Visible = false; + flexibleMessageBoxForm.richTextBoxMessage.Left -= flexibleMessageBoxForm.pictureBoxForIcon.Width; + flexibleMessageBoxForm.richTextBoxMessage.Width += flexibleMessageBoxForm.pictureBoxForIcon.Width; + break; + } + } + + static void SetDialogButtons(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxButtons buttons, MessageBoxDefaultButton defaultButton) + { + //Set the buttons visibilities and texts + switch (buttons) + { + case MessageBoxButtons.AbortRetryIgnore: + flexibleMessageBoxForm.visibleButtonsCount = 3; + + flexibleMessageBoxForm.button1.Visible = true; + flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.ABORT); + flexibleMessageBoxForm.button1.DialogResult = DialogResult.Abort; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.IGNORE); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Ignore; + + flexibleMessageBoxForm.ControlBox = false; + break; + + case MessageBoxButtons.OKCancel: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.OK; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case MessageBoxButtons.RetryCancel: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case MessageBoxButtons.YesNo: + flexibleMessageBoxForm.visibleButtonsCount = 2; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.Yes; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.No; + + flexibleMessageBoxForm.ControlBox = false; + break; + + case MessageBoxButtons.YesNoCancel: + flexibleMessageBoxForm.visibleButtonsCount = 3; + + flexibleMessageBoxForm.button1.Visible = true; + flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); + flexibleMessageBoxForm.button1.DialogResult = DialogResult.Yes; + + flexibleMessageBoxForm.button2.Visible = true; + flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); + flexibleMessageBoxForm.button2.DialogResult = DialogResult.No; + + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + + case MessageBoxButtons.OK: + default: + flexibleMessageBoxForm.visibleButtonsCount = 1; + flexibleMessageBoxForm.button3.Visible = true; + flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); + flexibleMessageBoxForm.button3.DialogResult = DialogResult.OK; + + flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; + break; + } + + //Set default button (used in FlexibleMessageBoxForm_Shown) + flexibleMessageBoxForm.defaultButton = defaultButton; + } + + #endregion + + #region Private event handlers + + void FlexibleMessageBoxForm_Shown(object sender, EventArgs e) + { + int buttonIndexToFocus = 1; + Button buttonToFocus; + + //Set the default button... + switch (defaultButton) + { + case MessageBoxDefaultButton.Button1: + default: + buttonIndexToFocus = 1; + break; + case MessageBoxDefaultButton.Button2: + buttonIndexToFocus = 2; + break; + case MessageBoxDefaultButton.Button3: + buttonIndexToFocus = 3; + break; + } + + if (buttonIndexToFocus > visibleButtonsCount) + { + buttonIndexToFocus = visibleButtonsCount; + } + + if (buttonIndexToFocus == 3) + { + buttonToFocus = button3; + } + else if (buttonIndexToFocus == 2) + { + buttonToFocus = button2; + } + else + { + buttonToFocus = button1; + } + + buttonToFocus.Focus(); + } + + void LinkClicked(object sender, LinkClickedEventArgs e) + { + try + { + Cursor.Current = Cursors.WaitCursor; + Process.Start(e.LinkText); + } + catch (Exception) + { + //Let the caller of FlexibleMessageBoxForm decide what to do with this exception... + throw; + } + finally + { + Cursor.Current = Cursors.Default; + } + } + + void FlexibleMessageBoxForm_KeyUp(object sender, KeyEventArgs e) + { + //Handle standard key strikes for clipboard copy: "Ctrl + C" and "Ctrl + Insert" + if (e.Control && (e.KeyCode == Keys.C || e.KeyCode == Keys.Insert)) + { + var buttonsTextLine = (button1.Visible ? button1.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) + + (button2.Visible ? button2.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) + + (button3.Visible ? button3.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty); + + //Build same clipboard text like the standard .Net MessageBox + var textForClipboard = STANDARD_MESSAGEBOX_SEPARATOR_LINES + + Text + Environment.NewLine + + STANDARD_MESSAGEBOX_SEPARATOR_LINES + + richTextBoxMessage.Text + Environment.NewLine + + STANDARD_MESSAGEBOX_SEPARATOR_LINES + + buttonsTextLine.Replace("&", string.Empty) + Environment.NewLine + + STANDARD_MESSAGEBOX_SEPARATOR_LINES; + + //Set text in clipboard + Clipboard.SetText(textForClipboard); + } + } + + #endregion + + #region Properties (only used for binding) + + public string CaptionText { get; set; } + public string MessageText { get; set; } + + #endregion + + #region Public show function + + public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) + { + //Create a new instance of the FlexibleMessageBox form + var flexibleMessageBoxForm = new FlexibleMessageBoxForm + { + ShowInTaskbar = false, + + //Bind the caption and the message text + CaptionText = caption, + MessageText = text + }; + flexibleMessageBoxForm.FlexibleMessageBoxFormBindingSource.DataSource = flexibleMessageBoxForm; + + //Set the buttons visibilities and texts. Also set a default button. + SetDialogButtons(flexibleMessageBoxForm, buttons, defaultButton); + + //Set the dialogs icon. When no icon is used: Correct placement and width of rich text box. + SetDialogIcon(flexibleMessageBoxForm, icon); + + //Set the font for all controls + flexibleMessageBoxForm.Font = FONT; + flexibleMessageBoxForm.richTextBoxMessage.Font = FONT; + + //Calculate the dialogs start size (Try to auto-size width to show longest text row). Also set the maximum dialog size. + SetDialogSizes(flexibleMessageBoxForm, text, caption); + + //Set the dialogs start position when given. Otherwise center the dialog on the current screen. + SetDialogStartPosition(flexibleMessageBoxForm, owner); + + //Show the dialog + return flexibleMessageBoxForm.ShowDialog(owner); + } + + #endregion + } //class FlexibleMessageBoxForm + + #endregion + } +} diff --git a/WinForms/UI/ImageComboBox.cs b/WinForms/UI/ImageComboBox.cs new file mode 100644 index 00000000..c928af92 --- /dev/null +++ b/WinForms/UI/ImageComboBox.cs @@ -0,0 +1,62 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + internal class ImageComboBox : ComboBox + { + private const int _imgSize = 15; + private bool _open = false; + + public ImageComboBox() + { + DrawMode = DrawMode.OwnerDrawFixed; + DropDownStyle = ComboBoxStyle.DropDown; + } + + protected override void OnDrawItem(DrawItemEventArgs e) + { + e.DrawBackground(); + e.DrawFocusRectangle(); + + if (e.Index >= 0) + { + ImageComboBoxItem item = Items[e.Index] as ImageComboBoxItem ?? throw new InvalidCastException($"Item was not of type \"{nameof(ImageComboBoxItem)}\""); + int indent = _open ? item.IndentLevel : 0; + e.Graphics.DrawImage(item.Image, e.Bounds.Left + (indent * _imgSize), e.Bounds.Top, _imgSize, _imgSize); + e.Graphics.DrawString(item.ToString(), e.Font, new SolidBrush(e.ForeColor), e.Bounds.Left + (indent * _imgSize) + _imgSize, e.Bounds.Top); + } + + base.OnDrawItem(e); + } + protected override void OnDropDown(EventArgs e) + { + _open = true; + base.OnDropDown(e); + } + protected override void OnDropDownClosed(EventArgs e) + { + _open = false; + base.OnDropDownClosed(e); + } + } + internal class ImageComboBoxItem + { + public object Item { get; } + public Image Image { get; } + public int IndentLevel { get; } + + public ImageComboBoxItem(object item, Image image, int indentLevel) + { + Item = item; + Image = image; + IndentLevel = indentLevel; + } + + public override string ToString() + { + return Item.ToString(); + } + } +} diff --git a/WinForms/UI/MainForm.cs b/WinForms/UI/MainForm.cs new file mode 100644 index 00000000..06a4cd16 --- /dev/null +++ b/WinForms/UI/MainForm.cs @@ -0,0 +1,789 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Windows.Forms; +using System.Windows.Interop; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory("")] + internal class MainForm : ThemedForm + { + private const int _intendedWidth = 675; + private const int _intendedHeight = 675 + 1 + 125 + 24; + + public static MainForm Instance { get; } = new MainForm(); + + public readonly bool[] PianoTracks = new bool[SongInfoControl.SongInfo.MaxTracks]; + + private bool _playlistPlaying; + private Config.Playlist _curPlaylist; + private long _curSong = -1; + private readonly List _playedSongs = new List(); + private readonly List _remainingSongs = new List(); + + private TrackViewer _trackViewer; + + #region Controls + + private readonly MenuStrip _mainMenu; + private readonly ToolStripMenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, + _dataItem, _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem, + _playlistItem, _endPlaylistItem; + private readonly Timer _timer; + private readonly ThemedNumeric _songNumerical; + private readonly ThemedButton _playButton, _pauseButton, _stopButton; + private readonly SplitContainer _splitContainer; + private readonly PianoControl _piano; + private readonly ColorSlider _volumeBar, _positionBar; + private readonly SongInfoControl _songInfo; + private readonly ImageComboBox _songsComboBox; + //private readonly ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; + + #endregion + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _timer.Dispose(); + } + base.Dispose(disposing); + } + private MainForm() + { + for (int i = 0; i < PianoTracks.Length; i++) + { + PianoTracks[i] = true; + } + + // File Menu + _openDSEItem = new ToolStripMenuItem { Text = Strings.MenuOpenDSE }; + _openDSEItem.Click += OpenDSE; + _openAlphaDreamItem = new ToolStripMenuItem { Text = Strings.MenuOpenAlphaDream }; + _openAlphaDreamItem.Click += OpenAlphaDream; + _openMP2KItem = new ToolStripMenuItem { Text = Strings.MenuOpenMP2K }; + _openMP2KItem.Click += OpenMP2K; + _openSDATItem = new ToolStripMenuItem { Text = Strings.MenuOpenSDAT }; + _openSDATItem.Click += OpenSDAT; + _fileItem = new ToolStripMenuItem { Text = Strings.MenuFile }; + _fileItem.DropDownItems.AddRange(new ToolStripItem[] { _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem }); + + // Data Menu + _trackViewerItem = new ToolStripMenuItem { ShortcutKeys = Keys.Control | Keys.T, Text = Strings.TrackViewerTitle }; + _trackViewerItem.Click += OpenTrackViewer; + _exportDLSItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveDLS }; + _exportDLSItem.Click += ExportDLS; + _exportMIDIItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveMIDI }; + _exportMIDIItem.Click += ExportMIDI; + _exportSF2Item = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveSF2 }; + _exportSF2Item.Click += ExportSF2; + _exportWAVItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveWAV }; + _exportWAVItem.Click += ExportWAV; + _dataItem = new ToolStripMenuItem { Text = Strings.MenuData }; + _dataItem.DropDownItems.AddRange(new ToolStripItem[] { _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem }); + + // Playlist Menu + _endPlaylistItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuEndPlaylist }; + _endPlaylistItem.Click += EndCurrentPlaylist; + _playlistItem = new ToolStripMenuItem { Text = Strings.MenuPlaylist }; + _playlistItem.DropDownItems.AddRange(new ToolStripItem[] { _endPlaylistItem }); + + // Main Menu + _mainMenu = new MenuStrip { Size = new Size(_intendedWidth, 24) }; + _mainMenu.Items.AddRange(new ToolStripItem[] { _fileItem, _dataItem, _playlistItem }); + + // Buttons + _playButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumSpringGreen, Text = Strings.PlayerPlay }; + _playButton.Click += (o, e) => Play(); + _pauseButton = new ThemedButton { Enabled = false, ForeColor = Color.DeepSkyBlue, Text = Strings.PlayerPause }; + _pauseButton.Click += (o, e) => Pause(); + _stopButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumVioletRed, Text = Strings.PlayerStop }; + _stopButton.Click += (o, e) => Stop(); + + // Numerical + _songNumerical = new ThemedNumeric { Enabled = false, Minimum = 0, Visible = false }; + _songNumerical.ValueChanged += SongNumerical_ValueChanged; + + // Timer + _timer = new Timer(); + _timer.Tick += UpdateUI; + + // Piano + _piano = new PianoControl(); + + // Volume bar + _volumeBar = new ColorSlider { Enabled = false, LargeChange = 20, Maximum = 100, SmallChange = 5 }; + _volumeBar.ValueChanged += VolumeBar_ValueChanged; + + // Position bar + _positionBar = new ColorSlider { AcceptKeys = false, Enabled = false, Maximum = 0 }; + _positionBar.MouseUp += PositionBar_MouseUp; + _positionBar.MouseDown += PositionBar_MouseDown; + + // Playlist box + _songsComboBox = new ImageComboBox { Enabled = false }; + _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; + + // Track info + _songInfo = new SongInfoControl { Dock = DockStyle.Fill }; + + // Split container + _splitContainer = new SplitContainer { BackColor = Theme.TitleBar, Dock = DockStyle.Fill, IsSplitterFixed = true, Orientation = Orientation.Horizontal, SplitterWidth = 1 }; + _splitContainer.Panel1.Controls.AddRange(new Control[] { _playButton, _pauseButton, _stopButton, _songNumerical, _songsComboBox, _piano, _volumeBar, _positionBar }); + _splitContainer.Panel2.Controls.Add(_songInfo); + + // MainForm + ClientSize = new Size(_intendedWidth, _intendedHeight); + Controls.AddRange(new Control[] { _splitContainer, _mainMenu }); + MainMenuStrip = _mainMenu; + MinimumSize = new Size(_intendedWidth + (Width - _intendedWidth), _intendedHeight + (Height - _intendedHeight)); // Borders + Resize += OnResize; + Text = Utils.ProgramName; + + OnResize(null, null); + } + + private void VolumeBar_ValueChanged(object sender, EventArgs e) + { + Engine.Instance.Mixer.SetVolume(_volumeBar.Value / (float)_volumeBar.Maximum); + } + public void SetVolumeBarValue(float volume) + { + _volumeBar.ValueChanged -= VolumeBar_ValueChanged; + _volumeBar.Value = (int)(volume * _volumeBar.Maximum); + _volumeBar.ValueChanged += VolumeBar_ValueChanged; + } + private bool _positionBarFree = true; + private void PositionBar_MouseUp(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + Engine.Instance.Player.SetCurrentPosition(_positionBar.Value); + _positionBarFree = true; + LetUIKnowPlayerIsPlaying(); + } + } + private void PositionBar_MouseDown(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + _positionBarFree = false; + } + } + + private bool _autoplay = false; + private void SongNumerical_ValueChanged(object sender, EventArgs e) + { + _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; + + long index = (long)_songNumerical.Value; + Stop(); + Text = Utils.ProgramName; + _songsComboBox.SelectedIndex = 0; + _songInfo.DeleteData(); + bool success; + try + { + Engine.Instance.Player.LoadSong(index); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, string.Format(Strings.ErrorLoadSong, Engine.Instance.Config.GetSongName(index))); + success = false; + } + + _trackViewer?.UpdateTracks(); + if (success) + { + Config config = Engine.Instance.Config; + List songs = config.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 + Config.Song song = songs.SingleOrDefault(s => s.Index == index); + + // When the song isn't a null value and is played + if (song != null) + { + Text = $"{Utils.ProgramName} - {song.Name}"; // Reads the song name from the .yaml file + _songsComboBox.SelectedIndex = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox + } + + // When the song is a null value and is played + if (song == null) + { + return; // Resets the music player and prevents the song from playing + } + _positionBar.Maximum = Engine.Instance.Player.MaxTicks; + _positionBar.LargeChange = _positionBar.Maximum / 10; + _positionBar.SmallChange = _positionBar.LargeChange / 4; + _songInfo.SetNumTracks(Engine.Instance.Player.Events.Length); + if (_autoplay) + { + Play(); + } + } + else + { + _songInfo.SetNumTracks(0); + } + int numTracks = (Engine.Instance.Player.Events?.Length).GetValueOrDefault(); + _positionBar.Enabled = _exportWAVItem.Enabled = success && numTracks > 0; + _exportMIDIItem.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_MP2K && numTracks > 0; + _exportDLSItem.Enabled = _exportSF2Item.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_AlphaDream; + + _autoplay = true; + _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; + } + private void SongsComboBox_SelectedIndexChanged(object sender, EventArgs e) + { + var item = (ImageComboBoxItem)_songsComboBox.SelectedItem; + if (item.Item is Config.Song song) + { + SetAndLoadSong(song.Index); + } + else if (item.Item is Config.Playlist playlist) + { + if (playlist.Songs.Count > 0 + && FlexibleMessageBox.Show(string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist), Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) + { + ResetPlaylistStuff(false); + _curPlaylist = playlist; + Engine.Instance.Player.ShouldFadeOut = _playlistPlaying = true; + Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + _endPlaylistItem.Enabled = true; + SetAndLoadNextPlaylistSong(); + } + } + } + private void SetAndLoadSong(long index) + { + _curSong = index; + if (_songNumerical.Value == index) + { + SongNumerical_ValueChanged(null, null); + } + else + { + _songNumerical.Value = index; + } + } + private void SetAndLoadNextPlaylistSong() + { + if (_remainingSongs.Count == 0) + { + _remainingSongs.AddRange(_curPlaylist.Songs.Select(s => s.Index)); + if (GlobalConfig.Instance.PlaylistMode == PlaylistMode.Random) + { + _remainingSongs.Shuffle(); + } + } + long nextSong = _remainingSongs[0]; + _remainingSongs.RemoveAt(0); + SetAndLoadSong(nextSong); + } + private void ResetPlaylistStuff(bool enableds) + { + if (Engine.Instance != null) + { + Engine.Instance.Player.ShouldFadeOut = false; + } + _playlistPlaying = false; + _curPlaylist = null; + _curSong = -1; + _remainingSongs.Clear(); + _playedSongs.Clear(); + _endPlaylistItem.Enabled = false; + _songNumerical.Enabled = _songsComboBox.Enabled = enableds; + } + private void EndCurrentPlaylist(object sender, EventArgs e) + { + if (FlexibleMessageBox.Show(Strings.EndPlaylistBody, Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) + { + ResetPlaylistStuff(true); + } + } + + private void OpenDSE(object sender, EventArgs e) + { + var d = new FolderBrowserDialog + { + Description = Strings.MenuOpenDSE, + UseDescriptionForTitle = true + }; + if (d.ShowDialog() == DialogResult.OK) + { + DisposeEngine(); + bool success; + + new Engine(Engine.EngineType.NDS_DSE, d.SelectedPath); + success = true; + if (success != true) + { + Exception ex = new Exception(); + FlexibleMessageBox.Show(ex, Strings.ErrorOpenDSE); + success = false; + } + if (success) + { + var config = (Core.NDS.DSE.Config)Engine.Instance.Config; + FinishLoading(config.BGMFiles.Length); + _songNumerical.Visible = false; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = false; + } + } + } + private void OpenAlphaDream(object sender, EventArgs e) + { + var d = new OpenFileDialog + { + Title = Strings.MenuOpenAlphaDream, + Filter = "Game Boy Advance ROM (*.gba, *.srl)|*.gba;*.srl|All files (*.*)|*.*", + FilterIndex = 2, + RestoreDirectory = true + }; + if (d.ShowDialog() == DialogResult.OK) + { + DisposeEngine(); + bool success; + try + { + new Engine(Engine.EngineType.GBA_AlphaDream, File.ReadAllBytes(d.FileName)); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenAlphaDream); + success = false; + } + if (success) + { + var config = (Core.GBA.AlphaDream.Config)Engine.Instance.Config; + FinishLoading(config.SongTableSizes[0]); + _songNumerical.Visible = true; + _exportDLSItem.Visible = true; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = true; + } + } + } + private void OpenMP2K(object sender, EventArgs e) + { + var d = new OpenFileDialog + { + Title = Strings.MenuOpenMP2K, + Filter = "Game Boy Advance ROM (*.gba, *.srl)|*.gba;*.srl|All files (*.*)|*.*", + FilterIndex = 1, + RestoreDirectory = true + }; + if (d.ShowDialog() == DialogResult.OK) + { + DisposeEngine(); + bool success; + try + { + new Engine(Engine.EngineType.GBA_MP2K, File.ReadAllBytes(d.FileName)); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenMP2K); + success = false; + } + if (success) + { + var config = (Core.GBA.MP2K.Config)Engine.Instance.Config; + FinishLoading(config.SongTableSizes[0]); + _songNumerical.Visible = true; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = true; + _exportSF2Item.Visible = false; + } + } + } + private void OpenSDAT(object sender, EventArgs e) + { + var d = new OpenFileDialog + { + Title = Strings.MenuOpenSDAT, + Filter = "Nitro Soundmaker Sound Data Archive (*.sdat)|*.sdat|All files (*.*)|*.*", + FilterIndex = 1, + RestoreDirectory = true + }; + if (d.ShowDialog() == DialogResult.OK) + { + DisposeEngine(); + bool success; + try + { + new Engine(Engine.EngineType.NDS_SDAT, new Core.NDS.SDAT.SDAT(File.ReadAllBytes(d.FileName))); + success = true; + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorOpenSDAT); + success = false; + } + if (success) + { + var config = (Core.NDS.SDAT.Config)Engine.Instance.Config; + FinishLoading(config.SDAT.INFOBlock.SequenceInfos.NumEntries); + _songNumerical.Visible = true; + _exportDLSItem.Visible = false; + _exportMIDIItem.Visible = false; + _exportSF2Item.Visible = false; + } + } + } + + private void ExportDLS(object sender, EventArgs e) + { + var d = new SaveFileDialog + { + FileName = Engine.Instance.Config.GetGameName(), + Filter = Strings.FilterSaveDLS, + FilterIndex = 1, + ValidateNames = true, + Title = Strings.MenuSaveDLS + }; + if (d.ShowDialog() == DialogResult.OK) + { + try + { + Core.GBA.AlphaDream.SoundFontSaver_DLS.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveDLS, d.FileName), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveDLS); + } + } + } + private void ExportMIDI(object sender, EventArgs e) + { + var d = new SaveFileDialog + { + FileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), + DefaultExt = ".mid", + ValidateNames = true, + Title = Strings.MenuSaveMIDI, + Filter = Strings.FilterSaveMIDI + }; + if (d.ShowDialog() == DialogResult.OK) + { + var p = (Core.GBA.MP2K.Player)Engine.Instance.Player; + var args = new Core.GBA.MP2K.Player.MIDISaveArgs + { + SaveCommandsBeforeTranspose = true, + ReverseVolume = false, + TimeSignatures = new List<(int AbsoluteTick, (byte Numerator, byte Denominator))> + { + (0, (4, 4)) + } + }; + try + { + p.SaveAsMIDI(d.FileName, args); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveMIDI, d.FileName), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveMIDI); + } + } + } + private void ExportSF2(object sender, EventArgs e) + { + var d = new SaveFileDialog + { + FileName = Engine.Instance.Config.GetGameName(), + DefaultExt = ".sf2", + ValidateNames = true, + Title = Strings.MenuSaveSF2, + Filter = Strings.FilterSaveSF2 + }; + if (d.ShowDialog() == DialogResult.OK) + { + try + { + Core.GBA.AlphaDream.SoundFontSaver_SF2.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveSF2, d.FileName), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveSF2); + } + } + } + private void ExportWAV(object sender, EventArgs e) + { + var d = new SaveFileDialog + { + FileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), + DefaultExt = ".wav", + ValidateNames = true, + Title = Strings.MenuSaveWAV, + Filter = Strings.FilterSaveWAV + }; + if (d.ShowDialog() == DialogResult.OK) + { + Stop(); + bool oldFade = Engine.Instance.Player.ShouldFadeOut; + long oldLoops = Engine.Instance.Player.NumLoops; + Engine.Instance.Player.ShouldFadeOut = true; + Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; + try + { + Engine.Instance.Player.Record(d.FileName); + FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, d.FileName), Text); + } + catch (Exception ex) + { + FlexibleMessageBox.Show(ex, Strings.ErrorSaveWAV); + } + Engine.Instance.Player.ShouldFadeOut = oldFade; + Engine.Instance.Player.NumLoops = oldLoops; + _stopUI = false; + } + } + + public void LetUIKnowPlayerIsPlaying() + { + if (!_timer.Enabled) + { + _pauseButton.Enabled = _stopButton.Enabled = true; + _pauseButton.Text = Strings.PlayerPause; + _timer.Interval = (int)(1_000d / GlobalConfig.Instance.RefreshRate); + _timer.Start(); + } + } + private void Play() + { + Engine.Instance.Player.Play(); + LetUIKnowPlayerIsPlaying(); + } + private void Pause() + { + Engine.Instance.Player.Pause(); + if (Engine.Instance.Player.State == PlayerState.Paused) + { + _pauseButton.Text = Strings.PlayerUnpause; + _timer.Stop(); + } + else + { + _pauseButton.Text = Strings.PlayerPause; + _timer.Start(); + } + } + private void Stop() + { + Engine.Instance.Player.Stop(); + _pauseButton.Enabled = _stopButton.Enabled = false; + _pauseButton.Text = Strings.PlayerPause; + _timer.Stop(); + _songInfo.DeleteData(); + _piano.UpdateKeys(_songInfo.Info, PianoTracks); + UpdatePositionIndicators(0L); + } + private void TogglePlayback(object sender, EventArgs e) + { + if (Engine.Instance.Player.State == PlayerState.Stopped) + { + Play(); + } + else if (Engine.Instance.Player.State == PlayerState.Paused || Engine.Instance.Player.State == PlayerState.Playing) + { + Pause(); + } + } + private void PlayPreviousSong(object sender, EventArgs e) + { + long prevSong; + if (_playlistPlaying) + { + int index = _playedSongs.Count - 1; + prevSong = _playedSongs[index]; + _playedSongs.RemoveAt(index); + _remainingSongs.Insert(0, _curSong); + } + else + { + prevSong = (long)_songNumerical.Value - 1; + } + SetAndLoadSong(prevSong); + } + private void PlayNextSong(object sender, EventArgs e) + { + if (_playlistPlaying) + { + _playedSongs.Add(_curSong); + SetAndLoadNextPlaylistSong(); + } + else + { + SetAndLoadSong((long)_songNumerical.Value + 1); + } + } + + private void FinishLoading(long numSongs) + { + Engine.Instance.Player.SongEnded += SongEnded; + foreach (Config.Playlist playlist in Engine.Instance.Config.Playlists) + { + _songsComboBox.Items.Add(new ImageComboBoxItem(playlist, Resources.IconPlaylist, 0)); + _songsComboBox.Items.AddRange(playlist.Songs.Select(s => new ImageComboBoxItem(s, Resources.IconSong, 1)).ToArray()); + } + _songNumerical.Maximum = numSongs - 1; +#if DEBUG + //VGMSDebug.EventScan(Engine.Instance.Config.Playlists[0].Songs, numericalVisible); +#endif + _autoplay = false; + SetAndLoadSong(Engine.Instance.Config.Playlists[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[0].Songs[0].Index); + _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = true; + } + private void DisposeEngine() + { + if (Engine.Instance != null) + { + Stop(); + Engine.Instance.Dispose(); + } + _trackViewer?.UpdateTracks(); + Text = Utils.ProgramName; + _songInfo.SetNumTracks(0); + _songInfo.ResetMutes(); + ResetPlaylistStuff(false); + UpdatePositionIndicators(0L); + _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; + _songNumerical.ValueChanged -= SongNumerical_ValueChanged; + _songNumerical.Visible = false; + _songNumerical.Value = _songNumerical.Maximum = 0; + _songsComboBox.SelectedItem = null; + _songsComboBox.Items.Clear(); + _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; + _songNumerical.ValueChanged += SongNumerical_ValueChanged; + } + private bool _stopUI = false; + private void UpdateUI(object sender, EventArgs e) + { + if (_stopUI) + { + _stopUI = false; + if (_playlistPlaying) + { + _playedSongs.Add(_curSong); + SetAndLoadNextPlaylistSong(); + } + else + { + Stop(); + } + } + else + { + if (WindowState != FormWindowState.Minimized) + { + SongInfoControl.SongInfo info = _songInfo.Info; + Engine.Instance.Player.GetSongState(info); + _piano.UpdateKeys(info, PianoTracks); + _songInfo.Invalidate(); + } + UpdatePositionIndicators(Engine.Instance.Player.ElapsedTicks); + } + } + private void SongEnded() + { + _stopUI = true; + } + + private void UpdatePositionIndicators(long ticks) + { + if (_positionBarFree) + { + _positionBar.Value = ticks; + } + } + + private void OpenTrackViewer(object sender, EventArgs e) + { + if (_trackViewer != null) + { + _trackViewer.Focus(); + return; + } + _trackViewer = new TrackViewer { Owner = this }; + _trackViewer.FormClosed += (o, s) => _trackViewer = null; + _trackViewer.Show(); + } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + DisposeEngine(); + base.OnFormClosing(e); + } + private void OnResize(object sender, EventArgs e) + { + if (WindowState != FormWindowState.Minimized) + { + _splitContainer.SplitterDistance = (int)(ClientSize.Height / 5.5) - 25; // -25 for menustrip (24) and itself (1) + + int w1 = (int)(_splitContainer.Panel1.Width / 2.35); + int h1 = (int)(_splitContainer.Panel1.Height / 5.0); + + int xoff = _splitContainer.Panel1.Width / 83; + int yoff = _splitContainer.Panel1.Height / 25; + int a, b, c, d; + + // Buttons + a = (w1 / 3) - xoff; + b = (xoff / 2) + 1; + _playButton.Location = new Point(xoff + b, yoff); + _pauseButton.Location = new Point((xoff * 2) + a + b, yoff); + _stopButton.Location = new Point((xoff * 3) + (a * 2) + b, yoff); + _playButton.Size = _pauseButton.Size = _stopButton.Size = new Size(a, h1); + c = yoff + ((h1 - 21) / 2); + _songNumerical.Location = new Point((xoff * 4) + (a * 3) + b, c); + _songNumerical.Size = new Size((int)(a / 1.175), 21); + // Song combobox + d = _splitContainer.Panel1.Width - w1 - xoff; + _songsComboBox.Location = new Point(d, c); + _songsComboBox.Size = new Size(w1, 21); + + // Volume bar + c = (int)(_splitContainer.Panel1.Height / 3.5); + _volumeBar.Location = new Point(xoff, c); + _volumeBar.Size = new Size(w1, h1); + // Position bar + _positionBar.Location = new Point(d, c); + _positionBar.Size = new Size(w1, h1); + + // Piano + _piano.Size = new Size(_splitContainer.Panel1.Width, (int)(_splitContainer.Panel1.Height / 2.5)); // Force it to initialize piano keys again + _piano.Location = new Point((_splitContainer.Panel1.Width - (_piano.WhiteKeyWidth * PianoControl.WhiteKeyCount)) / 2, _splitContainer.Panel1.Height - _piano.Height - 1); + } + } + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + if (keyData == Keys.Space && _playButton.Enabled && !_songsComboBox.Focused) + { + TogglePlayback(null, null); + return true; + } + else + { + return base.ProcessCmdKey(ref msg, keyData); + } + } + } +} diff --git a/WinForms/UI/PianoControl.cs b/WinForms/UI/PianoControl.cs new file mode 100644 index 00000000..102a4b0a --- /dev/null +++ b/WinForms/UI/PianoControl.cs @@ -0,0 +1,183 @@ +#region License + +/* Copyright (c) 2006 Leslie Sanford + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#endregion + +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Util; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory("")] + internal class PianoControl : Control + { + private enum KeyType : byte + { + Black, + White + } + private static readonly KeyType[] KeyTypeTable = new KeyType[12] + { + KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White + }; + private const double blackKeyScale = 2.0 / 3.0; + + public class PianoKey : Control + { + public bool Dirty; + public bool Pressed; + + public readonly SolidBrush OnBrush = new SolidBrush(Color.Transparent); + private readonly SolidBrush _offBrush; + + public PianoKey(byte k) + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, false); + _offBrush = new SolidBrush(new HSLColor(160.0, 0.0, KeyTypeTable[k % 12] == KeyType.White ? k / 12 % 2 == 0 ? 240.0 : 120.0 : 0.0)); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + OnBrush.Dispose(); + _offBrush.Dispose(); + } + base.Dispose(disposing); + } + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.FillRectangle(Pressed ? OnBrush : _offBrush, 1, 1, Width - 2, Height - 2); + e.Graphics.DrawRectangle(Pens.Black, 0, 0, Width - 1, Height - 1); + base.OnPaint(e); + } + } + + private readonly PianoKey[] _keys = new PianoKey[0x80]; + public const int WhiteKeyCount = 75; + public int WhiteKeyWidth; + + public PianoControl() + { + SetStyle(ControlStyles.Selectable, false); + for (byte k = 0; k <= 0x7F; k++) + { + var key = new PianoKey(k); + _keys[k] = key; + if (KeyTypeTable[k % 12] == KeyType.Black) + { + key.BringToFront(); + } + Controls.Add(key); + } + SetKeySizes(); + } + private void SetKeySizes() + { + WhiteKeyWidth = Width / WhiteKeyCount; + int blackKeyWidth = (int)(WhiteKeyWidth * blackKeyScale); + int blackKeyHeight = (int)(Height * blackKeyScale); + int offset = WhiteKeyWidth - (blackKeyWidth / 2); + int w = 0; + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + if (KeyTypeTable[k % 12] == KeyType.White) + { + key.Height = Height; + key.Width = WhiteKeyWidth; + key.Location = new Point(w * WhiteKeyWidth, 0); + w++; + } + else + { + key.Height = blackKeyHeight; + key.Width = blackKeyWidth; + key.Location = new Point(offset + ((w - 1) * WhiteKeyWidth)); + key.BringToFront(); + } + } + } + + public void UpdateKeys(SongInfoControl.SongInfo info, bool[] enabledTracks) + { + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + key.Dirty = key.Pressed; + key.Pressed = false; + } + for (int i = SongInfoControl.SongInfo.MaxTracks - 1; i >= 0; i--) + { + if (enabledTracks[i]) + { + SongInfoControl.SongInfo.Track tin = info.Tracks[i]; + for (int nk = 0; nk < SongInfoControl.SongInfo.MaxKeys; nk++) + { + byte k = tin.Keys[nk]; + if (k == byte.MaxValue) + { + break; + } + else + { + PianoKey key = _keys[k]; + key.OnBrush.Color = GlobalConfig.Instance.Colors[tin.Voice]; + key.Pressed = key.Dirty = true; + } + } + } + } + for (int k = 0; k <= 0x7F; k++) + { + PianoKey key = _keys[k]; + if (key.Dirty) + { + key.Invalidate(); + } + } + } + + protected override void OnResize(EventArgs e) + { + SetKeySizes(); + base.OnResize(e); + } + protected override void Dispose(bool disposing) + { + if (disposing) + { + for (int k = 0; k < 0x80; k++) + { + _keys[k].Dispose(); + } + } + base.Dispose(disposing); + } + } +} diff --git a/WinForms/UI/SongInfoControl.cs b/WinForms/UI/SongInfoControl.cs new file mode 100644 index 00000000..ce91a71d --- /dev/null +++ b/WinForms/UI/SongInfoControl.cs @@ -0,0 +1,354 @@ +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory("")] + internal class SongInfoControl : Control + { + public class SongInfo + { + public class Track + { + public long Position; + public byte Voice; + public byte Volume; + public int LFO; + public long Rest; + public sbyte Panpot; + public float LeftVolume; + public float RightVolume; + public int PitchBend; + public byte Extra; + public string Type; + public byte[] Keys = new byte[MaxKeys]; + + public int PreviousKeysTime; + public string PreviousKeys; + + public Track() + { + for (int i = 0; i < MaxKeys; i++) + { + Keys[i] = byte.MaxValue; + } + } + } + public const int MaxKeys = 32 + 1; // DSE is currently set to use 32 channels + public const int MaxTracks = 18; // PMD2 has a few songs with 18 tracks + + public ushort Tempo; + public Track[] Tracks = new Track[MaxTracks]; + + public SongInfo() + { + for (int i = 0; i < MaxTracks; i++) + { + Tracks[i] = new Track(); + } + } + } + + private const int _checkboxSize = 15; + + private readonly CheckBox[] _mutes; + private readonly CheckBox[] _pianos; + private readonly SolidBrush _solidBrush = new SolidBrush(Theme.PlayerColor); + private readonly Pen _pen = new Pen(Color.Transparent); + + public SongInfo Info; + private int _numTracksToDraw; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _solidBrush.Dispose(); + _pen.Dispose(); + } + base.Dispose(disposing); + } + public SongInfoControl() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, false); + Font = new Font("Segoe UI", 10.5f, FontStyle.Regular, GraphicsUnit.Point); + Size = new Size(675, 675); + + _pianos = new CheckBox[SongInfo.MaxTracks + 1]; + _mutes = new CheckBox[SongInfo.MaxTracks + 1]; + for (int i = 0; i < SongInfo.MaxTracks + 1; i++) + { + _pianos[i] = new CheckBox + { + BackColor = Color.Transparent, + Checked = true, + Size = new Size(_checkboxSize, _checkboxSize), + TabStop = false + }; + _pianos[i].CheckStateChanged += TogglePiano; + _mutes[i] = new CheckBox + { + BackColor = Color.Transparent, + Checked = true, + Size = new Size(_checkboxSize, _checkboxSize), + TabStop = false + }; + _mutes[i].CheckStateChanged += ToggleMute; + } + Controls.AddRange(_pianos); + Controls.AddRange(_mutes); + + SetNumTracks(0); + DeleteData(); + } + + private void TogglePiano(object sender, EventArgs e) + { + var check = (CheckBox)sender; + CheckBox master = _pianos[SongInfo.MaxTracks]; + if (check == master) + { + bool b = check.CheckState != CheckState.Unchecked; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + _pianos[i].Checked = b; + } + } + else + { + int numChecked = 0; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + if (_pianos[i] == check) + { + MainForm.Instance.PianoTracks[i] = _pianos[i].Checked; + } + if (_pianos[i].Checked) + { + numChecked++; + } + } + master.CheckStateChanged -= TogglePiano; + master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); + master.CheckStateChanged += TogglePiano; + } + } + private void ToggleMute(object sender, EventArgs e) + { + var check = (CheckBox)sender; + CheckBox master = _mutes[SongInfo.MaxTracks]; + if (check == master) + { + bool b = check.CheckState != CheckState.Unchecked; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + _mutes[i].Checked = b; + } + } + else + { + int numChecked = 0; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + if (_mutes[i] == check) + { + Engine.Instance.Mixer.Mutes[i] = !check.Checked; + } + if (_mutes[i].Checked) + { + numChecked++; + } + } + master.CheckStateChanged -= ToggleMute; + master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); + master.CheckStateChanged += ToggleMute; + } + } + + public void DeleteData() + { + Info = new SongInfo(); + Invalidate(); + } + public void SetNumTracks(int num) + { + _numTracksToDraw = num; + _pianos[SongInfo.MaxTracks].Enabled = _mutes[SongInfo.MaxTracks].Enabled = num > 0; + for (int i = 0; i < SongInfo.MaxTracks; i++) + { + _pianos[i].Visible = _mutes[i].Visible = i < num; + } + OnResize(EventArgs.Empty); + } + public void ResetMutes() + { + for (int i = 0; i < SongInfo.MaxTracks + 1; i++) + { + CheckBox mute = _mutes[i]; + mute.CheckStateChanged -= ToggleMute; + mute.CheckState = CheckState.Checked; + mute.CheckStateChanged += ToggleMute; + } + } + + private float _infoHeight, _infoY, _positionX, _keysX, _delayX, _typeEndX, _typeX, _voicesX, _row2ElementAdditionX, _yMargin, _trackHeight, _row2Offset, _tempoX; + private int _barHeight, _barStartX, _barWidth, _bwd, _barRightBoundX, _barCenterX; + protected override void OnResize(EventArgs e) + { + _infoHeight = Height / 30f; + _infoY = _infoHeight - (TextRenderer.MeasureText("A", Font).Height * 1.125f); + _positionX = (_checkboxSize * 2) + 2; + int fWidth = Width - (int)_positionX; // Width between checkboxes' edges and the window edge + _keysX = _positionX + (fWidth / 4.4f); + _delayX = _positionX + (fWidth / 7.5f); + _typeEndX = _positionX + fWidth - (fWidth / 100f); + _typeX = _typeEndX - TextRenderer.MeasureText(Strings.PlayerType, Font).Width; + _voicesX = _positionX + (fWidth / 25f); + _row2ElementAdditionX = fWidth / 15f; + + _yMargin = Height / 200f; + _trackHeight = (Height - _yMargin) / ((_numTracksToDraw < 16 ? 16 : _numTracksToDraw) * 1.04f); + _row2Offset = _trackHeight / 2.5f; + _barHeight = (int)(Height / 30.3f); + _barStartX = (int)(_positionX + (fWidth / 2.35f)); + _barWidth = (int)(fWidth / 2.95f); + _bwd = _barWidth % 2; // Add/Subtract by 1 if the bar width is odd + _barRightBoundX = _barStartX + _barWidth - _bwd; + _barCenterX = _barStartX + (_barWidth / 2); + + _tempoX = _barCenterX - (TextRenderer.MeasureText(string.Format("{0} - 999", Strings.PlayerTempo), Font).Width / 2); + + if (_mutes != null) + { + int x1 = 3; + int x2 = _checkboxSize + 4; + int y = (int)_infoY + 3; + _mutes[SongInfo.MaxTracks].Location = new Point(x1, y); + _pianos[SongInfo.MaxTracks].Location = new Point(x2, y); + for (int i = 0; i < _numTracksToDraw; i++) + { + float r1y = _infoHeight + _yMargin + (i * _trackHeight); + y = (int)r1y + 4; + _mutes[i].Location = new Point(x1, y); + _pianos[i].Location = new Point(x2, y); + } + } + + base.OnResize(e); + } + protected override void OnPaint(PaintEventArgs e) + { + _solidBrush.Color = Theme.PlayerColor; + e.Graphics.FillRectangle(_solidBrush, e.ClipRectangle); + + e.Graphics.DrawString(Strings.PlayerPosition, Font, Brushes.Lime, _positionX, _infoY); + e.Graphics.DrawString(Strings.PlayerRest, Font, Brushes.Crimson, _delayX, _infoY); + e.Graphics.DrawString(Strings.PlayerNotes, Font, Brushes.Turquoise, _keysX, _infoY); + e.Graphics.DrawString("L", Font, Brushes.GreenYellow, _barStartX - 5, _infoY); + e.Graphics.DrawString(string.Format("{0} - ", Strings.PlayerTempo) + Info.Tempo, Font, Brushes.Cyan, _tempoX, _infoY); + e.Graphics.DrawString("R", Font, Brushes.GreenYellow, _barRightBoundX - 5, _infoY); + e.Graphics.DrawString(Strings.PlayerType, Font, Brushes.DeepPink, _typeX, _infoY); + e.Graphics.DrawLine(Pens.Gold, 0, _infoHeight, Width, _infoHeight); + + for (int i = 0; i < _numTracksToDraw; i++) + { + SongInfo.Track track = Info.Tracks[i]; + float r1y = _infoHeight + _yMargin + (i * _trackHeight); // Row 1 y + e.Graphics.DrawString(string.Format("0x{0:X}", track.Position), Font, Brushes.Lime, _positionX, r1y); + e.Graphics.DrawString(track.Rest.ToString(), Font, Brushes.Crimson, _delayX, r1y); + + float r2y = r1y + _row2Offset; // Row 2 y + e.Graphics.DrawString(track.Panpot.ToString(), Font, Brushes.OrangeRed, _voicesX + _row2ElementAdditionX, r2y); + e.Graphics.DrawString(track.Volume.ToString(), Font, Brushes.LightSeaGreen, _voicesX + (_row2ElementAdditionX * 2), r2y); + e.Graphics.DrawString(track.LFO.ToString(), Font, Brushes.SkyBlue, _voicesX + (_row2ElementAdditionX * 3), r2y); + e.Graphics.DrawString(track.PitchBend.ToString(), Font, Brushes.Purple, _voicesX + (_row2ElementAdditionX * 4), r2y); + e.Graphics.DrawString(track.Extra.ToString(), Font, Brushes.HotPink, _voicesX + (_row2ElementAdditionX * 5), r2y); + + int by = (int)(r1y + _yMargin); // Bar y + int byh = by + _barHeight; + e.Graphics.DrawString(track.Type, Font, Brushes.DeepPink, _typeEndX - e.Graphics.MeasureString(track.Type, Font).Width, by + (_row2Offset / (Font.Size / 2.5f))); + e.Graphics.DrawLine(Pens.GreenYellow, _barStartX, by, _barStartX, byh); // Left bar bound line + e.Graphics.DrawLine(Pens.GreenYellow, _barRightBoundX, by, _barRightBoundX, byh); // Right bar bound line + if (GlobalConfig.Instance.PanpotIndicators) + { + int pax = (int)(_barStartX + (_barWidth / 2) + (_barWidth / 2 * (track.Panpot / (float)0x40))); // Pan line x + e.Graphics.DrawLine(Pens.OrangeRed, pax, by, pax, byh); // Pan line + } + + { + Color color = GlobalConfig.Instance.Colors[track.Voice]; + _solidBrush.Color = color; + _pen.Color = color; + e.Graphics.DrawString(track.Voice.ToString(), Font, _solidBrush, _voicesX, r2y); + var rect = new Rectangle((int)(_barStartX + (_barWidth / 2) - (track.LeftVolume * _barWidth / 2)) + _bwd, + by, + (int)((track.LeftVolume + track.RightVolume) * _barWidth / 2), + _barHeight); + if (!rect.IsEmpty) + { + float velocity = (track.LeftVolume + track.RightVolume) * 2f; + if (velocity > 1f) + { + velocity = 1f; + } + _solidBrush.Color = Color.FromArgb((byte)(velocity * byte.MaxValue), color); + e.Graphics.FillRectangle(_solidBrush, rect); + e.Graphics.DrawRectangle(_pen, rect); + } + if (GlobalConfig.Instance.CenterIndicators) + { + e.Graphics.DrawLine(_pen, _barCenterX, by, _barCenterX, byh); // Center line + } + } + { + string keysString; + if (track.Keys[0] == byte.MaxValue) + { + if (track.PreviousKeysTime != 0) + { + track.PreviousKeysTime--; + keysString = track.PreviousKeys; + } + else + { + keysString = string.Empty; + } + } + else + { + keysString = string.Empty; + for (int nk = 0; nk < SongInfo.MaxKeys; nk++) + { + byte k = track.Keys[nk]; + if (k == byte.MaxValue) + { + break; + } + else + { + if (nk != 0) + { + keysString += ' '; + } + keysString += Utils.GetNoteName(k); + } + } + track.PreviousKeysTime = GlobalConfig.Instance.RefreshRate << 2; + track.PreviousKeys = keysString; + } + if (keysString != string.Empty) + { + e.Graphics.DrawString(keysString, Font, Brushes.Turquoise, _keysX, r1y); + } + } + } + base.OnPaint(e); + } + } +} diff --git a/WinForms/UI/Theme.cs b/WinForms/UI/Theme.cs new file mode 100644 index 00000000..0973e2a7 --- /dev/null +++ b/WinForms/UI/Theme.cs @@ -0,0 +1,165 @@ +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + internal static class Theme + { + public static readonly Font Font = new Font("Segoe UI", 8f, FontStyle.Bold); + public static readonly Color + BackColor = Color.FromArgb(33, 33, 39), + BackColorDisabled = Color.FromArgb(35, 42, 47), + BackColorMouseOver = Color.FromArgb(32, 37, 47), + BorderColor = Color.FromArgb(25, 120, 186), + BorderColorDisabled = Color.FromArgb(47, 55, 60), + ForeColor = Color.FromArgb(94, 159, 230), + PlayerColor = Color.FromArgb(8, 8, 8), + SelectionColor = Color.FromArgb(7, 51, 141), + TitleBar = Color.FromArgb(16, 40, 63); + + public static HSLColor DrainColor(Color c) + { + var drained = new HSLColor(c); + drained.Saturation /= 2.5; + return drained; + } + } + + internal class ThemedButton : Button + { + public ThemedButton() : base() + { + FlatAppearance.MouseOverBackColor = Theme.BackColorMouseOver; + FlatStyle = FlatStyle.Flat; + Font = Theme.Font; + ForeColor = Theme.ForeColor; + } + protected override void OnEnabledChanged(EventArgs e) + { + base.OnEnabledChanged(e); + BackColor = Enabled ? Theme.BackColor : Theme.BackColorDisabled; + FlatAppearance.BorderColor = Enabled ? Theme.BorderColor : Theme.BorderColorDisabled; + } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + if (!Enabled) + { + TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, Theme.DrainColor(ForeColor), BackColor); + } + } + protected override bool ShowFocusCues => false; + } + internal class ThemedLabel : Label + { + public ThemedLabel() : base() + { + Font = Theme.Font; + ForeColor = Theme.ForeColor; + } + } + internal class ThemedForm : Form + { + public ThemedForm() : base() + { + BackColor = Theme.BackColor; + Icon = Resources.Icon; + } + } + internal class ThemedPanel : Panel + { + public ThemedPanel() : base() + { + SetStyle(ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); + } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + using (var b = new SolidBrush(BackColor)) + { + e.Graphics.FillRectangle(b, e.ClipRectangle); + } + using (var b = new SolidBrush(Theme.BorderColor)) + using (var p = new Pen(b, 2)) + { + e.Graphics.DrawRectangle(p, e.ClipRectangle); + } + } + private const int WM_PAINT = 0xF; + protected override void WndProc(ref Message m) + { + if (m.Msg == WM_PAINT) + { + Invalidate(); + } + base.WndProc(ref m); + } + } + internal class ThemedTextBox : TextBox + { + public ThemedTextBox() : base() + { + BackColor = Theme.BackColor; + Font = Theme.Font; + ForeColor = Theme.ForeColor; + } + [DllImport("user32.dll")] + private static extern IntPtr GetWindowDC(IntPtr hWnd); + [DllImport("user32.dll")] + private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); + [DllImport("user32.dll")] + private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprc, IntPtr hrgn, uint flags); + private const int WM_NCPAINT = 0x85; + private const uint RDW_INVALIDATE = 0x1; + private const uint RDW_IUPDATENOW = 0x100; + private const uint RDW_FRAME = 0x400; + protected override void WndProc(ref Message m) + { + base.WndProc(ref m); + if (m.Msg == WM_NCPAINT && BorderStyle == BorderStyle.Fixed3D) + { + IntPtr hdc = GetWindowDC(Handle); + using (var g = Graphics.FromHdcInternal(hdc)) + using (var p = new Pen(Theme.BorderColor)) + { + g.DrawRectangle(p, new Rectangle(0, 0, Width - 1, Height - 1)); + } + ReleaseDC(Handle, hdc); + } + } + protected override void OnSizeChanged(EventArgs e) + { + base.OnSizeChanged(e); + RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero, RDW_FRAME | RDW_IUPDATENOW | RDW_INVALIDATE); + } + } + internal class ThemedRichTextBox : RichTextBox + { + public ThemedRichTextBox() : base() + { + BackColor = Theme.BackColor; + Font = Theme.Font; + ForeColor = Theme.ForeColor; + SelectionColor = Theme.SelectionColor; + } + } + internal class ThemedNumeric : NumericUpDown + { + public ThemedNumeric() : base() + { + BackColor = Theme.BackColor; + Font = new Font(Theme.Font.FontFamily, 7.5f, Theme.Font.Style); + ForeColor = Theme.ForeColor; + TextAlign = HorizontalAlignment.Center; + } + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Enabled ? Theme.BorderColor : Theme.BorderColorDisabled, ButtonBorderStyle.Solid); + } + } +} diff --git a/WinForms/UI/TrackViewer.cs b/WinForms/UI/TrackViewer.cs new file mode 100644 index 00000000..7aa83b97 --- /dev/null +++ b/WinForms/UI/TrackViewer.cs @@ -0,0 +1,113 @@ +using BrightIdeasSoftware; +using Kermalis.VGMusicStudio.Core; +using Kermalis.VGMusicStudio.Properties; +using Kermalis.VGMusicStudio.Util; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + [DesignerCategory("")] + internal class TrackViewer : ThemedForm + { + private List _events; + private readonly ObjectListView _listView; + private readonly ComboBox _tracksBox; + + public TrackViewer() + { + int w = (600 / 2) - 12 - 6, h = 400 - 12 - 11; + _listView = new ObjectListView + { + FullRowSelect = true, + HeaderStyle = ColumnHeaderStyle.Nonclickable, + HideSelection = false, + Location = new Point(12, 12), + MultiSelect = false, + RowFormatter = RowColorer, + ShowGroups = false, + Size = new Size(w, h), + UseFiltering = true, + UseFilterIndicator = true + }; + OLVColumn c1, c2, c3, c4; + c1 = new OLVColumn(Strings.TrackViewerEvent, "Command.Label"); + c2 = new OLVColumn(Strings.TrackViewerArguments, "Command.Arguments") { UseFiltering = false }; + c3 = new OLVColumn(Strings.TrackViewerOffset, "Offset") { AspectToStringFormat = "0x{0:X}", UseFiltering = false }; + c4 = new OLVColumn(Strings.TrackViewerTicks, "Ticks") { AspectGetter = (o) => string.Join(", ", ((SongEvent)o).Ticks), UseFiltering = false }; + c1.Width = c2.Width = c3.Width = 72; + c4.Width = 47; + c1.Hideable = c2.Hideable = c3.Hideable = c4.Hideable = false; + c1.TextAlign = c2.TextAlign = c3.TextAlign = c4.TextAlign = HorizontalAlignment.Center; + _listView.AllColumns.AddRange(new OLVColumn[] { c1, c2, c3, c4 }); + _listView.RebuildColumns(); + _listView.ItemActivate += ListView_ItemActivate; + + var panel1 = new ThemedPanel { Location = new Point(306, 12), Size = new Size(w, h) }; + _tracksBox = new ComboBox + { + Enabled = false, + Location = new Point(4, 4), + Size = new Size(100, 21) + }; + _tracksBox.SelectedIndexChanged += TracksBox_SelectedIndexChanged; + panel1.Controls.AddRange(new Control[] { _tracksBox }); + + ClientSize = new Size(600, 400); + Controls.AddRange(new Control[] { _listView, panel1 }); + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + Text = $"{Utils.ProgramName} ― {Strings.TrackViewerTitle}"; + + UpdateTracks(); + } + + private void ListView_ItemActivate(object sender, EventArgs e) + { + List list = ((SongEvent)_listView.SelectedItem.RowObject).Ticks; + if (list.Count > 0) + { + Engine.Instance?.Player.SetCurrentPosition(list[0]); + MainForm.Instance.LetUIKnowPlayerIsPlaying(); + } + } + + private void RowColorer(OLVListItem item) + { + item.BackColor = ((SongEvent)item.RowObject).Command.Color; + } + + private void TracksBox_SelectedIndexChanged(object sender, EventArgs e) + { + int i = _tracksBox.SelectedIndex; + if (i != -1) + { + _events = Engine.Instance.Player.Events[i]; + _listView.SetObjects(_events); + } + else + { + _listView.Items.Clear(); + } + } + public void UpdateTracks() + { + int numTracks = (Engine.Instance?.Player.Events?.Length).GetValueOrDefault(); + bool tracks = numTracks > 0; + _tracksBox.Enabled = tracks; + if (tracks) + { + // Track 0, Track 1, ... + _tracksBox.DataSource = Enumerable.Range(0, numTracks).Select(i => string.Format(Strings.TrackViewerTrackX, i)).ToList(); + } + else + { + _tracksBox.DataSource = null; + } + } + } +} \ No newline at end of file diff --git a/WinForms/UI/ValueTextBox.cs b/WinForms/UI/ValueTextBox.cs new file mode 100644 index 00000000..a83e3516 --- /dev/null +++ b/WinForms/UI/ValueTextBox.cs @@ -0,0 +1,104 @@ +using Kermalis.VGMusicStudio.Util; +using System; +using System.Windows.Forms; + +namespace Kermalis.VGMusicStudio.UI +{ + internal class ValueTextBox : ThemedTextBox + { + private bool _hex = false; + public bool Hexadecimal + { + get => _hex; + set + { + _hex = value; + OnTextChanged(EventArgs.Empty); + SelectionStart = Text.Length; + } + } + private long _max = long.MaxValue; + public long Maximum + { + get => _max; + set + { + _max = value; + OnTextChanged(EventArgs.Empty); + } + } + private long _min = 0; + public long Minimum + { + get => _min; + set + { + _min = value; + OnTextChanged(EventArgs.Empty); + } + } + public long Value + { + get + { + if (TextLength > 0) + { + if (Utils.TryParseValue(Text, _min, _max, out long l)) + { + return l; + } + } + return _min; + } + set + { + int i = SelectionStart; + Text = Hexadecimal ? ("0x" + value.ToString("X")) : value.ToString(); + SelectionStart = i; + OnValueChanged(EventArgs.Empty); + } + } + + protected override void WndProc(ref Message m) + { + const int WM_NOTIFY = 0x0282; + if (m.Msg == WM_NOTIFY && m.WParam == new IntPtr(0xB)) + { + if (Hexadecimal && SelectionStart < 2) + { + SelectionStart = 2; + } + } + base.WndProc(ref m); + } + protected override void OnKeyPress(KeyPressEventArgs e) + { + e.Handled = true; // Don't pay attention to this event unless: + + if ((char.IsControl(e.KeyChar) && !(Hexadecimal && SelectionStart <= 2 && SelectionLength == 0 && e.KeyChar == (char)Keys.Back)) || // Backspace isn't used on the "0x" prefix + char.IsDigit(e.KeyChar) || // It is a digit + (e.KeyChar >= 'a' && e.KeyChar <= 'f') || // It is a letter that shows in hex + (e.KeyChar >= 'A' && e.KeyChar <= 'F')) + { + e.Handled = false; + } + base.OnKeyPress(e); + } + protected override void OnTextChanged(EventArgs e) + { + base.OnTextChanged(e); + Value = Value; + } + + private EventHandler _onValueChanged = null; + public event EventHandler ValueChanged + { + add => _onValueChanged += value; + remove => _onValueChanged -= value; + } + protected virtual void OnValueChanged(EventArgs e) + { + _onValueChanged?.Invoke(this, e); + } + } +} diff --git a/WinForms/Util/HSLColor.cs b/WinForms/Util/HSLColor.cs new file mode 100644 index 00000000..6dbda597 --- /dev/null +++ b/WinForms/Util/HSLColor.cs @@ -0,0 +1,161 @@ +using System.Drawing; + +namespace Kermalis.VGMusicStudio.Util +{ + // https://richnewman.wordpress.com/about/code-listings-and-diagrams/hslcolor-class/ + class HSLColor + { + // Private data members below are on scale 0-1 + // They are scaled for use externally based on scale + private double hue = 1.0; + private double saturation = 1.0; + private double luminosity = 1.0; + + private const double scale = 240.0; + + public double Hue + { + get { return hue * scale; } + set { hue = CheckRange(value / scale); } + } + public double Saturation + { + get { return saturation * scale; } + set { saturation = CheckRange(value / scale); } + } + public double Luminosity + { + get { return luminosity * scale; } + set { luminosity = CheckRange(value / scale); } + } + + private double CheckRange(double value) + { + if (value < 0.0) + { + value = 0.0; + } + else if (value > 1.0) + { + value = 1.0; + } + return value; + } + + public override string ToString() + { + return string.Format("H: {0:#0.##} S: {1:#0.##} L: {2:#0.##}", Hue, Saturation, Luminosity); + } + + public string ToRGBString() + { + Color color = this; + return string.Format("R: {0:#0.##} G: {1:#0.##} B: {2:#0.##}", color.R, color.G, color.B); + } + + #region Casts to/from System.Drawing.Color + public static implicit operator Color(HSLColor hslColor) + { + double r = 0, g = 0, b = 0; + if (hslColor.luminosity != 0) + { + if (hslColor.saturation == 0) + { + r = g = b = hslColor.luminosity; + } + else + { + double temp2 = GetTemp2(hslColor); + double temp1 = 2.0 * hslColor.luminosity - temp2; + + r = GetColorComponent(temp1, temp2, hslColor.hue + 1.0 / 3.0); + g = GetColorComponent(temp1, temp2, hslColor.hue); + b = GetColorComponent(temp1, temp2, hslColor.hue - 1.0 / 3.0); + } + } + return Color.FromArgb((int)(255 * r), (int)(255 * g), (int)(255 * b)); + } + + private static double GetColorComponent(double temp1, double temp2, double temp3) + { + temp3 = MoveIntoRange(temp3); + if (temp3 < 1.0 / 6.0) + { + return temp1 + (temp2 - temp1) * 6.0 * temp3; + } + else if (temp3 < 0.5) + { + return temp2; + } + else if (temp3 < 2.0 / 3.0) + { + return temp1 + ((temp2 - temp1) * ((2.0 / 3.0) - temp3) * 6.0); + } + else + { + return temp1; + } + } + private static double MoveIntoRange(double temp3) + { + if (temp3 < 0.0) + { + temp3 += 1.0; + } + else if (temp3 > 1.0) + { + temp3 -= 1.0; + } + return temp3; + } + private static double GetTemp2(HSLColor hslColor) + { + double temp2; + if (hslColor.luminosity < 0.5) //<=?? + { + temp2 = hslColor.luminosity * (1.0 + hslColor.saturation); + } + else + { + temp2 = hslColor.luminosity + hslColor.saturation - (hslColor.luminosity * hslColor.saturation); + } + return temp2; + } + + public static implicit operator HSLColor(Color color) + { + HSLColor hslColor = new HSLColor + { + hue = color.GetHue() / 360.0, // We store hue as 0-1 as opposed to 0-360 + luminosity = color.GetBrightness(), + saturation = color.GetSaturation() + }; + return hslColor; + } + #endregion + + public void SetRGB(int red, int green, int blue) + { + HSLColor hslColor = Color.FromArgb(red, green, blue); + hue = hslColor.hue; + saturation = hslColor.saturation; + luminosity = hslColor.luminosity; + } + + public HSLColor() { } + public HSLColor(Color color) + { + SetRGB(color.R, color.G, color.B); + } + public HSLColor(int red, int green, int blue) + { + SetRGB(red, green, blue); + } + public HSLColor(double hue, double saturation, double luminosity) + { + Hue = hue; + Saturation = saturation; + Luminosity = luminosity; + } + } +} diff --git a/WinForms/WinForms.csproj b/WinForms/WinForms.csproj new file mode 100644 index 00000000..ec3e0b7b --- /dev/null +++ b/WinForms/WinForms.csproj @@ -0,0 +1,57 @@ + + + + WinExe + net6.0-windows + enable + true + enable + publish\ + true + Disk + + + + ..\Build\ + Off + + + + ..\Build\ + On + + + + Properties\Icon.ico + + + + Kermalis.VGMusicStudio.Program + + + + + + + + + Component + + + Component + + + + + + Designer + + + Designer + + + Designer + + + + \ No newline at end of file From faeeaffd734664c440b43941e63f446dcbbd2c44 Mon Sep 17 00:00:00 2001 From: Davin Date: Wed, 31 Aug 2022 00:30:17 +1000 Subject: [PATCH 7/8] My new separate-ui branch encountered a problem and broke, and kinda imported what I didn't want to import to the main, so the winforms folder is removed --- WinForms/Properties/Icon48.png | Bin 870 -> 0 bytes WinForms/Properties/Icon528.png | Bin 10133 -> 0 bytes WinForms/Properties/Next.ico | Bin 140062 -> 0 bytes WinForms/Properties/Next.png | Bin 4026 -> 0 bytes WinForms/Properties/Pause.ico | Bin 118062 -> 0 bytes WinForms/Properties/Pause.png | Bin 1450 -> 0 bytes WinForms/Properties/Play.ico | Bin 140062 -> 0 bytes WinForms/Properties/Play.png | Bin 2298 -> 0 bytes WinForms/Properties/Playlist.png | Bin 3045 -> 0 bytes WinForms/Properties/Previous.ico | Bin 140062 -> 0 bytes WinForms/Properties/Previous.png | Bin 3708 -> 0 bytes WinForms/Properties/Resources.Designer.cs | 133 ---- WinForms/Properties/Resources.resx | 142 ---- WinForms/Properties/Settings.Designer.cs | 26 - WinForms/Properties/Settings.settings | 7 - WinForms/Properties/Song.png | Bin 2328 -> 0 bytes WinForms/Properties/Strings.Designer.cs | 919 ---------------------- WinForms/Properties/Strings.es.resx | 348 -------- WinForms/Properties/Strings.it.resx | 348 -------- WinForms/Properties/Strings.resx | 380 --------- WinForms/UI/ColorSlider.cs | 485 ------------ WinForms/UI/FlexibleMessageBox.cs | 697 ---------------- WinForms/UI/ImageComboBox.cs | 62 -- WinForms/UI/MainForm.cs | 789 ------------------- WinForms/UI/PianoControl.cs | 183 ----- WinForms/UI/SongInfoControl.cs | 354 --------- WinForms/UI/Theme.cs | 165 ---- WinForms/UI/TrackViewer.cs | 113 --- WinForms/UI/ValueTextBox.cs | 104 --- WinForms/Util/HSLColor.cs | 161 ---- WinForms/WinForms.csproj | 57 -- 31 files changed, 5473 deletions(-) delete mode 100644 WinForms/Properties/Icon48.png delete mode 100644 WinForms/Properties/Icon528.png delete mode 100644 WinForms/Properties/Next.ico delete mode 100644 WinForms/Properties/Next.png delete mode 100644 WinForms/Properties/Pause.ico delete mode 100644 WinForms/Properties/Pause.png delete mode 100644 WinForms/Properties/Play.ico delete mode 100644 WinForms/Properties/Play.png delete mode 100644 WinForms/Properties/Playlist.png delete mode 100644 WinForms/Properties/Previous.ico delete mode 100644 WinForms/Properties/Previous.png delete mode 100644 WinForms/Properties/Resources.Designer.cs delete mode 100644 WinForms/Properties/Resources.resx delete mode 100644 WinForms/Properties/Settings.Designer.cs delete mode 100644 WinForms/Properties/Settings.settings delete mode 100644 WinForms/Properties/Song.png delete mode 100644 WinForms/Properties/Strings.Designer.cs delete mode 100644 WinForms/Properties/Strings.es.resx delete mode 100644 WinForms/Properties/Strings.it.resx delete mode 100644 WinForms/Properties/Strings.resx delete mode 100644 WinForms/UI/ColorSlider.cs delete mode 100644 WinForms/UI/FlexibleMessageBox.cs delete mode 100644 WinForms/UI/ImageComboBox.cs delete mode 100644 WinForms/UI/MainForm.cs delete mode 100644 WinForms/UI/PianoControl.cs delete mode 100644 WinForms/UI/SongInfoControl.cs delete mode 100644 WinForms/UI/Theme.cs delete mode 100644 WinForms/UI/TrackViewer.cs delete mode 100644 WinForms/UI/ValueTextBox.cs delete mode 100644 WinForms/Util/HSLColor.cs delete mode 100644 WinForms/WinForms.csproj diff --git a/WinForms/Properties/Icon48.png b/WinForms/Properties/Icon48.png deleted file mode 100644 index 4eb3e79cc24883b466fc38e31fe8dc910bbef28a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 870 zcmV-s1DX7ZP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y9E;jv63FrU-0@q1IK~!i%)s;(b)IbzPvz@=Ulgwz8Z$%O}M^(|9s26EyQAP zkiWmXA6kTn#oqka0g%4=d@}}U5h50QQUe?f5sN)Z0gi--#a>8WcmT$GLd0S(Y5)(R zF>|&AiN#)|0N??{+90vmgE{$2TnZA4J=hyS%!N2B^x&wxNFgSJUbG4THN@ngUZem= zLV1X@!&;L>KYjc#N^q~eHN1HM3A6|i4_$(Vl{!bL?+5GY`IH2)~CMqceHLx5ayq4Bxr6CuSm#i_4P?;-I^fGo84ru zxWhhmT+fSi=}UYmK;>nh zlarvHm){xy=6e5{L0-^zdEi1ya3yh(p1xio z`xGZ~Yq%)yAzX;-XaHc}rU8;X90}m^HNX*hm;^}Tq6UD6hldlj=cK3aftmzJ(&Fyj z-#^qOcZsE^PyL8Ii~*9gK%RSx04@(l^y>$x0H6@uB>*m%&^(a5gsHt+06<;=gbOBoE+paB z@(_!l1C!hwF3q|s%)O;N#G+6L2O-HhwI=b_!j)KbJcOiN6kJ{bdy9)$tmsJsa!4j& wr|!{{EjABN%T4^6cbP8a*t5dZ)H07*qoM6N<$g3Q5&p8x;= diff --git a/WinForms/Properties/Icon528.png b/WinForms/Properties/Icon528.png deleted file mode 100644 index 75601de2ef2e68898be2f817784b750d79ffbeb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10133 zcmeHN2~?BUwuTrn7?cnyiO3KTD3u{Z&;lYunFP^-Pz%ank|L;=F{qI;hztgSq@|#s zC>E+!#DStB4uCNbi%3j+6{^UPC?KK`1dKA>eNf-M>#qCOwSCXNdS6+KCHeE8v%meF zefDUk_iFb!vo&WcC@9SFSmClpK>E7}nA0jJ z1%(P8ew;p2L1Bi1x0jzQJpbq;+Q(LwL8dfM56|GB;C1Vmqj9^YzAvkF!NnMa1kQLM~q=1V75EB7iW=s)#vaq?paKe>C4yRiC) zQ1ShJKR=(Ey8m-Qox-BjnjqKoyxGr97f=xAti`Mz)ksFxhUE!d^h<F3?>As2OB!@wkJjxA)HyBWFJb%t zgqP(?mCeahSr(@KS$$g$I3}bjx|8)4@na<~?-~(0Hq@zQtf=GH9IH96Eclolou)@T zbB0=WIszk`qohUWVBht!=+z!taZc)-D2v-T=j@51T(-(j^N@y(IOnGLtUl{!i;#wS zY8eG18n4-l0YvoYb3`zp$|`RlTWiYkgLsmobB(oS&2Xs^;gX7(+m7Z|(J!R1hJ8tY#WMDN zTW>6yKV~x~X_EiM+wa)F_$>LM4bvekq-cJ~OCNPkP=-dSU08olf0m_mrNd@S#f@8x zyk8H(jCf{H%9r6groIB0(##hTU++xUC2>RKle@h@kx!eVvaqJ(7ox}c&mF!1DQrg4 zdV>vJ!gMLAW=aomG^jtj2gd7Qu2nlYp;`z?4;6H1DErG}y5#8t$=5*_nPB`pSTqkW zq$W%`gSOcpc2~J#G?D_Xle)%q7Xu%5wA$YSMMTW-|3y-YuZq6}@KZS+%U6;ap;V_7|g1R2=|poPxhrf)_b2@8zFtj|6=vj5Yrko7jCfMmuyO`b7Gc z+6@@lSN1~6DgH@~Qxm(C1^l5bvFNabvgD0I}?#I$<; zZWh?^GCw?Yz)|Z_P~330ozWq7BtY6O79V(4z?M`4q;^;LsAi@lvj-r>leDkAlLb}A znfdtGtL!HXkWLpjn0YcD?h3RBzEl9Z`=5=;%Jr|Nkg{9M0feSTL|ci$TJ!5OrRTB$ zV2100-*0*En&HSB$rD+95X(RB+9eyT$6|u^v`5fJk8vT6&od{hhImnK9qGNV!Q{OU zFZmE>)q6hB_dLa)DZN|hczB8Uj2T2d|4a*)A3wXWA#j;G?yQmKc~=ju4@}fDjH_pp zvbTg`Qtr51J8zts$}r>;41tH3C!qBH@Kz1O81Hfya>;{5fflC@Dr48F&$(A(Fn3Qr z&N(kI9p7-yGR1LA34J|8(h4G-lY1ii>V<7agz!~Xf62W{nGZ3{e4GrK1~2x+g)PXWw>A6yR-n%^9p!!1Q|w(WE+ZPB2gqs7a69JSEis5y z3!5~qYQ$o~A^|^eN#|H5=dBOLq*%gzNrmIH5EKFdi&N?f(SfYbb0JHbfTlvn&L(67 zW}f;gG2MXCixS-pCwoi7P%Xpy8E9feux7z|g=gjBoQqz7zUsK5YA}(rt{!Q6&Er0i z`O4TgF!+SGgE7?ZXdOq-6UF{Ql27u3n1-Inzz!XW*VC6BQI$=2QM`Wu5yPgp@pKL~ zkuv~Rn2TVB9S_DiCp>aaa8SqP{RLEAzH;OfvSg*MWDM;23&7bMP)6btLS7O&L|BX= z(p9wP3L!)O({6c^5yAKu;46M_U%G{0pS|EYSm=*P*-AfZnqJMYUQMww_P?O0ye*U{ zT-rAbQ0Am+OiQ}1sL(IZMYa7}AznA<#N~rjI0Ja*@!mgscqzqO$M?Pj^Lf|IirD6w z-mAOi;Scm9M|8NrvND(ne3R#@^0p-$ z8gMt4?4m|*ge=R>?yhV{jV_&ouc<491aN3DT-fs>DNWB@jtSccIo7pO?c$y>wTug$ z_@Y9{(_U{t{(Uy+9+*LAHpO?{a>(85uG;*l1&#om?-h$<{sGp;>YUxfHu2G`YwZQ@ zQgGAhPW6p^r$Ur#iBnlGNA&D9!J$)C7OmBo+lZCuC31C&em<=#6NBsjlI~scL$vpF zmNwj3l$6NfH9(NSr|!v5PhFhCUQt{+t7rJJbAlCk>|nBAxv5s(bbhzo`uh-FNoma0 z3#0tQjL@#bUKE|5dU~(gOuQuM=0Fr6Da|v%&k9AE;r|da9NVO-r0YdX$mBq6Xvo>8 z+lP+eoCB8;qoMdf+S}Rj3CjMhq?2O@6F%Y-TDYignO0SmXW@d;7424fXrFfo;b26|4)h2QJ zxL_ZB#7%B^@4$nVP(FhHMTeV;b4GQ%H*wAc&PzT|E13aGYe~jB+ zb5P2>1IOJ56|^4O9-{&1wXY;D6ADq8dK(f;XM`SW3gpX9QWD~X5x5TXY-#+v%MM|CvIpbatVIs%3NSBQN@yz(0bkiGtT1)k#sQ)A0U zuk23V#6xD`0A_)#J;wX&i{(Z0-)Q5`Cjc*kov*IKpI`nnLx*Ok<+Fgr0BXXfF5$7H zy;xH%h!NARE*Z1jv}nF|a!M+rA5y{}tCg|DGtmjqnbC&MjgSXkbz|K4%(;7@^*}a+ z!V0po5FgIaH=V8=w@QIDY zq`VRwfZq7KVAQgBUj>hiIgU$`C$z^(L{y7TYs-teCKc2&QQ?TjGz~HN1tXPOx;K;e z_N{O%7u1-+l|9p_pIJp1n< zCl7=}pfFS#Hx;@+D?VZiJ>80^8{iH=2sr(G^5APB(ER=53K`l$$)h&K-LUv( zRjlXQB5$N34Rf@+s3-Gi7}^^K^_p*-!oEjq(?I5l;F zRpy7SqUaE(;{v@-G_zpcvP%q|Hr1@bB=`okl1zMvPdMEn$jJ+ZJDFv)$LWa`L_;w*d=grqU>qveM>Q*h z->(;I15K1J*OwPRTqz!6DQQ{Sz=7B9mNM&Tw+lvWPUqzMXF3H=s|+ro45CNy0>cVo z>IG;IJ9Bv!0zXJ}E-&?D7%Ig;k9hj(9B*h9?TE+|ctSG{48q2qPw>sDD1LSq+A?Ip zE2`4;j%qR;dd~R}O`uB$;p>4M=j^ovqTH5JgS89-G^(dBRl#Nma4l{g~r1Kv&tSkI_C;1^CMQ2ss(-7#jUoQlY*!Mm?>6pbg?zpzEWNiK9 z^Nxyv<>)&4<^)jtvl8*Im0Ce&z(Jw`No&>MVDBUhBe-dLN?N`-XfW~~yyih^8dyGg zFC^iki^=og8s_+z8*~=+u_K_DYD9pE%VOdgi&0jY^c-|rA-Ji~8bRAu(Q$4I^kE|~ zDHa0bg^GBXq1XmPm-f6emQN_n#+rgv{jQrlX>ly*_!32!ePG;r2&NCM==2; zN`xW*Jw#5d$w3fN{3)+uGlpL*MOvbht!$TMihJ%AfQ~5E{0@uT7vPjR`RUYpN>U!y z1p`SyB^Mmt*lQdcz_QXSgf-B+lnHvD!C2Cw6I~!?2t>Ho(8pJv;4G+;3^xW~=2mygza_P-%WQRqi%&>4d{ zXT5r<2*#du;D7N<-mii)`7kPbG0ajt%1zH2DalF=C`UC*1HsD;>;W&=-VTyW1Hrfa z!9Jc&k*9{Lu}hjFT)!`Z^=n5qqhZ7lqYDkndM3@2|+xrA)54P zeL&|V0W8T1kMbgxcbeYIt%$OBx5yWc7i)U9yU4!wptK6+mSc=M8FQTM;1(7M(~~bk znrxG@b)~}IrkjAbKMOK`^!34NG)_sRJ(knp>r%^)KTfg!fr=7-_&Tn;8;*RmzFaCq zUpKe4!RgkZd>_6R7ao8K%`c=%F6=6&*|J!m-xx~w-$v+wl7#e9ZBCYl>j1PjMb)2` zyKIF)4-^s(lQu?#V_cy?of+CCbP!@Ow`B!vM!D&@yr;nNu&s{zr`h@Gx>H)}I6onb zfGU0%loo^Y>tlP_(bj4tjkBOBs|^DrC|P*PTxD#TlqM7S2gqe2T8iUa2w^)@lD=Ij z?lDtR`(}?Wn&Pl(KG+__ZgTh;)Mri>0w+4pL~zhU1<6N&J>j?|P-@p*ya^NYfHvg? zJn&wG!6n-9PfLc|9mhXmF$s7}@O4rRyNc& zgxXsZETehPvW~xM)@xL0PTl$M7yQF^3HpTO8{cVc)CRGwq**!pR{dLXXtg0mHc&WX lGuE)|t=`X?8W&Be*0#EKNN@JpqyH4;;kw%8)F&Hu{SDJKj*%fAP@>LHYg>P%3$KCAWm#zt5WPzD@qOt5CgGM0FMr!@9CbNdGp>xQBO2AI`q&8X zHUZQ>4O|K^Bs~5e;1WQh<5_k;@C2|5Kvd)7sow{D4`3hv5#TWrdpye$dw^YOUOJ|) zr1(9gl4c&?NdG75zl&i=Bk^mLKk{Sg(h)F1SpUT+y6)Ga=o&EessPk+AIqpbQ_eGV z9;PD@yAPibjJPhHXA=ATvziCMlIT5E3e3j7rNHzmWe&r>F9CLrQt`h*=HGzd2Bt}A zQk)H={{+4U9Aor0%QF89thdTE|F=jvPyQ0P10cn%9Ept`0LwfDtOaP3l|F3z47klI z)BH_Hd93h1$p0xY2cX#cS>`R^pMleWUVyR?cmcSX8-axNvksm+?gy3ueZVt-+E@A# zboBXrfPQ-!xB&>IQvJ4$4LIOMKsuZ9+`!mf0lWr$83?6X{kE=Z`L|Ii_g@`Wj_pp? zPakgpn(d!f#<$u0Z1wnR_59H4`KGZF zBCwBTSeD0x^>CXfsXkx@FuD#q)YoBfhl_il6CRj?Mh^jIWHdev`g4K4jQyed1Srk| zriME*wwFUc2yhL0Fc1p+^)JBVzy(01GEk4o`N;hO;F^=`&sxIUSX_f`1!h$!%=JIp zXPGyEy8xbBdw@#vJ5c@~!17N5e*jdf7wT9p#3#!R05fCF_z&AY%Ms54e+e88gksxg z*}cHMz$w5%Kq$Wt1rogdz*89we?rC+w=O|1BLeh?iFOowr%h9 z4?XzK@}a>i<_ry<);lyft7m9%O0;qi>z9h)K9*rw9K&N-2O8Lx)yT2%&=TNcVDvu4 z(SDx-?r?DrxCh(=?g95e5k0_rQI+oF)YDhq2jP8(Mul~J1UM5o9heK$lH*Z;-~BSp zy2n_nj(I4+`=2X-O80c2&LzB8_Xw~D;B`Ll_0soJ^$hTH z;CkSFz(j=gJO%JR)L!6YKrLB{0y_a7!|$tkUzG79uks4p<@eF7gZJY812`Y3b?+KQ z#tH5IZ-Dji+J7ZbOKksn9KrkHHv+!`?>nr!4+_SSb?~0&a-fz} z>OYU)5Bv~V1ysAAj@?nE{<99^r@&=ErF-X4kCTPi;=LXo_d39_TuZY0ul9M5koQ_o z1}fd}Wc}6p&wAbl9soEVtIc)CVE@km9>ej=u~|!M^^5yyvmx5$mlbjr5=G(2oxR@>|}1+j2Z-u#PVQe7=wSo&y#GwWQJh z+kP~f=RC#n!Fq;)qk&pdr2bp`e~OOx)#^Oc zkLp!BX}ju&`lWuV->Dt4t66?hJ$z1UGjKjuntD!a#iKn3{H`D6EB+qM_3jtvfOEh( z;2dxcI0u{q&H?9ubHF*^9B>Xe2b=@W0q1~oz&YR?a1J;JoCD4Q=YVs-Ip7>{4mby# z1I_{GfOEh(;2dxcI0u{q&H?9ubHF*^9B>Xe2b=@W0q1~oz&YR?a1J;JoCD4Q=YVs- zIp7>{4mby#1I_{GfOEh(;2dxcI0u{q&H?9ubHF*^9B>Xe2b=@W0q1~oz&YR?a1J;J zoCD4Q=YVs-Ip7>{4mby#1I~eNmBK7k*-Ym{1LkAa|n zlQn*{doqdUrVqmC%K*N+{ihcxdy+4;WZBMZXUP3Q~X&;EHp?`Y*1RBVy$8j(%&VG-XQ{Sa`P zu}-lxs;*X+%cGu!7%e)TEE)ZYUZyUeo(8slaq#NQF@|AXzuRI?z zPkbNv5FqKcbBxyPSHM8UUABj*#_}sDJF9?vRe$Z68$e%ARPd0NIeuAurQM7T$1C%^ zzPK7V7@%-{&@F2O@%0+mL|gZzDf1!3*B7zxkOK3S{kHQCZMp$C44{-Xe%1J@SlPMu zaoBoxY;p2Y=CLX>v>vz~3fgSvCiy_A_&SVoGYigF_1VVnDfIi(fQ0cA2c#=8#z~M7f24q|Ce(UpfEK)@pp!`Q{mvn_d#WR*K)f=s#b3FDXm>^72*O zWxN!#myqW@OVv@fu^PteW+?LVbvw#VD-mBS#cwBU_yX+!C|n~bma(FMAL?JN`BnBR zwR7!*uxpvItt?|zT~>c@gCZ|qpG4WICFEeZY;TGAS}A_~-huNLg}h)4izbcml{OQcYqjRxf&2x4gj`S? z(iJULTsER(dHMP<%J!6;uZkbncZ%OI@~Z(!(c&jA)w>>$tv0o5gzv!M<7G5b&0$7W zn5_7E7kZSJuMe^JUBXwz(8g~I%q!wO+h+2WcH8T*Ut;@8z}~Y}dr%if)TeKuN3_Al zO|ev}eFN-$m+_T0a-P-o^d97Sk8d_WDZ(00v1-Qrs+clWT$z6qDDpEH^1*%RXV`aZ zlikPpryKbydu{x9ZTVZYHZcg;2Jo@?8|SJ|ytb9(^bbMcZ+j8(QR`Y1C!fKP{Y`yhm>e&LW#U~?QZTvX*aC--E5g=*h z8bR@DH(zO|ool(x>+|=^Xr%2p$H;!R#ov;!A9?t?9_1!8U+E`8=Nsm~3oHR7t%#rE z)d{}JX7WQEc@_C~K7%3q`HU8ym1LjP7xmAknqO~6Ip=HQFU~vi*>2?ffkOZa*9h`) ztZ48{bhQ06z&_;R>y0Sqd>zl%O7Y`5|0wzjLSHw^Tg9s= zeAQSbXUJV{4+E=b3UPa@p{9w;9TVQarpTUs*2cz!i0k*4gqCVJkUs~o`q|VpGU)Dci-o|eLE<2Te zONG3xR}`Q{v1$yq2IMFzFvv4&ez&}l|RK>rZ)j|=|4!l z_iPofBJ)*TkO{~q=pBcqYldybL)hvC>=fZC9c!&H2|jBWb*HTGBNqiWkcSK9dTp8E&r z%T#K`Pw^@XU*!v~=XD+b3*`SC;JJm8cdW9k?B{oiym!d@tF2$f*L5i8d`%&)RnsU54ep1}}bx#r&ounzf5_1RQ>{ayOe`Px*z%Fi}_ zTj1uC>FZSZy*Z^D=2zLvl$;Z{sy%}tzMg>cx|Yhr*Z5wH^R*>>4deF~V)JES4nW}? z`(EG;KxJ+0%bd4kwbC;f;_FGUUHq=bSMB>0%KQA9@THafu<`p5Tz(Gy4&nC=d9IIJ z*|%2JO^#W+@51&`0EO!!a`wZ3+7~CaZBy+#8|9s^V?MJ!YIeR-{FcJ~{J#LUrI<9k z-A>nM*YkV^BfiE*O8E^y{i|lb_I;EG`Fdc~XYySuQyV{WRo5G>Y_kY;+qrfScAp6F zISg@8X|=wdf%4ARu`yoFSIypM!EcqrRT8YJGHy^R=yOfo|m9L~XKjZM8bYSI*14pWyRr=jPXHaq@jhT*Ozd8{+?7 z^y@93V>xXV6yhtdOOFG5el2dSiprsO_Dg)_a|TB{Ukf|0w38D>ZcBXSb6g9Yug+I+ zpbUKF{VzUm;p>57$E(O(YDeERzdB!?r|sZdqiu+IOAdGRQ{gHe-AU}|7v{I zKL0(;n7^ux>qhrkp8D?%{r85A@~KtFb6(Iju>YPL&VcB*vCRALx!W^e=Ha)UQ@0>C zCo@2)`2S@jj`07>=saKT^_2e~QNl~ksaEuV8UBAno#ShGJ+&SIIwB1c|I0-&qKexT zJ|D;XI-IX{O_68rE>anuN7VNn$X^6V$Wz%W9aH(j|L?6ce6{C(|3A%ykK&}|sq)&_ z2L+$Isx@BaU;jVN#`w*1zpkhJ|BB%Zi2q-4W1MQmxidWX--kGHEhY)qYtk##IkIf) z0XshZ|KJl|*80DXR`9Q$I(2U1G24OjL_(xl>orp(&X$d@;rR6TZA8(Xh$J~sRKD7n zk+U}e{C`0T*JE0f#flmqW#X$HpHILpf1gnj+#>VU#%uuH<^PmPYMuL4&ZcGHE61nS z=Da4l9+;gPQSbOn4;xKnzrQc8xocgeOZ?v|0T&>Ir34&rBTsEI=P?USK|i)`AXJYuREmzDi|j^f8aUcTPKTKxas#yyw%UASZk z{gO&6Vy1X?lCK<_8lSHs{{>)LYBT3k^)Xd7HBsICZsBntFJEtGFD5ZxIk#xtv;+Bz z=#NyIxn5J9N)@k8@HIU5^Sh4st7Ax8F+NT8aaQ)Ttw(^od|l6@Cm~~0<~N#<9(UV9m4uLBq>N;~Hk zwx@WtgRhD;IWYh%Wd~Ae*Z5R>N@YK{9{|`l+fQy=Xy0b&yPdCgZlMqE0T$8jR5}qe z#jBNk<@luSwD~z;Eg)(4_*A=<((gmbK8ll;r^;*Jw^>0K&#$tRc0UO;WrKH&X{ynNll3cHN2HfFSs@6nSKC1#3Mv-oP)=KK$i zD*=8tPvLy3_F_drKGdggp+9-~I>5en3197cZ5U>)rfsPdX}!iSDOSzks~w*_x5n@M zF?Off!X;Xl+=ZTzulmlF`4Bo+K13@@&ew3g*6Q!R>9R(p8GI#AX**-P1GoT?6m5K} zUp5tAH$s`0uMeYaPl@?z=awJ9mNNiJQO^D9k4+olYdAh{M#YDxm6HSFfo-pddBx`r zbS^JnA4A#R67toq*LK2&c7FHWjMz_k2Xol*Ras?b^W_cru0FJF0&XKIP~ z8qO{EqW{O5sK^IKRmg0;e6{2AC0NAoI#dU3R=$wNW^_ui;`t#jU$>*|w36`E&Mlq% z?mNT%b*aU#TGw0Q`1~eXS!h~P%#ErvS^e&t-wo&G>oX`jz2JPcG24g!cKW;TW)LC| zU+wthGk0g3hRe)TnIXRN-gqs(k^?L=49qMrU+q1P0ra(q_fpZVx}_Rl85^Ge8J{kOK0R@lqd){M|RYIU&j7tDRH%z50ht z?cFe^8gnA?^$IA-Q9Cz+VS};>|8~I zQz1`PUb=2bwH~+}#xUM_`1(4^%`OOEd7ocAXDUT^IJWH*2BF+Z)D zWx8d2rY4O`#MjS5n}@HQ%jm1Q1>x%^^o4UP``@nL5uvG$i192w(Xe@d<#W+tzEci>dfJ9r;fJTFZ6oxt}(N zBEIrovd%BHQeJ-^SrER;p2-w5+03+?$EUSZd98Im2*qWnqz2b=@W0q1~oz&YR?a1J;J zoCD4Q=YVs-Ip7>{4mby#1I_{GfOEh(;2dxcI0u{q&H?9ubHF*^9B>Xe2b=@W0q1~o zz&YR?a1J;JoCD4Q=YVs-Ip7>{4mby#1I_{GfOEh(;2dxcI0u{q&H?9ubHF*^9B>Xe z2b=@W0q1~oz&YR?a1J;JoCD4Q=YVs-Ip7>{4mby#1I_{GfOEh(;2dxcI0u{q&H?9u zb6}Ej0Okhr8eXbYNnD?J#r@!Zu;XJweAtlGu)lw-rnsIL((QpH-51RniS>6T+q=&R z^w*{OrGft1{#db_uSAE8)^pW{czgG>{x}`euTAtl1A+dCL?0y$kCmU2>XU}Y^u5cu zV|Vnfc>kC_`bnZcsGq6PFHZDH!$u!BygN!7Hu|_>=uZsli5rGKZg^j`Ao1H(QQR=} zal=;6hM|uew)**rK5p3Rr=@x}yx-K{vplXRZum{3XTzvJZg`v?4O8E0{oFv`vplXJ?}Hv+ zK6)(C_x1<+rHMY;5a?&7`gMVRTB@HL=zC70fAAveKp$;Q^u2#!{Rv%KKDsr~&q&Hg zYXZIAEO=<3Pm4#hGW2-&^eEp{Hc4GyL_dUa%!+#!PqKKG!NV-xX7N00Tx5--jB%GW zPP4{!);y3kZ)D6fJ#Xxm)_E%HyvAlx{{iR0pCtOYS##dZI?raEm$S~}Ygzrs?xa~$ z|As^#t`n~9PxRrs;<{KpvO8Rd+?ndbb;}Eheq<7!AY PydL+1rG@ihln?(8zw=kQ diff --git a/WinForms/Properties/Next.png b/WinForms/Properties/Next.png deleted file mode 100644 index dc839b50f18c9d65802cc276b8eaa1c7eeabce56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4026 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%0D$qRH; zyXtyZUEit5-_fJ&^J>Zk+;?M@Ps28yCFwT*4$p8uFn$-gRj>RKK}M5_qTQppzwt`b z9N0fuD%6E11`d4!NrL7hyD!T#fu^dJ^oKGfnFLdH(4~cTpQv7b+197fUcTZq*S13w z{uG@;u^0{`sMJFl!}NjZ&}TWsoe6RF0kWn|CFsC^`M}UPlCjMLT|@Av73bi9Piv5! Mwd1L(lYUqK2WetO7XSbN diff --git a/WinForms/Properties/Pause.ico b/WinForms/Properties/Pause.ico deleted file mode 100644 index 30b143620803667ca703b20759d56c50743d8b20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118062 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 diff --git a/WinForms/Properties/Pause.png b/WinForms/Properties/Pause.png deleted file mode 100644 index 9a736deda0138a1282a6d9c36f6f0b9190b9839c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1450 zcmeAS@N?(olHy`uVBq!ia0vp^b3mAb4M=wFIOGeY7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`0h7I;J!GcfQS24TkI`72U@f)XXJ5hcO-X(i=}MX3x0iJ5sNdU>fO3MP7n zdWOkbbG#WCSfxE(978JN-d=KCC!8+ens{PDYRg`OQ(ZHZZ!n*laW%EgMIk8em;3dg zov&YL=GmXJwNS0yq!E&PXHK}kzyB}$e?MMVmMmtQ&Lk4)YMxb~=`b;LsBu@ya`jwT zyY4}dwTSq_=-7ix%QW79Qg*j(7?&a-#1@!_X4 zQ}5;4<=H*?95>(Wy?MS_cHHyN&jVso%%`WXNne+~{{4dY`>sAUUtiwv{>7Ps%CwpK zv%j`a|5mp7+4h=$GF{zoo_*^1>ho(|^;)=!^Rwk==l|Su{CiE^-9P&xKTkTi7HHD- z_q&cGn`3YOn*aZk&vO5teSUv-Jy)LX?z-!?diyK)GS|4Lx1BaVEivcd*%P!P?!NdW z?^^%8r{|z7@4HX$m+qHtI4#ItUvcaK&pS8lbe`L>2Yqbc6L#2a?l~xV2dH(35WRZ; z1HY7?KOu4F;sd?^aldvm?vN?`(4Nk7?m=D&b~-JE=UqktKh2}z?1`Fte{R&Aer~(( z`RCWq!dbV^i{1ZPZu6oEZlKI zx$WM?nGb`k1;iKO%OP8x3k7#w6^-EvND#fd;^0yS*RYGdQb2CX5aX83i~GyIsW5h$ Uqzzvruqb8lboFyt=akR{092E?-2eap diff --git a/WinForms/Properties/Play.ico b/WinForms/Properties/Play.ico deleted file mode 100644 index e8be3d8757c7a4f9b418a3650f1cd6c69278d3db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140062 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 F;r}DLY(@Y8 diff --git a/WinForms/Properties/Play.png b/WinForms/Properties/Play.png deleted file mode 100644 index f386fd325480bec804b628fe9fb3016a349159db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2298 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 diff --git a/WinForms/Properties/Previous.ico b/WinForms/Properties/Previous.ico deleted file mode 100644 index fe2ef73e77e4f7b2ae6c6c6f1701a2b5ffdaa4bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140062 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 diff --git a/WinForms/Properties/Previous.png b/WinForms/Properties/Previous.png deleted file mode 100644 index 63496267cc5cabedf50440f1a3b095a96bef2eb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3708 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

-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 y2S{H_$3dd9qdC6QL6S$6aNP{V$@my diff --git a/WinForms/Properties/Resources.Designer.cs b/WinForms/Properties/Resources.Designer.cs deleted file mode 100644 index 1aa1551a..00000000 --- a/WinForms/Properties/Resources.Designer.cs +++ /dev/null @@ -1,133 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Kermalis.VGMusicStudio.Properties { - using System; - - - ///

- /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon Icon { - get { - object obj = ResourceManager.GetObject("Icon", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon IconNext { - get { - object obj = ResourceManager.GetObject("IconNext", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon IconPause { - get { - object obj = ResourceManager.GetObject("IconPause", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon IconPlay { - get { - object obj = ResourceManager.GetObject("IconPlay", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap IconPlaylist { - get { - object obj = ResourceManager.GetObject("IconPlaylist", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon IconPrevious { - get { - object obj = ResourceManager.GetObject("IconPrevious", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap IconSong { - get { - object obj = ResourceManager.GetObject("IconSong", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - } -} diff --git a/WinForms/Properties/Resources.resx b/WinForms/Properties/Resources.resx deleted file mode 100644 index ad871cdc..00000000 --- a/WinForms/Properties/Resources.resx +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - ..\Properties\Icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - Next.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - Pause.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - Play.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Properties\Playlist.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - Previous.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Properties\Song.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - \ No newline at end of file diff --git a/WinForms/Properties/Settings.Designer.cs b/WinForms/Properties/Settings.Designer.cs deleted file mode 100644 index 875ce0f2..00000000 --- a/WinForms/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Kermalis.VGMusicStudio.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/WinForms/Properties/Settings.settings b/WinForms/Properties/Settings.settings deleted file mode 100644 index 39645652..00000000 --- a/WinForms/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/WinForms/Properties/Song.png b/WinForms/Properties/Song.png deleted file mode 100644 index 62a132e5d49752cc1cccbe330aeb2d0148cbbbe5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2328 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fo{p?&#~tz_78O`%fY(0|PTd zfKP}kP<#}OhQNpofz1YAIzSnvB*-uL|HuXm!Qa_cfeC@Lz$3Dlfr0N32s4Umcr^e8 zB}-f*N`mv#O3D+9QW**oGxJLH@={9_O!N%&j0`_2WKIPZFtwg8jv*CsZzo2UNgML8 z)c*Ijo)pUWc+W*0mSvt&vG%eDopq-6pMMm=5Gls3a5{-GB~qtBr<-*`>PCi5I${pf zjxwFtM3!V<(vSKBCs^(FJ^U`f#`pY}dWf4rp4s;O3(Oi47w`Ndy{alr&>)XLh&SDQ z#p#U<*y)*W2kJlE{Ch_00r$SWQ&+OuJp6roM%)4Bd3O$TG3AABXlsxbU8D5CjP*6g zj?BxgEO!S!pGKNV$N@@~(PhBHgA zi%i(Dm32av#S-aTM?elJ|KdE~;565Xs5?v^rZIvIZh_ZCCv4dXQliANHYtK(Wt(); z1??7)fFj3PC6EAj(*>PFx0r;sZ3h~_-m);(el<78`+T5{*Yb13&VRW2PCftBY#-Hx z)pZ>F(pUVgX80ap=34$xGN8sObv`F+OxS_d`Wkx|Y&$)LX&3K@-_omgpV3@!nPLAw zr^@M`tTCn^YR=pLYmD0s41!&ayct_hZ($PRxsWlxlC_2R!n1}>RuQl2$xr&hXKx+F2TK_3_8pUAIw$eq^lBZY2LX)nvFVbGyV-s$ zX)k5j6MkU+?+DKW+nD}cIUU6$_u(gZmf$qu3B90j+3gtHM?Lyy!43W&mzlYEUlqNR zJQFy3&(BAp*NP7(=-ohPwmj#pOOgVT*ped}#h^^R(r|P7q#2Hq{47KA- zC+aK|xb8k_Vc%RYaqA>72Z&$mceb|_bNF`XSK$kFb&DBqg%>Q8;a0Ft{LPY~0o34k z!;Vqv6i}t`HvfjUBAtdeEhtLT#TdU_PG(Hee9j7m!dhW5<%;T#*D5wrKJTRFl(EI}sMiX -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Kermalis.VGMusicStudio.Properties -{ - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings - { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() - { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if (object.ReferenceEquals(resourceMan, null)) - { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kermalis.VGMusicStudio.Properties.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { - return resourceCulture; - } - set - { - resourceCulture = value; - } - } - - /// - /// Informs whenever an audio output device was removed. - /// - internal static string AudioDeviceRemoved - { - get - { - return ResourceManager.GetString("AudioDeviceRemoved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} key. - /// - internal static string ConfigKeySubkey - { - get - { - return ResourceManager.GetString("ConfigKeySubkey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Would you like to stop playing the current playlist?. - /// - internal static string EndPlaylistBody - { - get - { - return ResourceManager.GetString("EndPlaylistBody", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid command in track {0} at 0x{1:X}: 0x{2:X}. - /// - internal static string ErrorAlphaDreamDSEMP2KSDATInvalidCommand - { - get - { - return ResourceManager.GetString("ErrorAlphaDreamDSEMP2KSDATInvalidCommand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot copy invalid game code "{0}". - /// - internal static string ErrorAlphaDreamMP2KCopyInvalidGameCode - { - get - { - return ResourceManager.GetString("ErrorAlphaDreamMP2KCopyInvalidGameCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Game code "{0}" is missing.. - /// - internal static string ErrorAlphaDreamMP2KMissingGameCode - { - get - { - return ResourceManager.GetString("ErrorAlphaDreamMP2KMissingGameCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error parsing game code "{0}" in "{1}"{2}. - /// - internal static string ErrorAlphaDreamMP2KParseGameCode - { - get - { - return ResourceManager.GetString("ErrorAlphaDreamMP2KParseGameCode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Playlist "{0}" has song {1} defined more than once between decimal and hexadecimal.. - /// - internal static string ErrorAlphaDreamMP2KSongRepeated - { - get - { - return ResourceManager.GetString("ErrorAlphaDreamMP2KSongRepeated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" count must be the same as "{1}" count.. - /// - internal static string ErrorAlphaDreamMP2KSongTableCounts - { - get - { - return ResourceManager.GetString("ErrorAlphaDreamMP2KSongTableCounts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" must be True or False.. - /// - internal static string ErrorBoolParse - { - get - { - return ResourceManager.GetString("ErrorBoolParse", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Color {0} has an invalid key.. - /// - internal static string ErrorConfigColorInvalidKey - { - get - { - return ResourceManager.GetString("ErrorConfigColorInvalidKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Color {0} is not defined.. - /// - internal static string ErrorConfigColorMissing - { - get - { - return ResourceManager.GetString("ErrorConfigColorMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Color {0} is defined more than once between decimal and hexadecimal.. - /// - internal static string ErrorConfigColorRepeated - { - get - { - return ResourceManager.GetString("ErrorConfigColorRepeated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" is invalid.. - /// - internal static string ErrorConfigKeyInvalid - { - get - { - return ResourceManager.GetString("ErrorConfigKeyInvalid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" is missing.. - /// - internal static string ErrorConfigKeyMissing - { - get - { - return ResourceManager.GetString("ErrorConfigKeyMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" must have at least one entry.. - /// - internal static string ErrorConfigKeyNoEntries - { - get - { - return ResourceManager.GetString("ErrorConfigKeyNoEntries", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unknown header version: 0x{0:X}. - /// - internal static string ErrorDSEInvalidHeaderVersion - { - get - { - return ResourceManager.GetString("ErrorDSEInvalidHeaderVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid key in track {0} at 0x{1:X}: {2}. - /// - internal static string ErrorDSEInvalidKey - { - get - { - return ResourceManager.GetString("ErrorDSEInvalidKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are no "bgm(NNNN).smd" files.. - /// - internal static string ErrorDSENoSequences - { - get - { - return ResourceManager.GetString("ErrorDSENoSequences", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading Global Config. - /// - internal static string ErrorGlobalConfig - { - get - { - return ResourceManager.GetString("ErrorGlobalConfig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading Song {0}. - /// - internal static string ErrorLoadSong - { - get - { - return ResourceManager.GetString("ErrorLoadSong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invalid running status command in track {0} at 0x{1:X}: 0x{2:X}. - /// - internal static string ErrorMP2KInvalidRunningStatusCommand - { - get - { - return ResourceManager.GetString("ErrorMP2KInvalidRunningStatusCommand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Too many nested call events in track {0}. - /// - internal static string ErrorMP2KSDATNestedCalls - { - get - { - return ResourceManager.GetString("ErrorMP2KSDATNestedCalls", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading GBA ROM (AlphaDream). - /// - internal static string ErrorOpenAlphaDream - { - get - { - return ResourceManager.GetString("ErrorOpenAlphaDream", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading DSE Folder. - /// - internal static string ErrorOpenDSE - { - get - { - return ResourceManager.GetString("ErrorOpenDSE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading GBA ROM (MP2K). - /// - internal static string ErrorOpenMP2K - { - get - { - return ResourceManager.GetString("ErrorOpenMP2K", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Loading SDAT File. - /// - internal static string ErrorOpenSDAT - { - get - { - return ResourceManager.GetString("ErrorOpenSDAT", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error parsing "{0}"{1}. - /// - internal static string ErrorParseConfig - { - get - { - return ResourceManager.GetString("ErrorParseConfig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Exporting DLS. - /// - internal static string ErrorSaveDLS - { - get - { - return ResourceManager.GetString("ErrorSaveDLS", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Exporting MIDI. - /// - internal static string ErrorSaveMIDI - { - get - { - return ResourceManager.GetString("ErrorSaveMIDI", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Exporting SF2. - /// - internal static string ErrorSaveSF2 - { - get - { - return ResourceManager.GetString("ErrorSaveSF2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Exporting WAV. - /// - internal static string ErrorSaveWAV - { - get - { - return ResourceManager.GetString("ErrorSaveWAV", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This SDAT archive has no sequences.. - /// - internal static string ErrorSDATNoSequences - { - get - { - return ResourceManager.GetString("ErrorSDATNoSequences", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" is not an integer value.. - /// - internal static string ErrorValueParse - { - get - { - return ResourceManager.GetString("ErrorValueParse", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "{0}" must be between {1} and {2}.. - /// - internal static string ErrorValueParseRanged - { - get - { - return ResourceManager.GetString("ErrorValueParseRanged", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to GBA Files. - /// - internal static string FilterOpenGBA - { - get - { - return ResourceManager.GetString("FilterOpenGBA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SDAT Files. - /// - internal static string FilterOpenSDAT - { - get - { - return ResourceManager.GetString("FilterOpenSDAT", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DLS Files. - /// - internal static string FilterSaveDLS - { - get - { - return ResourceManager.GetString("FilterSaveDLS", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MIDI Files. - /// - internal static string FilterSaveMIDI - { - get - { - return ResourceManager.GetString("FilterSaveMIDI", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SF2 Files. - /// - internal static string FilterSaveSF2 - { - get - { - return ResourceManager.GetString("FilterSaveSF2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WAV Files. - /// - internal static string FilterSaveWAV - { - get - { - return ResourceManager.GetString("FilterSaveWAV", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Data. - /// - internal static string MenuData - { - get - { - return ResourceManager.GetString("MenuData", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to End Current Playlist. - /// - internal static string MenuEndPlaylist - { - get - { - return ResourceManager.GetString("MenuEndPlaylist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to File. - /// - internal static string MenuFile - { - get - { - return ResourceManager.GetString("MenuFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open GBA ROM (AlphaDream). - /// - internal static string MenuOpenAlphaDream - { - get - { - return ResourceManager.GetString("MenuOpenAlphaDream", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open DSE Folder. - /// - internal static string MenuOpenDSE - { - get - { - return ResourceManager.GetString("MenuOpenDSE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open GBA ROM (MP2K). - /// - internal static string MenuOpenMP2K - { - get - { - return ResourceManager.GetString("MenuOpenMP2K", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open SDAT File. - /// - internal static string MenuOpenSDAT - { - get - { - return ResourceManager.GetString("MenuOpenSDAT", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Playlist. - /// - internal static string MenuPlaylist - { - get - { - return ResourceManager.GetString("MenuPlaylist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export VoiceTable as DLS. - /// - internal static string MenuSaveDLS - { - get - { - return ResourceManager.GetString("MenuSaveDLS", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export Song as MIDI. - /// - internal static string MenuSaveMIDI - { - get - { - return ResourceManager.GetString("MenuSaveMIDI", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export VoiceTable as SF2. - /// - internal static string MenuSaveSF2 - { - get - { - return ResourceManager.GetString("MenuSaveSF2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Export Song as WAV. - /// - internal static string MenuSaveWAV - { - get - { - return ResourceManager.GetString("MenuSaveWAV", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to C;C#;D;D#;E;F;F#;G;G#;A;A#;B. - /// - internal static string Notes - { - get - { - return ResourceManager.GetString("Notes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Next Song. - /// - internal static string PlayerNextSong - { - get - { - return ResourceManager.GetString("PlayerNextSong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Notes. - /// - internal static string PlayerNotes - { - get - { - return ResourceManager.GetString("PlayerNotes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Pause. - /// - internal static string PlayerPause - { - get - { - return ResourceManager.GetString("PlayerPause", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Play. - /// - internal static string PlayerPlay - { - get - { - return ResourceManager.GetString("PlayerPlay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Position. - /// - internal static string PlayerPosition - { - get - { - return ResourceManager.GetString("PlayerPosition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Previous Song. - /// - internal static string PlayerPreviousSong - { - get - { - return ResourceManager.GetString("PlayerPreviousSong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Rest. - /// - internal static string PlayerRest - { - get - { - return ResourceManager.GetString("PlayerRest", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Stop. - /// - internal static string PlayerStop - { - get - { - return ResourceManager.GetString("PlayerStop", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tempo. - /// - internal static string PlayerTempo - { - get - { - return ResourceManager.GetString("PlayerTempo", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Type. - /// - internal static string PlayerType - { - get - { - return ResourceManager.GetString("PlayerType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unpause. - /// - internal static string PlayerUnpause - { - get - { - return ResourceManager.GetString("PlayerUnpause", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Music. - /// - internal static string PlaylistMusic - { - get - { - return ResourceManager.GetString("PlaylistMusic", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Would you like to play the following playlist?{0}. - /// - internal static string PlayPlaylistBody - { - get - { - return ResourceManager.GetString("PlayPlaylistBody", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to VoiceTable saved to {0}.. - /// - internal static string SuccessSaveDLS - { - get - { - return ResourceManager.GetString("SuccessSaveDLS", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MIDI saved to {0}.. - /// - internal static string SuccessSaveMIDI - { - get - { - return ResourceManager.GetString("SuccessSaveMIDI", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to VoiceTable saved to {0}.. - /// - internal static string SuccessSaveSF2 - { - get - { - return ResourceManager.GetString("SuccessSaveSF2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WAV saved to {0}.. - /// - internal static string SuccessSaveWAV - { - get - { - return ResourceManager.GetString("SuccessSaveWAV", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Arguments. - /// - internal static string TrackViewerArguments - { - get - { - return ResourceManager.GetString("TrackViewerArguments", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Event. - /// - internal static string TrackViewerEvent - { - get - { - return ResourceManager.GetString("TrackViewerEvent", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Offset. - /// - internal static string TrackViewerOffset - { - get - { - return ResourceManager.GetString("TrackViewerOffset", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ticks. - /// - internal static string TrackViewerTicks - { - get - { - return ResourceManager.GetString("TrackViewerTicks", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Track Viewer. - /// - internal static string TrackViewerTitle - { - get - { - return ResourceManager.GetString("TrackViewerTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Track {0}. - /// - internal static string TrackViewerTrackX - { - get - { - return ResourceManager.GetString("TrackViewerTrackX", resourceCulture); - } - } - } -} diff --git a/WinForms/Properties/Strings.es.resx b/WinForms/Properties/Strings.es.resx deleted file mode 100644 index d98bb266..00000000 --- a/WinForms/Properties/Strings.es.resx +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Quisiera detener la Lista de Reproducción actual? - - - Error al Cargar Canción {0} - - - Error al Abrir Carpeta DSE - - - Error al Abrir GBA ROM (AlphaDream) - - - Error al Abrir GBA ROM (MP2K) - - - Error al Abrir Archivo SDAT - - - Error al Exportar MIDI - - - Archivo GBA - - - Archivo SDAT - - - Archivos MIDI - - - Datos - - - Detener Lista de Reproducción Actual - - - Archivo - - - Abrir Carpeta DSE - - - Abrir GBA ROM (AlphaDream) - - - Abrir GBA ROM (MP2K) - - - Abrir Archivo SDAT - - - Playlist - - - Exportar Canción como MIDI - - - Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si - - - Siguiente Canción - - - Notas - - - Pausar - - - Reproducir - - - Posición - - - Canción Anterior - - - Retraso - - - Detener - - - Tempo - - - Tipo - - - Resumir - - - Música - - - Quisiera reproducir la siguiente Lista de Reproducción? {0} - - - MIDI guardado en {0}. - - - Argumento - - - Evento - - - Offset - - - Ticks - - - Visor de Eventos - - - Pista {0} - - - {0} clave - - - "{0}" debe ser Verdadero o Falso. - - - El color {0} tiene una clave inválida. - - - El color {0} no está definido. - - - El color {0} está definido más de una vez entre decimal y hexadecimal. - - - "{0}" es inválido. - - - "{0}" no se encuentra. - - - "{0}" debe tener al menos una entrada. - - - Versión del encabezado desconocida: 0x{0:X} - - - Clave inválida en la pista {0} en 0x{1:X}: {2} - - - Comando inválido en la pista {0} en 0x{1:X}: 0x{2:X} - - - No hay ningún archivo "bgm(NNNN).smd". - - - Error al Cargar Configuración Global - - - No se puede copiar, el código del juego "{0}" es inválido. - - - No se encuentra el código del juego "{0}". - - - Error analizando el código del juego "{0}" en "{1}"{2} - - - La Lista de Reproducción "{0}" tiene a la canción {1} definida más de una vez entre decimal y hexadecimal. - - - El número de "{0}" debe ser igual al de "{1}". - - - Comando de estado en ejecución inválido en la pista {0} en 0x{1:X}: 0x{2:X} - - - Demasiadas llamadas a eventos anidadas en la pista {0} - - - Error analizando "{0}"{1} - - - Este archivo SDAT no tiene secuencias. - - - "{0}" no es un valor entero. - - - "{0}" debe estar entre {1} y {2}. - - - Error al Exportar WAV - - - Archivos WAV - - - Exportar Canción como WAV - - - WAV guardado en {0}. - - - Error al Exportar SF2 - - - Archivos SF2 - - - Exportar VoiceTable como SF2 - - - SF2 guardado en {0}. - - - Error al Exportar DLS - - - Archivos DLS - - - Exportar VoiceTable como DLS - - - DLS guardado en {0}. - - \ No newline at end of file diff --git a/WinForms/Properties/Strings.it.resx b/WinForms/Properties/Strings.it.resx deleted file mode 100644 index 935f17e8..00000000 --- a/WinForms/Properties/Strings.it.resx +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Errore durante il caricamento del Brano {0} - - - Errore durante il caricamento della ROM GBA (AlphaDream) - - - Errore Durante L'Esportazione in MIDI - - - File GBA - - - File MIDI - - - Dati - - - File - - - Apri ROM GBA (MP2K) - - - Esporta Brano in MIDI - - - Do;Do#;Re;Re#;Mi;Fa;Fa#;Sol;Sol#;La;La#;Si - - - Pausa - - - Note - - - Pausa - - - Play - - - Posizione - - - Stop - - - Tempo - - - Tipo - - - Riprendi - - - MIDI salvato in {0}. - - - Desideri riprodurre la seguente playlist?{0} - - - Brano Sucessivo - - - Brano Precedente - - - Desideri interrompere la riproduzione della playlist attuale? - - - Errore durante il caricamento della Cartella DSE - - - Errore durante il caricamento della ROM GBA (MP2K) - - - Errore durante il caricamento del File SDAT - - - File SDAT - - - Termina la Playlist Attuale - - - Apri Cartella DSE - - - Apri ROM GBA (AlphaDream) - - - Apri File SDAT - - - Playlist - - - Musica - - - Argomenti - - - Evento - - - Posizione - - - Tick - - - Visualizzatore Traccia - - - Traccia {0} - - - La Chiave {0} - - - "{0}" deve essere Vero o Falso. - - - Il colore {0} non ha una chiave valida. - - - Il colore {0} non è definito. - - - Il colore {0} è definito più di una volta tra decimale ed esadecimale. - - - "{0}" non è valido. - - - "{0}" è mancante. - - - "{0}" deve possedere almeno una voce. - - - Versione dell'header sconosciuta: 0x{0:X} - - - Tasto del pianoforte non valido nella traccia {0} a 0x{1:X}: {2} - - - Comando non valido nella traccia {0} a 0x{1:X}: 0x{2:X} - - - Non sono presenti file "bgm(NNNN).smd". - - - Errore nel Caricamento della Configurazione Globale - - - Non è possibile copiare il Codice Gioco invalido "{0}" - - - Il Codice Gioco "{0}" non è presente. - - - Errore nell'analisi del Codice Gioco "{0}" in "{1}"{2} - - - La Playlist "{0}" presenta il brano {1} più volte definito tra decimale e esadecimale. - - - Il numero di "{0}" deve essere uguale a quello di "{1}". - - - Comando di stato in esecuzione non valido nella traccia {0} a 0x{1:X}: 0x{2:X} - - - Troppi eventi di chiamata nidificati nella traccia {0} - - - Errore nell'analisi di "{0}"{1} - - - Questo archivio SDAT non ha sequenze. - - - "{0}" non è un valore intero. - - - "{0}" deve essere compreso tra {1} e {2}. - - - Errore Durante L'Esportazione in WAV - - - File WAV - - - Esporta Brano in WAV - - - WAV salvato in {0}. - - - Errore Durante L'Esportazione in SF2 - - - File SF2 - - - Esporta VoiceTable In SF2 - - - VoiceTable salvata in {0}. - - - Errore Durante L'Esportazione in DLS - - - File DLS - - - Esporta VoiceTable In DLS - - - VoiceTable salvata in {0}. - - \ No newline at end of file diff --git a/WinForms/Properties/Strings.resx b/WinForms/Properties/Strings.resx deleted file mode 100644 index 228bc168..00000000 --- a/WinForms/Properties/Strings.resx +++ /dev/null @@ -1,380 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Error Loading Song {0} - {0} is the song number. - - - Error Loading GBA ROM (AlphaDream) - - - Error Exporting MIDI - - - GBA Files - - - MIDI Files - - - Data - - - File - - - Open GBA ROM (MP2K) - - - Export Song as MIDI - - - C;C#;D;D#;E;F;F#;G;G#;A;A#;B - - - Rest - - - Notes - - - Pause - - - Play - - - Position - - - Stop - - - Tempo - - - Type - - - Unpause - - - MIDI saved to {0}. - {0} is the file name. - - - Would you like to play the following playlist?{0} - {0} is a newline character followed by the playlist name. - - - Next Song - - - Previous Song - - - Would you like to stop playing the current playlist? - - - Error Loading DSE Folder - - - Error Loading GBA ROM (MP2K) - - - Error Loading SDAT File - - - SDAT Files - - - End Current Playlist - - - Open DSE Folder - - - Open GBA ROM (AlphaDream) - - - Open SDAT File - - - Playlist - - - Music - - - Arguments - - - Event - - - Offset - - - Ticks - - - Track Viewer - - - Track {0} - {0} is the track number. - - - {0} key - {0} is the parent key name. - - - "{0}" must be True or False. - {0} is the value name. - - - Color {0} has an invalid key. - {0} is the color number. - - - Color {0} is not defined. - {0} is the color number. - - - Color {0} is defined more than once between decimal and hexadecimal. - {0} is the color number. - - - "{0}" is invalid. - {0} is the invalid key. - - - "{0}" is missing. - {0} is the missing key. - - - "{0}" must have at least one entry. - {0} is the key. - - - Unknown header version: 0x{0:X} - {0} is the header version. - - - Invalid key in track {0} at 0x{1:X}: {2} - {0} is the track number, {1} is the offset, {2} is the key. - - - Invalid command in track {0} at 0x{1:X}: 0x{2:X} - {0} is the track number, {1} is the offset, {2} is the command. - - - There are no "bgm(NNNN).smd" files. - - - Error Loading Global Config - - - Cannot copy invalid game code "{0}" - {0} is the invalid game code. - - - Game code "{0}" is missing. - {0} is the game code. - - - Error parsing game code "{0}" in "{1}"{2} - {0} is the game code, {1} is the config filename, {2} is a newline character followed by the error message. - - - Playlist "{0}" has song {1} defined more than once between decimal and hexadecimal. - {0} is the playlist name, {1} is the song index. - - - "{0}" count must be the same as "{1}" count. - {0} is key 1, {1} is key 2. - - - Invalid running status command in track {0} at 0x{1:X}: 0x{2:X} - {0} is the track number, {1} is the offset, {2} is the command. - - - Too many nested call events in track {0} - {0} is the track number. - - - Error parsing "{0}"{1} - {0} is the config filename, {1} is a newline character followed by the error message. - - - This SDAT archive has no sequences. - - - "{0}" is not an integer value. - {0} is the value name. - - - "{0}" must be between {1} and {2}. - {0} is the value name, {1} is the minimum allowed value, {2} is the maximum allowed value. - - - Error Exporting WAV - - - WAV Files - - - Export Song as WAV - - - WAV saved to {0}. - {0} is the file name. - - - Error Exporting SF2 - - - SF2 Files - - - Export VoiceTable as SF2 - - - VoiceTable saved to {0}. - {0} is the file name. - - - Error Exporting DLS - - - DLS Files - - - Export VoiceTable as DLS - - - VoiceTable saved to {0}. - {0} is the file name. - - - An audio output device was {0}. {1} was disconnected. - {0} is the status of the device. {1} is the device itself - - \ No newline at end of file diff --git a/WinForms/UI/ColorSlider.cs b/WinForms/UI/ColorSlider.cs deleted file mode 100644 index b23df9ce..00000000 --- a/WinForms/UI/ColorSlider.cs +++ /dev/null @@ -1,485 +0,0 @@ -#region License - -/* Copyright (c) 2017 Fabrice Lacharme - * This code is inspired from Michal Brylka - * https://www.codeproject.com/Articles/17395/Owner-drawn-trackbar-slider - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#endregion - - -using System; -using System.ComponentModel; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory(""), ToolboxBitmap(typeof(TrackBar))] - internal class ColorSlider : Control - { - private const int thumbSize = 14; - private Rectangle thumbRect; - - private long _value = 0L; - public long Value - { - get => _value; - set - { - if (value >= _minimum && value <= _maximum) - { - _value = value; - ValueChanged?.Invoke(this, new EventArgs()); - Invalidate(); - } - else - { - throw new ArgumentOutOfRangeException(nameof(Value), $"{nameof(Value)} must be between {nameof(Minimum)} and {nameof(Maximum)}."); - } - } - } - private long _minimum = 0L; - public long Minimum - { - get => _minimum; - set - { - if (value <= _maximum) - { - _minimum = value; - if (_value < _minimum) - { - _value = _minimum; - ValueChanged?.Invoke(this, new EventArgs()); - } - Invalidate(); - } - else - { - throw new ArgumentOutOfRangeException(nameof(Minimum), $"{nameof(Minimum)} cannot be higher than {nameof(Maximum)}."); - } - } - } - private long _maximum = 10L; - public long Maximum - { - get => _maximum; - set - { - if (value >= _minimum) - { - _maximum = value; - if (_value > _maximum) - { - _value = _maximum; - ValueChanged?.Invoke(this, new EventArgs()); - } - Invalidate(); - } - else - { - throw new ArgumentOutOfRangeException(nameof(Maximum), $"{nameof(Maximum)} cannot be lower than {nameof(Minimum)}."); - } - } - } - private long _smallChange = 1L; - public long SmallChange - { - get => _smallChange; - set - { - if (value >= 0) - { - _smallChange = value; - } - else - { - throw new ArgumentOutOfRangeException(nameof(SmallChange), $"{nameof(SmallChange)} must be greater than or equal to 0."); - } - } - } - private long _largeChange = 5L; - public long LargeChange - { - get => _largeChange; - set - { - if (value >= 0) - { - _largeChange = value; - } - else - { - throw new ArgumentOutOfRangeException(nameof(LargeChange), $"{nameof(LargeChange)} must be greater than or equal to 0."); - } - } - } - private bool _acceptKeys = true; - public bool AcceptKeys - { - get => _acceptKeys; - set - { - _acceptKeys = value; - SetStyle(ControlStyles.Selectable, value); - } - } - - public event EventHandler ValueChanged; - - private readonly Color _thumbOuterColor = Color.White; - private readonly Color _thumbInnerColor = Color.White; - private readonly Color _thumbPenColor = Color.FromArgb(125, 125, 125); - private readonly Color _barInnerColor = Theme.BackColorMouseOver; - private readonly Color _elapsedPenColorTop = Theme.ForeColor; - private readonly Color _elapsedPenColorBottom = Theme.ForeColor; - private readonly Color _barPenColorTop = Color.FromArgb(85, 90, 104); - private readonly Color _barPenColorBottom = Color.FromArgb(117, 124, 140); - private readonly Color _elapsedInnerColor = Theme.BorderColor; - private readonly Color _tickColor = Color.White; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - pen.Dispose(); - } - base.Dispose(disposing); - } - public ColorSlider() - { - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | - ControlStyles.ResizeRedraw | ControlStyles.Selectable | - ControlStyles.SupportsTransparentBackColor | ControlStyles.UserMouse | - ControlStyles.UserPaint, true); - Size = new Size(200, 48); - } - - protected override void OnPaint(PaintEventArgs e) - { - if (!Enabled) - { - Color[] c = DesaturateColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, - _barInnerColor, - _elapsedPenColorTop, _elapsedPenColorBottom, - _barPenColorTop, _barPenColorBottom, - _elapsedInnerColor); - Draw(e, - c[0], c[1], c[2], - c[3], - c[4], c[5], - c[6], c[7], - c[8]); - } - else - { - if (mouseInRegion) - { - Color[] c = LightenColors(_thumbOuterColor, _thumbInnerColor, _thumbPenColor, - _barInnerColor, - _elapsedPenColorTop, _elapsedPenColorBottom, - _barPenColorTop, _barPenColorBottom, - _elapsedInnerColor); - Draw(e, - c[0], c[1], c[2], - c[3], - c[4], c[5], - c[6], c[7], - c[8]); - } - else - { - Draw(e, - _thumbOuterColor, _thumbInnerColor, _thumbPenColor, - _barInnerColor, - _elapsedPenColorTop, _elapsedPenColorBottom, - _barPenColorTop, _barPenColorBottom, - _elapsedInnerColor); - } - } - } - private readonly Pen pen = new Pen(Color.Transparent); - private void Draw(PaintEventArgs e, - Color thumbOuterColorPaint, Color thumbInnerColorPaint, Color thumbPenColorPaint, - Color barInnerColorPaint, - Color elapsedTopPenColorPaint, Color elapsedBottomPenColorPaint, - Color barTopPenColorPaint, Color barBottomPenColorPaint, - Color elapsedInnerColorPaint) - { - if (Focused) - { - ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Color.FromArgb(50, elapsedTopPenColorPaint), ButtonBorderStyle.Dashed); - } - - long a = _maximum - _minimum; - long x = a == 0 ? 0 : (_value - _minimum) * (ClientRectangle.Width - thumbSize) / a; - thumbRect = new Rectangle((int)x, ClientRectangle.Y + (ClientRectangle.Height / 2) - (thumbSize / 2), thumbSize, thumbSize); - Rectangle barRect = ClientRectangle; - barRect.Inflate(-1, -barRect.Height / 3); - Rectangle elapsedRect = barRect; - elapsedRect.Width = thumbRect.Left + (thumbSize / 2); - - pen.Color = barInnerColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + (barRect.Height / 2)); - pen.Color = elapsedInnerColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + (barRect.Height / 2)); - pen.Color = elapsedTopPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2)); - pen.Color = elapsedBottomPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y + 1 + (barRect.Height / 2), barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2)); - pen.Color = barTopPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2)); - pen.Color = barBottomPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X + elapsedRect.Width, barRect.Y + 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); - pen.Color = barTopPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X, barRect.Y - 1 + (barRect.Height / 2), barRect.X, barRect.Y + (barRect.Height / 2) + 1); - pen.Color = barBottomPenColorPaint; - e.Graphics.DrawLine(pen, barRect.X + barRect.Width, barRect.Y - 1 + (barRect.Height / 2), barRect.X + barRect.Width, barRect.Y + 1 + (barRect.Height / 2)); - - e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; - Color newthumbOuterColorPaint = thumbOuterColorPaint, - newthumbInnerColorPaint = thumbInnerColorPaint; - if (busyMouse) - { - newthumbOuterColorPaint = Color.FromArgb(175, thumbOuterColorPaint); - newthumbInnerColorPaint = Color.FromArgb(175, thumbInnerColorPaint); - } - using (GraphicsPath thumbPath = CreateRoundRectPath(thumbRect, thumbSize)) - { - using (var lgbThumb = new LinearGradientBrush(thumbRect, newthumbOuterColorPaint, newthumbInnerColorPaint, LinearGradientMode.Vertical) { WrapMode = WrapMode.TileFlipXY }) - { - e.Graphics.FillPath(lgbThumb, thumbPath); - } - Color newThumbPenColor = thumbPenColorPaint; - if (busyMouse || mouseInThumbRegion) - { - newThumbPenColor = ControlPaint.Dark(newThumbPenColor); - } - pen.Color = newThumbPenColor; - e.Graphics.DrawPath(pen, thumbPath); - } - - const int numTicks = 1 + (10 * (5 + 1)); - int interval = 0; - int start = thumbRect.Width / 2; - int w = barRect.Width - thumbRect.Width; - int idx = 0; - pen.Color = _tickColor; - for (int i = 0; i <= 10; i++) - { - e.Graphics.DrawLine(pen, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height, start + barRect.X + interval, ClientRectangle.Y + ClientRectangle.Height - 5); - if (i < 10) - { - for (int j = 0; j <= 5; j++) - { - idx++; - interval = idx * w / (numTicks - 1); - } - } - } - } - - private bool mouseInRegion = false; - private bool mouseInThumbRegion = false; - private bool busyMouse = false; - private void SetValueFromPoint(Point p) - { - int x = p.X; - int margin = thumbSize / 2; - x -= margin; - _value = (long)((x * ((_maximum - _minimum) / (ClientSize.Width - (2f * margin)))) + _minimum); - if (_value < _minimum) - { - _value = _minimum; - } - else if (_value > _maximum) - { - _value = _maximum; - } - ValueChanged?.Invoke(this, new EventArgs()); - } - protected override void OnEnabledChanged(EventArgs e) - { - base.OnEnabledChanged(e); - Invalidate(); - } - protected override void OnMouseEnter(EventArgs e) - { - base.OnMouseEnter(e); - mouseInRegion = true; - Invalidate(); - } - protected override void OnMouseLeave(EventArgs e) - { - base.OnMouseLeave(e); - mouseInRegion = false; - mouseInThumbRegion = false; - Invalidate(); - } - protected override void OnMouseDown(MouseEventArgs e) - { - base.OnMouseDown(e); - mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); - busyMouse = (MouseButtons & MouseButtons.Left) != MouseButtons.None; - if (busyMouse) - { - SetValueFromPoint(e.Location); - } - Invalidate(); - } - protected override void OnMouseMove(MouseEventArgs e) - { - base.OnMouseMove(e); - mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); - if (busyMouse) - { - SetValueFromPoint(e.Location); - } - Invalidate(); - } - protected override void OnMouseUp(MouseEventArgs e) - { - base.OnMouseUp(e); - mouseInThumbRegion = IsPointInRect(e.Location, thumbRect); - bool old = busyMouse; - busyMouse = old && e.Button == MouseButtons.Left ? false : old; - Invalidate(); - } - protected override void OnGotFocus(EventArgs e) - { - base.OnGotFocus(e); - Invalidate(); - } - protected override void OnLostFocus(EventArgs e) - { - base.OnLostFocus(e); - Invalidate(); - } - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (_acceptKeys && !busyMouse) - { - switch (e.KeyCode) - { - case Keys.Down: - case Keys.Left: - { - long newVal = _value - _smallChange; - if (newVal < _minimum) - { - newVal = _minimum; - } - Value = newVal; - break; - } - case Keys.Up: - case Keys.Right: - { - long newVal = _value + _smallChange; - if (newVal > _maximum) - { - newVal = _maximum; - } - Value = newVal; - break; - } - case Keys.Home: - { - Value = _minimum; - break; - } - case Keys.End: - { - Value = _maximum; - break; - } - case Keys.PageDown: - { - long newVal = _value - _largeChange; - if (newVal < _minimum) - { - newVal = _minimum; - } - Value = newVal; - break; - } - case Keys.PageUp: - { - long newVal = _value + _largeChange; - if (newVal > _maximum) - { - newVal = _maximum; - } - Value = newVal; - break; - } - } - } - } - protected override bool ProcessDialogKey(Keys keyData) - { - return !_acceptKeys || keyData == Keys.Tab || ModifierKeys == Keys.Shift ? base.ProcessDialogKey(keyData) : false; - } - - private static GraphicsPath CreateRoundRectPath(Rectangle rect, int size) - { - var gp = new GraphicsPath(); - gp.AddLine(rect.Left + (size / 2), rect.Top, rect.Right - (size / 2), rect.Top); - gp.AddArc(rect.Right - size, rect.Top, size, size, 270, 90); - - gp.AddLine(rect.Right, rect.Top + (size / 2), rect.Right, rect.Bottom - (size / 2)); - gp.AddArc(rect.Right - size, rect.Bottom - size, size, size, 0, 90); - - gp.AddLine(rect.Right - (size / 2), rect.Bottom, rect.Left + (size / 2), rect.Bottom); - gp.AddArc(rect.Left, rect.Bottom - size, size, size, 90, 90); - - gp.AddLine(rect.Left, rect.Bottom - (size / 2), rect.Left, rect.Top + (size / 2)); - gp.AddArc(rect.Left, rect.Top, size, size, 180, 90); - return gp; - } - private static Color[] DesaturateColors(params Color[] colors) - { - var ret = new Color[colors.Length]; - for (int i = 0; i < colors.Length; i++) - { - int gray = (int)((colors[i].R * 0.3) + (colors[i].G * 0.6) + (colors[i].B * 0.1)); - ret[i] = Color.FromArgb((-0x010101 * (255 - gray)) - 1); - } - return ret; - } - private static Color[] LightenColors(params Color[] colors) - { - var ret = new Color[colors.Length]; - for (int i = 0; i < colors.Length; i++) - { - ret[i] = ControlPaint.Light(colors[i]); - } - return ret; - } - private static bool IsPointInRect(Point p, Rectangle rect) - { - return p.X > rect.Left & p.X < rect.Right & p.Y > rect.Top & p.Y < rect.Bottom; - } - } -} diff --git a/WinForms/UI/FlexibleMessageBox.cs b/WinForms/UI/FlexibleMessageBox.cs deleted file mode 100644 index 30d29ad1..00000000 --- a/WinForms/UI/FlexibleMessageBox.cs +++ /dev/null @@ -1,697 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.Drawing; -using System.Globalization; -using System.Linq; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - /* FlexibleMessageBox – A flexible replacement for the .NET MessageBox - * - * Author: Jörg Reichert (public@jreichert.de) - * Contributors: Thanks to: David Hall, Roink - * Version: 1.3 - * Published at: http://www.codeproject.com/Articles/601900/FlexibleMessageBox - * - ************************************************************************************************************ - * Features: - * - It can be simply used instead of MessageBox since all important static "Show"-Functions are supported - * - It is small, only one source file, which could be added easily to each solution - * - It can be resized and the content is correctly word-wrapped - * - It tries to auto-size the width to show the longest text row - * - It never exceeds the current desktop working area - * - It displays a vertical scrollbar when needed - * - It does support hyperlinks in text - * - * Because the interface is identical to MessageBox, you can add this single source file to your project - * and use the FlexibleMessageBox almost everywhere you use a standard MessageBox. - * The goal was NOT to produce as many features as possible but to provide a simple replacement to fit my - * own needs. Feel free to add additional features on your own, but please left my credits in this class. - * - ************************************************************************************************************ - * Usage examples: - * - * FlexibleMessageBox.Show("Just a text"); - * - * FlexibleMessageBox.Show("A text", - * "A caption"); - * - * FlexibleMessageBox.Show("Some text with a link: www.google.com", - * "Some caption", - * MessageBoxButtons.AbortRetryIgnore, - * MessageBoxIcon.Information, - * MessageBoxDefaultButton.Button2); - * - * var dialogResult = FlexibleMessageBox.Show("Do you know the answer to life the universe and everything?", - * "One short question", - * MessageBoxButtons.YesNo); - * - ************************************************************************************************************ - * THE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS", WITHOUT WARRANTY - * OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHOR BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THIS - * SOFTWARE. - * - ************************************************************************************************************ - * History: - * Version 1.3 - 19.Dezember 2014 - * - Added refactoring function GetButtonText() - * - Used CurrentUICulture instead of InstalledUICulture - * - Added more button localizations. Supported languages are now: ENGLISH, GERMAN, SPANISH, ITALIAN - * - Added standard MessageBox handling for "copy to clipboard" with + and + - * - Tab handling is now corrected (only tabbing over the visible buttons) - * - Added standard MessageBox handling for ALT-Keyboard shortcuts - * - SetDialogSizes: Refactored completely: Corrected sizing and added caption driven sizing - * - * Version 1.2 - 10.August 2013 - * - Do not ShowInTaskbar anymore (original MessageBox is also hidden in taskbar) - * - Added handling for Escape-Button - * - Adapted top right close button (red X) to behave like MessageBox (but hidden instead of deactivated) - * - * Version 1.1 - 14.June 2013 - * - Some Refactoring - * - Added internal form class - * - Added missing code comments, etc. - * - * Version 1.0 - 15.April 2013 - * - Initial Version - */ - - internal class FlexibleMessageBox - { - #region Public statics - - /// - /// Defines the maximum width for all FlexibleMessageBox instances in percent of the working area. - /// - /// Allowed values are 0.2 - 1.0 where: - /// 0.2 means: The FlexibleMessageBox can be at most half as wide as the working area. - /// 1.0 means: The FlexibleMessageBox can be as wide as the working area. - /// - /// Default is: 70% of the working area width. - /// - public static double MAX_WIDTH_FACTOR = 0.7; - - /// - /// Defines the maximum height for all FlexibleMessageBox instances in percent of the working area. - /// - /// Allowed values are 0.2 - 1.0 where: - /// 0.2 means: The FlexibleMessageBox can be at most half as high as the working area. - /// 1.0 means: The FlexibleMessageBox can be as high as the working area. - /// - /// Default is: 90% of the working area height. - /// - public static double MAX_HEIGHT_FACTOR = 0.9; - - /// - /// Defines the font for all FlexibleMessageBox instances. - /// - /// Default is: Theme.Font - /// - public static Font FONT = Theme.Font; - - #endregion - - #region Public show functions - - public static DialogResult Show(string text) - { - return FlexibleMessageBoxForm.Show(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text) - { - return FlexibleMessageBoxForm.Show(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption) - { - return FlexibleMessageBoxForm.Show(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(Exception ex, string caption) - { - return FlexibleMessageBoxForm.Show(null, string.Format("Error Details:{1}{1}{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace), caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text, string caption) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption, MessageBoxButtons buttons) - { - return FlexibleMessageBoxForm.Show(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) - { - return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1); - } - public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) - { - return FlexibleMessageBoxForm.Show(null, text, caption, buttons, icon, defaultButton); - } - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) - { - return FlexibleMessageBoxForm.Show(owner, text, caption, buttons, icon, defaultButton); - } - - #endregion - - #region Internal form class - - class FlexibleMessageBoxForm : ThemedForm - { - IContainer components = null; - - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - void InitializeComponent() - { - components = new Container(); - button1 = new ThemedButton(); - richTextBoxMessage = new ThemedRichTextBox(); - FlexibleMessageBoxFormBindingSource = new BindingSource(components); - panel1 = new ThemedPanel(); - pictureBoxForIcon = new PictureBox(); - button2 = new ThemedButton(); - button3 = new ThemedButton(); - ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).BeginInit(); - panel1.SuspendLayout(); - ((ISupportInitialize)(pictureBoxForIcon)).BeginInit(); - SuspendLayout(); - // - // button1 - // - button1.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; - button1.AutoSize = true; - button1.DialogResult = DialogResult.OK; - button1.Location = new Point(11, 67); - button1.MinimumSize = new Size(0, 24); - button1.Name = "button1"; - button1.Size = new Size(75, 24); - button1.TabIndex = 2; - button1.Text = "OK"; - button1.UseVisualStyleBackColor = true; - button1.Visible = false; - // - // richTextBoxMessage - // - richTextBoxMessage.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) - | AnchorStyles.Left) - | AnchorStyles.Right); - richTextBoxMessage.BorderStyle = BorderStyle.None; - richTextBoxMessage.DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "MessageText", true, DataSourceUpdateMode.OnPropertyChanged)); - richTextBoxMessage.Font = new Font(Theme.Font.FontFamily, 9); - richTextBoxMessage.Location = new Point(50, 26); - richTextBoxMessage.Margin = new Padding(0); - richTextBoxMessage.Name = "richTextBoxMessage"; - richTextBoxMessage.ReadOnly = true; - richTextBoxMessage.ScrollBars = RichTextBoxScrollBars.Vertical; - richTextBoxMessage.Size = new Size(200, 20); - richTextBoxMessage.TabIndex = 0; - richTextBoxMessage.TabStop = false; - richTextBoxMessage.Text = ""; - richTextBoxMessage.LinkClicked += new LinkClickedEventHandler(LinkClicked); - // - // panel1 - // - panel1.Anchor = (((AnchorStyles.Top | AnchorStyles.Bottom) - | AnchorStyles.Left) - | AnchorStyles.Right); - panel1.Controls.Add(pictureBoxForIcon); - panel1.Controls.Add(richTextBoxMessage); - panel1.Location = new Point(-3, -4); - panel1.Name = "panel1"; - panel1.Size = new Size(268, 59); - panel1.TabIndex = 1; - // - // pictureBoxForIcon - // - pictureBoxForIcon.BackColor = Color.Transparent; - pictureBoxForIcon.Location = new Point(15, 19); - pictureBoxForIcon.Name = "pictureBoxForIcon"; - pictureBoxForIcon.Size = new Size(32, 32); - pictureBoxForIcon.TabIndex = 8; - pictureBoxForIcon.TabStop = false; - // - // button2 - // - button2.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); - button2.DialogResult = DialogResult.OK; - button2.Location = new Point(92, 67); - button2.MinimumSize = new Size(0, 24); - button2.Name = "button2"; - button2.Size = new Size(75, 24); - button2.TabIndex = 3; - button2.Text = "OK"; - button2.UseVisualStyleBackColor = true; - button2.Visible = false; - // - // button3 - // - button3.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right); - button3.AutoSize = true; - button3.DialogResult = DialogResult.OK; - button3.Location = new Point(173, 67); - button3.MinimumSize = new Size(0, 24); - button3.Name = "button3"; - button3.Size = new Size(75, 24); - button3.TabIndex = 0; - button3.Text = "OK"; - button3.UseVisualStyleBackColor = true; - button3.Visible = false; - // - // FlexibleMessageBoxForm - // - AutoScaleDimensions = new SizeF(6F, 13F); - AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(260, 102); - Controls.Add(button3); - Controls.Add(button2); - Controls.Add(panel1); - Controls.Add(button1); - DataBindings.Add(new Binding("Text", FlexibleMessageBoxFormBindingSource, "CaptionText", true)); - Icon = Properties.Resources.Icon; - MaximizeBox = false; - MinimizeBox = false; - MinimumSize = new Size(276, 140); - Name = "FlexibleMessageBoxForm"; - SizeGripStyle = SizeGripStyle.Show; - StartPosition = FormStartPosition.CenterParent; - Text = ""; - Shown += new EventHandler(FlexibleMessageBoxForm_Shown); - ((ISupportInitialize)(FlexibleMessageBoxFormBindingSource)).EndInit(); - panel1.ResumeLayout(false); - ((ISupportInitialize)(pictureBoxForIcon)).EndInit(); - ResumeLayout(false); - PerformLayout(); - } - - ThemedButton button1, button2, button3; - private BindingSource FlexibleMessageBoxFormBindingSource; - ThemedRichTextBox richTextBoxMessage; - ThemedPanel panel1; - private PictureBox pictureBoxForIcon; - - #region Private constants - - //These separators are used for the "copy to clipboard" standard operation, triggered by Ctrl + C (behavior and clipboard format is like in a standard MessageBox) - static readonly String STANDARD_MESSAGEBOX_SEPARATOR_LINES = "---------------------------\n"; - static readonly String STANDARD_MESSAGEBOX_SEPARATOR_SPACES = " "; - - //These are the possible buttons (in a standard MessageBox) - private enum ButtonID { OK = 0, CANCEL, YES, NO, ABORT, RETRY, IGNORE }; - - //These are the buttons texts for different languages. - //If you want to add a new language, add it here and in the GetButtonText-Function - private enum TwoLetterISOLanguageID { en, de, es, it }; - static readonly String[] BUTTON_TEXTS_ENGLISH_EN = { "OK", "Cancel", "&Yes", "&No", "&Abort", "&Retry", "&Ignore" }; //Note: This is also the fallback language - static readonly String[] BUTTON_TEXTS_GERMAN_DE = { "OK", "Abbrechen", "&Ja", "&Nein", "&Abbrechen", "&Wiederholen", "&Ignorieren" }; - static readonly String[] BUTTON_TEXTS_SPANISH_ES = { "Aceptar", "Cancelar", "&Sí", "&No", "&Abortar", "&Reintentar", "&Ignorar" }; - static readonly String[] BUTTON_TEXTS_ITALIAN_IT = { "OK", "Annulla", "&Sì", "&No", "&Interrompi", "&Riprova", "&Ignora" }; - - #endregion - - #region Private members - - MessageBoxDefaultButton defaultButton; - int visibleButtonsCount; - readonly TwoLetterISOLanguageID languageID = TwoLetterISOLanguageID.en; - - #endregion - - #region Private constructor - - private FlexibleMessageBoxForm() - { - InitializeComponent(); - - //Try to evaluate the language. If this fails, the fallback language English will be used - Enum.TryParse(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, out languageID); - - KeyPreview = true; - KeyUp += FlexibleMessageBoxForm_KeyUp; - } - - #endregion - - #region Private helper functions - - static string[] GetStringRows(string message) - { - if (string.IsNullOrEmpty(message)) - { - return null; - } - - var messageRows = message.Split(new char[] { '\n' }, StringSplitOptions.None); - return messageRows; - } - - string GetButtonText(ButtonID buttonID) - { - var buttonTextArrayIndex = Convert.ToInt32(buttonID); - - switch (languageID) - { - case TwoLetterISOLanguageID.de: return BUTTON_TEXTS_GERMAN_DE[buttonTextArrayIndex]; - case TwoLetterISOLanguageID.es: return BUTTON_TEXTS_SPANISH_ES[buttonTextArrayIndex]; - case TwoLetterISOLanguageID.it: return BUTTON_TEXTS_ITALIAN_IT[buttonTextArrayIndex]; - - default: return BUTTON_TEXTS_ENGLISH_EN[buttonTextArrayIndex]; - } - } - - static double GetCorrectedWorkingAreaFactor(double workingAreaFactor) - { - const double MIN_FACTOR = 0.2; - const double MAX_FACTOR = 1.0; - - if (workingAreaFactor < MIN_FACTOR) - { - return MIN_FACTOR; - } - - if (workingAreaFactor > MAX_FACTOR) - { - return MAX_FACTOR; - } - - return workingAreaFactor; - } - - static void SetDialogStartPosition(FlexibleMessageBoxForm flexibleMessageBoxForm, IWin32Window owner) - { - //If no owner given: Center on current screen - if (owner == null) - { - var screen = Screen.FromPoint(Cursor.Position); - flexibleMessageBoxForm.StartPosition = FormStartPosition.Manual; - flexibleMessageBoxForm.Left = screen.Bounds.Left + screen.Bounds.Width / 2 - flexibleMessageBoxForm.Width / 2; - flexibleMessageBoxForm.Top = screen.Bounds.Top + screen.Bounds.Height / 2 - flexibleMessageBoxForm.Height / 2; - } - } - - static void SetDialogSizes(FlexibleMessageBoxForm flexibleMessageBoxForm, string text, string caption) - { - //First set the bounds for the maximum dialog size - flexibleMessageBoxForm.MaximumSize = new Size(Convert.ToInt32(SystemInformation.WorkingArea.Width * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_WIDTH_FACTOR)), - Convert.ToInt32(SystemInformation.WorkingArea.Height * FlexibleMessageBoxForm.GetCorrectedWorkingAreaFactor(MAX_HEIGHT_FACTOR))); - - //Get rows. Exit if there are no rows to render... - var stringRows = GetStringRows(text); - if (stringRows == null) - { - return; - } - - //Calculate whole text height - var textHeight = TextRenderer.MeasureText(text, FONT).Height; - - //Calculate width for longest text line - const int SCROLLBAR_WIDTH_OFFSET = 15; - var longestTextRowWidth = stringRows.Max(textForRow => TextRenderer.MeasureText(textForRow, FONT).Width); - var captionWidth = TextRenderer.MeasureText(caption, SystemFonts.CaptionFont).Width; - var textWidth = Math.Max(longestTextRowWidth + SCROLLBAR_WIDTH_OFFSET, captionWidth); - - //Calculate margins - var marginWidth = flexibleMessageBoxForm.Width - flexibleMessageBoxForm.richTextBoxMessage.Width; - var marginHeight = flexibleMessageBoxForm.Height - flexibleMessageBoxForm.richTextBoxMessage.Height; - - //Set calculated dialog size (if the calculated values exceed the maximums, they were cut by windows forms automatically) - flexibleMessageBoxForm.Size = new Size(textWidth + marginWidth, - textHeight + marginHeight); - } - - static void SetDialogIcon(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxIcon icon) - { - switch (icon) - { - case MessageBoxIcon.Information: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Information.ToBitmap(); - break; - case MessageBoxIcon.Warning: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Warning.ToBitmap(); - break; - case MessageBoxIcon.Error: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Error.ToBitmap(); - break; - case MessageBoxIcon.Question: - flexibleMessageBoxForm.pictureBoxForIcon.Image = SystemIcons.Question.ToBitmap(); - break; - default: - //When no icon is used: Correct placement and width of rich text box. - flexibleMessageBoxForm.pictureBoxForIcon.Visible = false; - flexibleMessageBoxForm.richTextBoxMessage.Left -= flexibleMessageBoxForm.pictureBoxForIcon.Width; - flexibleMessageBoxForm.richTextBoxMessage.Width += flexibleMessageBoxForm.pictureBoxForIcon.Width; - break; - } - } - - static void SetDialogButtons(FlexibleMessageBoxForm flexibleMessageBoxForm, MessageBoxButtons buttons, MessageBoxDefaultButton defaultButton) - { - //Set the buttons visibilities and texts - switch (buttons) - { - case MessageBoxButtons.AbortRetryIgnore: - flexibleMessageBoxForm.visibleButtonsCount = 3; - - flexibleMessageBoxForm.button1.Visible = true; - flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.ABORT); - flexibleMessageBoxForm.button1.DialogResult = DialogResult.Abort; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.IGNORE); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Ignore; - - flexibleMessageBoxForm.ControlBox = false; - break; - - case MessageBoxButtons.OKCancel: - flexibleMessageBoxForm.visibleButtonsCount = 2; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.OK; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - - case MessageBoxButtons.RetryCancel: - flexibleMessageBoxForm.visibleButtonsCount = 2; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.RETRY); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.Retry; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - - case MessageBoxButtons.YesNo: - flexibleMessageBoxForm.visibleButtonsCount = 2; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.Yes; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.No; - - flexibleMessageBoxForm.ControlBox = false; - break; - - case MessageBoxButtons.YesNoCancel: - flexibleMessageBoxForm.visibleButtonsCount = 3; - - flexibleMessageBoxForm.button1.Visible = true; - flexibleMessageBoxForm.button1.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.YES); - flexibleMessageBoxForm.button1.DialogResult = DialogResult.Yes; - - flexibleMessageBoxForm.button2.Visible = true; - flexibleMessageBoxForm.button2.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.NO); - flexibleMessageBoxForm.button2.DialogResult = DialogResult.No; - - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.CANCEL); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.Cancel; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - - case MessageBoxButtons.OK: - default: - flexibleMessageBoxForm.visibleButtonsCount = 1; - flexibleMessageBoxForm.button3.Visible = true; - flexibleMessageBoxForm.button3.Text = flexibleMessageBoxForm.GetButtonText(ButtonID.OK); - flexibleMessageBoxForm.button3.DialogResult = DialogResult.OK; - - flexibleMessageBoxForm.CancelButton = flexibleMessageBoxForm.button3; - break; - } - - //Set default button (used in FlexibleMessageBoxForm_Shown) - flexibleMessageBoxForm.defaultButton = defaultButton; - } - - #endregion - - #region Private event handlers - - void FlexibleMessageBoxForm_Shown(object sender, EventArgs e) - { - int buttonIndexToFocus = 1; - Button buttonToFocus; - - //Set the default button... - switch (defaultButton) - { - case MessageBoxDefaultButton.Button1: - default: - buttonIndexToFocus = 1; - break; - case MessageBoxDefaultButton.Button2: - buttonIndexToFocus = 2; - break; - case MessageBoxDefaultButton.Button3: - buttonIndexToFocus = 3; - break; - } - - if (buttonIndexToFocus > visibleButtonsCount) - { - buttonIndexToFocus = visibleButtonsCount; - } - - if (buttonIndexToFocus == 3) - { - buttonToFocus = button3; - } - else if (buttonIndexToFocus == 2) - { - buttonToFocus = button2; - } - else - { - buttonToFocus = button1; - } - - buttonToFocus.Focus(); - } - - void LinkClicked(object sender, LinkClickedEventArgs e) - { - try - { - Cursor.Current = Cursors.WaitCursor; - Process.Start(e.LinkText); - } - catch (Exception) - { - //Let the caller of FlexibleMessageBoxForm decide what to do with this exception... - throw; - } - finally - { - Cursor.Current = Cursors.Default; - } - } - - void FlexibleMessageBoxForm_KeyUp(object sender, KeyEventArgs e) - { - //Handle standard key strikes for clipboard copy: "Ctrl + C" and "Ctrl + Insert" - if (e.Control && (e.KeyCode == Keys.C || e.KeyCode == Keys.Insert)) - { - var buttonsTextLine = (button1.Visible ? button1.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) - + (button2.Visible ? button2.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty) - + (button3.Visible ? button3.Text + STANDARD_MESSAGEBOX_SEPARATOR_SPACES : string.Empty); - - //Build same clipboard text like the standard .Net MessageBox - var textForClipboard = STANDARD_MESSAGEBOX_SEPARATOR_LINES - + Text + Environment.NewLine - + STANDARD_MESSAGEBOX_SEPARATOR_LINES - + richTextBoxMessage.Text + Environment.NewLine - + STANDARD_MESSAGEBOX_SEPARATOR_LINES - + buttonsTextLine.Replace("&", string.Empty) + Environment.NewLine - + STANDARD_MESSAGEBOX_SEPARATOR_LINES; - - //Set text in clipboard - Clipboard.SetText(textForClipboard); - } - } - - #endregion - - #region Properties (only used for binding) - - public string CaptionText { get; set; } - public string MessageText { get; set; } - - #endregion - - #region Public show function - - public static DialogResult Show(IWin32Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton) - { - //Create a new instance of the FlexibleMessageBox form - var flexibleMessageBoxForm = new FlexibleMessageBoxForm - { - ShowInTaskbar = false, - - //Bind the caption and the message text - CaptionText = caption, - MessageText = text - }; - flexibleMessageBoxForm.FlexibleMessageBoxFormBindingSource.DataSource = flexibleMessageBoxForm; - - //Set the buttons visibilities and texts. Also set a default button. - SetDialogButtons(flexibleMessageBoxForm, buttons, defaultButton); - - //Set the dialogs icon. When no icon is used: Correct placement and width of rich text box. - SetDialogIcon(flexibleMessageBoxForm, icon); - - //Set the font for all controls - flexibleMessageBoxForm.Font = FONT; - flexibleMessageBoxForm.richTextBoxMessage.Font = FONT; - - //Calculate the dialogs start size (Try to auto-size width to show longest text row). Also set the maximum dialog size. - SetDialogSizes(flexibleMessageBoxForm, text, caption); - - //Set the dialogs start position when given. Otherwise center the dialog on the current screen. - SetDialogStartPosition(flexibleMessageBoxForm, owner); - - //Show the dialog - return flexibleMessageBoxForm.ShowDialog(owner); - } - - #endregion - } //class FlexibleMessageBoxForm - - #endregion - } -} diff --git a/WinForms/UI/ImageComboBox.cs b/WinForms/UI/ImageComboBox.cs deleted file mode 100644 index c928af92..00000000 --- a/WinForms/UI/ImageComboBox.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Drawing; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - internal class ImageComboBox : ComboBox - { - private const int _imgSize = 15; - private bool _open = false; - - public ImageComboBox() - { - DrawMode = DrawMode.OwnerDrawFixed; - DropDownStyle = ComboBoxStyle.DropDown; - } - - protected override void OnDrawItem(DrawItemEventArgs e) - { - e.DrawBackground(); - e.DrawFocusRectangle(); - - if (e.Index >= 0) - { - ImageComboBoxItem item = Items[e.Index] as ImageComboBoxItem ?? throw new InvalidCastException($"Item was not of type \"{nameof(ImageComboBoxItem)}\""); - int indent = _open ? item.IndentLevel : 0; - e.Graphics.DrawImage(item.Image, e.Bounds.Left + (indent * _imgSize), e.Bounds.Top, _imgSize, _imgSize); - e.Graphics.DrawString(item.ToString(), e.Font, new SolidBrush(e.ForeColor), e.Bounds.Left + (indent * _imgSize) + _imgSize, e.Bounds.Top); - } - - base.OnDrawItem(e); - } - protected override void OnDropDown(EventArgs e) - { - _open = true; - base.OnDropDown(e); - } - protected override void OnDropDownClosed(EventArgs e) - { - _open = false; - base.OnDropDownClosed(e); - } - } - internal class ImageComboBoxItem - { - public object Item { get; } - public Image Image { get; } - public int IndentLevel { get; } - - public ImageComboBoxItem(object item, Image image, int indentLevel) - { - Item = item; - Image = image; - IndentLevel = indentLevel; - } - - public override string ToString() - { - return Item.ToString(); - } - } -} diff --git a/WinForms/UI/MainForm.cs b/WinForms/UI/MainForm.cs deleted file mode 100644 index 06a4cd16..00000000 --- a/WinForms/UI/MainForm.cs +++ /dev/null @@ -1,789 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; -using System.Windows.Forms; -using System.Windows.Interop; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class MainForm : ThemedForm - { - private const int _intendedWidth = 675; - private const int _intendedHeight = 675 + 1 + 125 + 24; - - public static MainForm Instance { get; } = new MainForm(); - - public readonly bool[] PianoTracks = new bool[SongInfoControl.SongInfo.MaxTracks]; - - private bool _playlistPlaying; - private Config.Playlist _curPlaylist; - private long _curSong = -1; - private readonly List _playedSongs = new List(); - private readonly List _remainingSongs = new List(); - - private TrackViewer _trackViewer; - - #region Controls - - private readonly MenuStrip _mainMenu; - private readonly ToolStripMenuItem _fileItem, _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem, - _dataItem, _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem, - _playlistItem, _endPlaylistItem; - private readonly Timer _timer; - private readonly ThemedNumeric _songNumerical; - private readonly ThemedButton _playButton, _pauseButton, _stopButton; - private readonly SplitContainer _splitContainer; - private readonly PianoControl _piano; - private readonly ColorSlider _volumeBar, _positionBar; - private readonly SongInfoControl _songInfo; - private readonly ImageComboBox _songsComboBox; - //private readonly ThumbnailToolBarButton _prevTButton, _toggleTButton, _nextTButton; - - #endregion - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _timer.Dispose(); - } - base.Dispose(disposing); - } - private MainForm() - { - for (int i = 0; i < PianoTracks.Length; i++) - { - PianoTracks[i] = true; - } - - // File Menu - _openDSEItem = new ToolStripMenuItem { Text = Strings.MenuOpenDSE }; - _openDSEItem.Click += OpenDSE; - _openAlphaDreamItem = new ToolStripMenuItem { Text = Strings.MenuOpenAlphaDream }; - _openAlphaDreamItem.Click += OpenAlphaDream; - _openMP2KItem = new ToolStripMenuItem { Text = Strings.MenuOpenMP2K }; - _openMP2KItem.Click += OpenMP2K; - _openSDATItem = new ToolStripMenuItem { Text = Strings.MenuOpenSDAT }; - _openSDATItem.Click += OpenSDAT; - _fileItem = new ToolStripMenuItem { Text = Strings.MenuFile }; - _fileItem.DropDownItems.AddRange(new ToolStripItem[] { _openDSEItem, _openAlphaDreamItem, _openMP2KItem, _openSDATItem }); - - // Data Menu - _trackViewerItem = new ToolStripMenuItem { ShortcutKeys = Keys.Control | Keys.T, Text = Strings.TrackViewerTitle }; - _trackViewerItem.Click += OpenTrackViewer; - _exportDLSItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveDLS }; - _exportDLSItem.Click += ExportDLS; - _exportMIDIItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveMIDI }; - _exportMIDIItem.Click += ExportMIDI; - _exportSF2Item = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveSF2 }; - _exportSF2Item.Click += ExportSF2; - _exportWAVItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuSaveWAV }; - _exportWAVItem.Click += ExportWAV; - _dataItem = new ToolStripMenuItem { Text = Strings.MenuData }; - _dataItem.DropDownItems.AddRange(new ToolStripItem[] { _trackViewerItem, _exportDLSItem, _exportMIDIItem, _exportSF2Item, _exportWAVItem }); - - // Playlist Menu - _endPlaylistItem = new ToolStripMenuItem { Enabled = false, Text = Strings.MenuEndPlaylist }; - _endPlaylistItem.Click += EndCurrentPlaylist; - _playlistItem = new ToolStripMenuItem { Text = Strings.MenuPlaylist }; - _playlistItem.DropDownItems.AddRange(new ToolStripItem[] { _endPlaylistItem }); - - // Main Menu - _mainMenu = new MenuStrip { Size = new Size(_intendedWidth, 24) }; - _mainMenu.Items.AddRange(new ToolStripItem[] { _fileItem, _dataItem, _playlistItem }); - - // Buttons - _playButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumSpringGreen, Text = Strings.PlayerPlay }; - _playButton.Click += (o, e) => Play(); - _pauseButton = new ThemedButton { Enabled = false, ForeColor = Color.DeepSkyBlue, Text = Strings.PlayerPause }; - _pauseButton.Click += (o, e) => Pause(); - _stopButton = new ThemedButton { Enabled = false, ForeColor = Color.MediumVioletRed, Text = Strings.PlayerStop }; - _stopButton.Click += (o, e) => Stop(); - - // Numerical - _songNumerical = new ThemedNumeric { Enabled = false, Minimum = 0, Visible = false }; - _songNumerical.ValueChanged += SongNumerical_ValueChanged; - - // Timer - _timer = new Timer(); - _timer.Tick += UpdateUI; - - // Piano - _piano = new PianoControl(); - - // Volume bar - _volumeBar = new ColorSlider { Enabled = false, LargeChange = 20, Maximum = 100, SmallChange = 5 }; - _volumeBar.ValueChanged += VolumeBar_ValueChanged; - - // Position bar - _positionBar = new ColorSlider { AcceptKeys = false, Enabled = false, Maximum = 0 }; - _positionBar.MouseUp += PositionBar_MouseUp; - _positionBar.MouseDown += PositionBar_MouseDown; - - // Playlist box - _songsComboBox = new ImageComboBox { Enabled = false }; - _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; - - // Track info - _songInfo = new SongInfoControl { Dock = DockStyle.Fill }; - - // Split container - _splitContainer = new SplitContainer { BackColor = Theme.TitleBar, Dock = DockStyle.Fill, IsSplitterFixed = true, Orientation = Orientation.Horizontal, SplitterWidth = 1 }; - _splitContainer.Panel1.Controls.AddRange(new Control[] { _playButton, _pauseButton, _stopButton, _songNumerical, _songsComboBox, _piano, _volumeBar, _positionBar }); - _splitContainer.Panel2.Controls.Add(_songInfo); - - // MainForm - ClientSize = new Size(_intendedWidth, _intendedHeight); - Controls.AddRange(new Control[] { _splitContainer, _mainMenu }); - MainMenuStrip = _mainMenu; - MinimumSize = new Size(_intendedWidth + (Width - _intendedWidth), _intendedHeight + (Height - _intendedHeight)); // Borders - Resize += OnResize; - Text = Utils.ProgramName; - - OnResize(null, null); - } - - private void VolumeBar_ValueChanged(object sender, EventArgs e) - { - Engine.Instance.Mixer.SetVolume(_volumeBar.Value / (float)_volumeBar.Maximum); - } - public void SetVolumeBarValue(float volume) - { - _volumeBar.ValueChanged -= VolumeBar_ValueChanged; - _volumeBar.Value = (int)(volume * _volumeBar.Maximum); - _volumeBar.ValueChanged += VolumeBar_ValueChanged; - } - private bool _positionBarFree = true; - private void PositionBar_MouseUp(object sender, MouseEventArgs e) - { - if (e.Button == MouseButtons.Left) - { - Engine.Instance.Player.SetCurrentPosition(_positionBar.Value); - _positionBarFree = true; - LetUIKnowPlayerIsPlaying(); - } - } - private void PositionBar_MouseDown(object sender, MouseEventArgs e) - { - if (e.Button == MouseButtons.Left) - { - _positionBarFree = false; - } - } - - private bool _autoplay = false; - private void SongNumerical_ValueChanged(object sender, EventArgs e) - { - _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; - - long index = (long)_songNumerical.Value; - Stop(); - Text = Utils.ProgramName; - _songsComboBox.SelectedIndex = 0; - _songInfo.DeleteData(); - bool success; - try - { - Engine.Instance.Player.LoadSong(index); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, string.Format(Strings.ErrorLoadSong, Engine.Instance.Config.GetSongName(index))); - success = false; - } - - _trackViewer?.UpdateTracks(); - if (success) - { - Config config = Engine.Instance.Config; - List songs = config.Playlists[0].Songs; // Complete "Music" playlist is present in all configs at index 0 - Config.Song song = songs.SingleOrDefault(s => s.Index == index); - - // When the song isn't a null value and is played - if (song != null) - { - Text = $"{Utils.ProgramName} - {song.Name}"; // Reads the song name from the .yaml file - _songsComboBox.SelectedIndex = songs.IndexOf(song) + 1; // + 1 because the "Music" playlist is first in the combobox - } - - // When the song is a null value and is played - if (song == null) - { - return; // Resets the music player and prevents the song from playing - } - _positionBar.Maximum = Engine.Instance.Player.MaxTicks; - _positionBar.LargeChange = _positionBar.Maximum / 10; - _positionBar.SmallChange = _positionBar.LargeChange / 4; - _songInfo.SetNumTracks(Engine.Instance.Player.Events.Length); - if (_autoplay) - { - Play(); - } - } - else - { - _songInfo.SetNumTracks(0); - } - int numTracks = (Engine.Instance.Player.Events?.Length).GetValueOrDefault(); - _positionBar.Enabled = _exportWAVItem.Enabled = success && numTracks > 0; - _exportMIDIItem.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_MP2K && numTracks > 0; - _exportDLSItem.Enabled = _exportSF2Item.Enabled = success && Engine.Instance.Type == Engine.EngineType.GBA_AlphaDream; - - _autoplay = true; - _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; - } - private void SongsComboBox_SelectedIndexChanged(object sender, EventArgs e) - { - var item = (ImageComboBoxItem)_songsComboBox.SelectedItem; - if (item.Item is Config.Song song) - { - SetAndLoadSong(song.Index); - } - else if (item.Item is Config.Playlist playlist) - { - if (playlist.Songs.Count > 0 - && FlexibleMessageBox.Show(string.Format(Strings.PlayPlaylistBody, Environment.NewLine + playlist), Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) - { - ResetPlaylistStuff(false); - _curPlaylist = playlist; - Engine.Instance.Player.ShouldFadeOut = _playlistPlaying = true; - Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; - _endPlaylistItem.Enabled = true; - SetAndLoadNextPlaylistSong(); - } - } - } - private void SetAndLoadSong(long index) - { - _curSong = index; - if (_songNumerical.Value == index) - { - SongNumerical_ValueChanged(null, null); - } - else - { - _songNumerical.Value = index; - } - } - private void SetAndLoadNextPlaylistSong() - { - if (_remainingSongs.Count == 0) - { - _remainingSongs.AddRange(_curPlaylist.Songs.Select(s => s.Index)); - if (GlobalConfig.Instance.PlaylistMode == PlaylistMode.Random) - { - _remainingSongs.Shuffle(); - } - } - long nextSong = _remainingSongs[0]; - _remainingSongs.RemoveAt(0); - SetAndLoadSong(nextSong); - } - private void ResetPlaylistStuff(bool enableds) - { - if (Engine.Instance != null) - { - Engine.Instance.Player.ShouldFadeOut = false; - } - _playlistPlaying = false; - _curPlaylist = null; - _curSong = -1; - _remainingSongs.Clear(); - _playedSongs.Clear(); - _endPlaylistItem.Enabled = false; - _songNumerical.Enabled = _songsComboBox.Enabled = enableds; - } - private void EndCurrentPlaylist(object sender, EventArgs e) - { - if (FlexibleMessageBox.Show(Strings.EndPlaylistBody, Strings.MenuPlaylist, MessageBoxButtons.YesNo) == DialogResult.Yes) - { - ResetPlaylistStuff(true); - } - } - - private void OpenDSE(object sender, EventArgs e) - { - var d = new FolderBrowserDialog - { - Description = Strings.MenuOpenDSE, - UseDescriptionForTitle = true - }; - if (d.ShowDialog() == DialogResult.OK) - { - DisposeEngine(); - bool success; - - new Engine(Engine.EngineType.NDS_DSE, d.SelectedPath); - success = true; - if (success != true) - { - Exception ex = new Exception(); - FlexibleMessageBox.Show(ex, Strings.ErrorOpenDSE); - success = false; - } - if (success) - { - var config = (Core.NDS.DSE.Config)Engine.Instance.Config; - FinishLoading(config.BGMFiles.Length); - _songNumerical.Visible = false; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = false; - _exportSF2Item.Visible = false; - } - } - } - private void OpenAlphaDream(object sender, EventArgs e) - { - var d = new OpenFileDialog - { - Title = Strings.MenuOpenAlphaDream, - Filter = "Game Boy Advance ROM (*.gba, *.srl)|*.gba;*.srl|All files (*.*)|*.*", - FilterIndex = 2, - RestoreDirectory = true - }; - if (d.ShowDialog() == DialogResult.OK) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.GBA_AlphaDream, File.ReadAllBytes(d.FileName)); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenAlphaDream); - success = false; - } - if (success) - { - var config = (Core.GBA.AlphaDream.Config)Engine.Instance.Config; - FinishLoading(config.SongTableSizes[0]); - _songNumerical.Visible = true; - _exportDLSItem.Visible = true; - _exportMIDIItem.Visible = false; - _exportSF2Item.Visible = true; - } - } - } - private void OpenMP2K(object sender, EventArgs e) - { - var d = new OpenFileDialog - { - Title = Strings.MenuOpenMP2K, - Filter = "Game Boy Advance ROM (*.gba, *.srl)|*.gba;*.srl|All files (*.*)|*.*", - FilterIndex = 1, - RestoreDirectory = true - }; - if (d.ShowDialog() == DialogResult.OK) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.GBA_MP2K, File.ReadAllBytes(d.FileName)); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenMP2K); - success = false; - } - if (success) - { - var config = (Core.GBA.MP2K.Config)Engine.Instance.Config; - FinishLoading(config.SongTableSizes[0]); - _songNumerical.Visible = true; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = true; - _exportSF2Item.Visible = false; - } - } - } - private void OpenSDAT(object sender, EventArgs e) - { - var d = new OpenFileDialog - { - Title = Strings.MenuOpenSDAT, - Filter = "Nitro Soundmaker Sound Data Archive (*.sdat)|*.sdat|All files (*.*)|*.*", - FilterIndex = 1, - RestoreDirectory = true - }; - if (d.ShowDialog() == DialogResult.OK) - { - DisposeEngine(); - bool success; - try - { - new Engine(Engine.EngineType.NDS_SDAT, new Core.NDS.SDAT.SDAT(File.ReadAllBytes(d.FileName))); - success = true; - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorOpenSDAT); - success = false; - } - if (success) - { - var config = (Core.NDS.SDAT.Config)Engine.Instance.Config; - FinishLoading(config.SDAT.INFOBlock.SequenceInfos.NumEntries); - _songNumerical.Visible = true; - _exportDLSItem.Visible = false; - _exportMIDIItem.Visible = false; - _exportSF2Item.Visible = false; - } - } - } - - private void ExportDLS(object sender, EventArgs e) - { - var d = new SaveFileDialog - { - FileName = Engine.Instance.Config.GetGameName(), - Filter = Strings.FilterSaveDLS, - FilterIndex = 1, - ValidateNames = true, - Title = Strings.MenuSaveDLS - }; - if (d.ShowDialog() == DialogResult.OK) - { - try - { - Core.GBA.AlphaDream.SoundFontSaver_DLS.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveDLS, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveDLS); - } - } - } - private void ExportMIDI(object sender, EventArgs e) - { - var d = new SaveFileDialog - { - FileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), - DefaultExt = ".mid", - ValidateNames = true, - Title = Strings.MenuSaveMIDI, - Filter = Strings.FilterSaveMIDI - }; - if (d.ShowDialog() == DialogResult.OK) - { - var p = (Core.GBA.MP2K.Player)Engine.Instance.Player; - var args = new Core.GBA.MP2K.Player.MIDISaveArgs - { - SaveCommandsBeforeTranspose = true, - ReverseVolume = false, - TimeSignatures = new List<(int AbsoluteTick, (byte Numerator, byte Denominator))> - { - (0, (4, 4)) - } - }; - try - { - p.SaveAsMIDI(d.FileName, args); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveMIDI, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveMIDI); - } - } - } - private void ExportSF2(object sender, EventArgs e) - { - var d = new SaveFileDialog - { - FileName = Engine.Instance.Config.GetGameName(), - DefaultExt = ".sf2", - ValidateNames = true, - Title = Strings.MenuSaveSF2, - Filter = Strings.FilterSaveSF2 - }; - if (d.ShowDialog() == DialogResult.OK) - { - try - { - Core.GBA.AlphaDream.SoundFontSaver_SF2.Save((Core.GBA.AlphaDream.Config)Engine.Instance.Config, d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveSF2, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveSF2); - } - } - } - private void ExportWAV(object sender, EventArgs e) - { - var d = new SaveFileDialog - { - FileName = Engine.Instance.Config.GetSongName((long)_songNumerical.Value), - DefaultExt = ".wav", - ValidateNames = true, - Title = Strings.MenuSaveWAV, - Filter = Strings.FilterSaveWAV - }; - if (d.ShowDialog() == DialogResult.OK) - { - Stop(); - bool oldFade = Engine.Instance.Player.ShouldFadeOut; - long oldLoops = Engine.Instance.Player.NumLoops; - Engine.Instance.Player.ShouldFadeOut = true; - Engine.Instance.Player.NumLoops = GlobalConfig.Instance.PlaylistSongLoops; - try - { - Engine.Instance.Player.Record(d.FileName); - FlexibleMessageBox.Show(string.Format(Strings.SuccessSaveWAV, d.FileName), Text); - } - catch (Exception ex) - { - FlexibleMessageBox.Show(ex, Strings.ErrorSaveWAV); - } - Engine.Instance.Player.ShouldFadeOut = oldFade; - Engine.Instance.Player.NumLoops = oldLoops; - _stopUI = false; - } - } - - public void LetUIKnowPlayerIsPlaying() - { - if (!_timer.Enabled) - { - _pauseButton.Enabled = _stopButton.Enabled = true; - _pauseButton.Text = Strings.PlayerPause; - _timer.Interval = (int)(1_000d / GlobalConfig.Instance.RefreshRate); - _timer.Start(); - } - } - private void Play() - { - Engine.Instance.Player.Play(); - LetUIKnowPlayerIsPlaying(); - } - private void Pause() - { - Engine.Instance.Player.Pause(); - if (Engine.Instance.Player.State == PlayerState.Paused) - { - _pauseButton.Text = Strings.PlayerUnpause; - _timer.Stop(); - } - else - { - _pauseButton.Text = Strings.PlayerPause; - _timer.Start(); - } - } - private void Stop() - { - Engine.Instance.Player.Stop(); - _pauseButton.Enabled = _stopButton.Enabled = false; - _pauseButton.Text = Strings.PlayerPause; - _timer.Stop(); - _songInfo.DeleteData(); - _piano.UpdateKeys(_songInfo.Info, PianoTracks); - UpdatePositionIndicators(0L); - } - private void TogglePlayback(object sender, EventArgs e) - { - if (Engine.Instance.Player.State == PlayerState.Stopped) - { - Play(); - } - else if (Engine.Instance.Player.State == PlayerState.Paused || Engine.Instance.Player.State == PlayerState.Playing) - { - Pause(); - } - } - private void PlayPreviousSong(object sender, EventArgs e) - { - long prevSong; - if (_playlistPlaying) - { - int index = _playedSongs.Count - 1; - prevSong = _playedSongs[index]; - _playedSongs.RemoveAt(index); - _remainingSongs.Insert(0, _curSong); - } - else - { - prevSong = (long)_songNumerical.Value - 1; - } - SetAndLoadSong(prevSong); - } - private void PlayNextSong(object sender, EventArgs e) - { - if (_playlistPlaying) - { - _playedSongs.Add(_curSong); - SetAndLoadNextPlaylistSong(); - } - else - { - SetAndLoadSong((long)_songNumerical.Value + 1); - } - } - - private void FinishLoading(long numSongs) - { - Engine.Instance.Player.SongEnded += SongEnded; - foreach (Config.Playlist playlist in Engine.Instance.Config.Playlists) - { - _songsComboBox.Items.Add(new ImageComboBoxItem(playlist, Resources.IconPlaylist, 0)); - _songsComboBox.Items.AddRange(playlist.Songs.Select(s => new ImageComboBoxItem(s, Resources.IconSong, 1)).ToArray()); - } - _songNumerical.Maximum = numSongs - 1; -#if DEBUG - //VGMSDebug.EventScan(Engine.Instance.Config.Playlists[0].Songs, numericalVisible); -#endif - _autoplay = false; - SetAndLoadSong(Engine.Instance.Config.Playlists[0].Songs.Count == 0 ? 0 : Engine.Instance.Config.Playlists[0].Songs[0].Index); - _songsComboBox.Enabled = _songNumerical.Enabled = _playButton.Enabled = _volumeBar.Enabled = true; - } - private void DisposeEngine() - { - if (Engine.Instance != null) - { - Stop(); - Engine.Instance.Dispose(); - } - _trackViewer?.UpdateTracks(); - Text = Utils.ProgramName; - _songInfo.SetNumTracks(0); - _songInfo.ResetMutes(); - ResetPlaylistStuff(false); - UpdatePositionIndicators(0L); - _songsComboBox.SelectedIndexChanged -= SongsComboBox_SelectedIndexChanged; - _songNumerical.ValueChanged -= SongNumerical_ValueChanged; - _songNumerical.Visible = false; - _songNumerical.Value = _songNumerical.Maximum = 0; - _songsComboBox.SelectedItem = null; - _songsComboBox.Items.Clear(); - _songsComboBox.SelectedIndexChanged += SongsComboBox_SelectedIndexChanged; - _songNumerical.ValueChanged += SongNumerical_ValueChanged; - } - private bool _stopUI = false; - private void UpdateUI(object sender, EventArgs e) - { - if (_stopUI) - { - _stopUI = false; - if (_playlistPlaying) - { - _playedSongs.Add(_curSong); - SetAndLoadNextPlaylistSong(); - } - else - { - Stop(); - } - } - else - { - if (WindowState != FormWindowState.Minimized) - { - SongInfoControl.SongInfo info = _songInfo.Info; - Engine.Instance.Player.GetSongState(info); - _piano.UpdateKeys(info, PianoTracks); - _songInfo.Invalidate(); - } - UpdatePositionIndicators(Engine.Instance.Player.ElapsedTicks); - } - } - private void SongEnded() - { - _stopUI = true; - } - - private void UpdatePositionIndicators(long ticks) - { - if (_positionBarFree) - { - _positionBar.Value = ticks; - } - } - - private void OpenTrackViewer(object sender, EventArgs e) - { - if (_trackViewer != null) - { - _trackViewer.Focus(); - return; - } - _trackViewer = new TrackViewer { Owner = this }; - _trackViewer.FormClosed += (o, s) => _trackViewer = null; - _trackViewer.Show(); - } - - protected override void OnFormClosing(FormClosingEventArgs e) - { - DisposeEngine(); - base.OnFormClosing(e); - } - private void OnResize(object sender, EventArgs e) - { - if (WindowState != FormWindowState.Minimized) - { - _splitContainer.SplitterDistance = (int)(ClientSize.Height / 5.5) - 25; // -25 for menustrip (24) and itself (1) - - int w1 = (int)(_splitContainer.Panel1.Width / 2.35); - int h1 = (int)(_splitContainer.Panel1.Height / 5.0); - - int xoff = _splitContainer.Panel1.Width / 83; - int yoff = _splitContainer.Panel1.Height / 25; - int a, b, c, d; - - // Buttons - a = (w1 / 3) - xoff; - b = (xoff / 2) + 1; - _playButton.Location = new Point(xoff + b, yoff); - _pauseButton.Location = new Point((xoff * 2) + a + b, yoff); - _stopButton.Location = new Point((xoff * 3) + (a * 2) + b, yoff); - _playButton.Size = _pauseButton.Size = _stopButton.Size = new Size(a, h1); - c = yoff + ((h1 - 21) / 2); - _songNumerical.Location = new Point((xoff * 4) + (a * 3) + b, c); - _songNumerical.Size = new Size((int)(a / 1.175), 21); - // Song combobox - d = _splitContainer.Panel1.Width - w1 - xoff; - _songsComboBox.Location = new Point(d, c); - _songsComboBox.Size = new Size(w1, 21); - - // Volume bar - c = (int)(_splitContainer.Panel1.Height / 3.5); - _volumeBar.Location = new Point(xoff, c); - _volumeBar.Size = new Size(w1, h1); - // Position bar - _positionBar.Location = new Point(d, c); - _positionBar.Size = new Size(w1, h1); - - // Piano - _piano.Size = new Size(_splitContainer.Panel1.Width, (int)(_splitContainer.Panel1.Height / 2.5)); // Force it to initialize piano keys again - _piano.Location = new Point((_splitContainer.Panel1.Width - (_piano.WhiteKeyWidth * PianoControl.WhiteKeyCount)) / 2, _splitContainer.Panel1.Height - _piano.Height - 1); - } - } - protected override bool ProcessCmdKey(ref Message msg, Keys keyData) - { - if (keyData == Keys.Space && _playButton.Enabled && !_songsComboBox.Focused) - { - TogglePlayback(null, null); - return true; - } - else - { - return base.ProcessCmdKey(ref msg, keyData); - } - } - } -} diff --git a/WinForms/UI/PianoControl.cs b/WinForms/UI/PianoControl.cs deleted file mode 100644 index 102a4b0a..00000000 --- a/WinForms/UI/PianoControl.cs +++ /dev/null @@ -1,183 +0,0 @@ -#region License - -/* Copyright (c) 2006 Leslie Sanford - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#endregion - -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Util; -using System; -using System.ComponentModel; -using System.Drawing; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class PianoControl : Control - { - private enum KeyType : byte - { - Black, - White - } - private static readonly KeyType[] KeyTypeTable = new KeyType[12] - { - KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White, KeyType.Black, KeyType.White - }; - private const double blackKeyScale = 2.0 / 3.0; - - public class PianoKey : Control - { - public bool Dirty; - public bool Pressed; - - public readonly SolidBrush OnBrush = new SolidBrush(Color.Transparent); - private readonly SolidBrush _offBrush; - - public PianoKey(byte k) - { - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); - SetStyle(ControlStyles.Selectable, false); - _offBrush = new SolidBrush(new HSLColor(160.0, 0.0, KeyTypeTable[k % 12] == KeyType.White ? k / 12 % 2 == 0 ? 240.0 : 120.0 : 0.0)); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - OnBrush.Dispose(); - _offBrush.Dispose(); - } - base.Dispose(disposing); - } - protected override void OnPaint(PaintEventArgs e) - { - e.Graphics.FillRectangle(Pressed ? OnBrush : _offBrush, 1, 1, Width - 2, Height - 2); - e.Graphics.DrawRectangle(Pens.Black, 0, 0, Width - 1, Height - 1); - base.OnPaint(e); - } - } - - private readonly PianoKey[] _keys = new PianoKey[0x80]; - public const int WhiteKeyCount = 75; - public int WhiteKeyWidth; - - public PianoControl() - { - SetStyle(ControlStyles.Selectable, false); - for (byte k = 0; k <= 0x7F; k++) - { - var key = new PianoKey(k); - _keys[k] = key; - if (KeyTypeTable[k % 12] == KeyType.Black) - { - key.BringToFront(); - } - Controls.Add(key); - } - SetKeySizes(); - } - private void SetKeySizes() - { - WhiteKeyWidth = Width / WhiteKeyCount; - int blackKeyWidth = (int)(WhiteKeyWidth * blackKeyScale); - int blackKeyHeight = (int)(Height * blackKeyScale); - int offset = WhiteKeyWidth - (blackKeyWidth / 2); - int w = 0; - for (int k = 0; k <= 0x7F; k++) - { - PianoKey key = _keys[k]; - if (KeyTypeTable[k % 12] == KeyType.White) - { - key.Height = Height; - key.Width = WhiteKeyWidth; - key.Location = new Point(w * WhiteKeyWidth, 0); - w++; - } - else - { - key.Height = blackKeyHeight; - key.Width = blackKeyWidth; - key.Location = new Point(offset + ((w - 1) * WhiteKeyWidth)); - key.BringToFront(); - } - } - } - - public void UpdateKeys(SongInfoControl.SongInfo info, bool[] enabledTracks) - { - for (int k = 0; k <= 0x7F; k++) - { - PianoKey key = _keys[k]; - key.Dirty = key.Pressed; - key.Pressed = false; - } - for (int i = SongInfoControl.SongInfo.MaxTracks - 1; i >= 0; i--) - { - if (enabledTracks[i]) - { - SongInfoControl.SongInfo.Track tin = info.Tracks[i]; - for (int nk = 0; nk < SongInfoControl.SongInfo.MaxKeys; nk++) - { - byte k = tin.Keys[nk]; - if (k == byte.MaxValue) - { - break; - } - else - { - PianoKey key = _keys[k]; - key.OnBrush.Color = GlobalConfig.Instance.Colors[tin.Voice]; - key.Pressed = key.Dirty = true; - } - } - } - } - for (int k = 0; k <= 0x7F; k++) - { - PianoKey key = _keys[k]; - if (key.Dirty) - { - key.Invalidate(); - } - } - } - - protected override void OnResize(EventArgs e) - { - SetKeySizes(); - base.OnResize(e); - } - protected override void Dispose(bool disposing) - { - if (disposing) - { - for (int k = 0; k < 0x80; k++) - { - _keys[k].Dispose(); - } - } - base.Dispose(disposing); - } - } -} diff --git a/WinForms/UI/SongInfoControl.cs b/WinForms/UI/SongInfoControl.cs deleted file mode 100644 index ce91a71d..00000000 --- a/WinForms/UI/SongInfoControl.cs +++ /dev/null @@ -1,354 +0,0 @@ -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.ComponentModel; -using System.Drawing; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class SongInfoControl : Control - { - public class SongInfo - { - public class Track - { - public long Position; - public byte Voice; - public byte Volume; - public int LFO; - public long Rest; - public sbyte Panpot; - public float LeftVolume; - public float RightVolume; - public int PitchBend; - public byte Extra; - public string Type; - public byte[] Keys = new byte[MaxKeys]; - - public int PreviousKeysTime; - public string PreviousKeys; - - public Track() - { - for (int i = 0; i < MaxKeys; i++) - { - Keys[i] = byte.MaxValue; - } - } - } - public const int MaxKeys = 32 + 1; // DSE is currently set to use 32 channels - public const int MaxTracks = 18; // PMD2 has a few songs with 18 tracks - - public ushort Tempo; - public Track[] Tracks = new Track[MaxTracks]; - - public SongInfo() - { - for (int i = 0; i < MaxTracks; i++) - { - Tracks[i] = new Track(); - } - } - } - - private const int _checkboxSize = 15; - - private readonly CheckBox[] _mutes; - private readonly CheckBox[] _pianos; - private readonly SolidBrush _solidBrush = new SolidBrush(Theme.PlayerColor); - private readonly Pen _pen = new Pen(Color.Transparent); - - public SongInfo Info; - private int _numTracksToDraw; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _solidBrush.Dispose(); - _pen.Dispose(); - } - base.Dispose(disposing); - } - public SongInfoControl() - { - SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); - SetStyle(ControlStyles.Selectable, false); - Font = new Font("Segoe UI", 10.5f, FontStyle.Regular, GraphicsUnit.Point); - Size = new Size(675, 675); - - _pianos = new CheckBox[SongInfo.MaxTracks + 1]; - _mutes = new CheckBox[SongInfo.MaxTracks + 1]; - for (int i = 0; i < SongInfo.MaxTracks + 1; i++) - { - _pianos[i] = new CheckBox - { - BackColor = Color.Transparent, - Checked = true, - Size = new Size(_checkboxSize, _checkboxSize), - TabStop = false - }; - _pianos[i].CheckStateChanged += TogglePiano; - _mutes[i] = new CheckBox - { - BackColor = Color.Transparent, - Checked = true, - Size = new Size(_checkboxSize, _checkboxSize), - TabStop = false - }; - _mutes[i].CheckStateChanged += ToggleMute; - } - Controls.AddRange(_pianos); - Controls.AddRange(_mutes); - - SetNumTracks(0); - DeleteData(); - } - - private void TogglePiano(object sender, EventArgs e) - { - var check = (CheckBox)sender; - CheckBox master = _pianos[SongInfo.MaxTracks]; - if (check == master) - { - bool b = check.CheckState != CheckState.Unchecked; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - _pianos[i].Checked = b; - } - } - else - { - int numChecked = 0; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - if (_pianos[i] == check) - { - MainForm.Instance.PianoTracks[i] = _pianos[i].Checked; - } - if (_pianos[i].Checked) - { - numChecked++; - } - } - master.CheckStateChanged -= TogglePiano; - master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); - master.CheckStateChanged += TogglePiano; - } - } - private void ToggleMute(object sender, EventArgs e) - { - var check = (CheckBox)sender; - CheckBox master = _mutes[SongInfo.MaxTracks]; - if (check == master) - { - bool b = check.CheckState != CheckState.Unchecked; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - _mutes[i].Checked = b; - } - } - else - { - int numChecked = 0; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - if (_mutes[i] == check) - { - Engine.Instance.Mixer.Mutes[i] = !check.Checked; - } - if (_mutes[i].Checked) - { - numChecked++; - } - } - master.CheckStateChanged -= ToggleMute; - master.CheckState = numChecked == SongInfo.MaxTracks ? CheckState.Checked : (numChecked == 0 ? CheckState.Unchecked : CheckState.Indeterminate); - master.CheckStateChanged += ToggleMute; - } - } - - public void DeleteData() - { - Info = new SongInfo(); - Invalidate(); - } - public void SetNumTracks(int num) - { - _numTracksToDraw = num; - _pianos[SongInfo.MaxTracks].Enabled = _mutes[SongInfo.MaxTracks].Enabled = num > 0; - for (int i = 0; i < SongInfo.MaxTracks; i++) - { - _pianos[i].Visible = _mutes[i].Visible = i < num; - } - OnResize(EventArgs.Empty); - } - public void ResetMutes() - { - for (int i = 0; i < SongInfo.MaxTracks + 1; i++) - { - CheckBox mute = _mutes[i]; - mute.CheckStateChanged -= ToggleMute; - mute.CheckState = CheckState.Checked; - mute.CheckStateChanged += ToggleMute; - } - } - - private float _infoHeight, _infoY, _positionX, _keysX, _delayX, _typeEndX, _typeX, _voicesX, _row2ElementAdditionX, _yMargin, _trackHeight, _row2Offset, _tempoX; - private int _barHeight, _barStartX, _barWidth, _bwd, _barRightBoundX, _barCenterX; - protected override void OnResize(EventArgs e) - { - _infoHeight = Height / 30f; - _infoY = _infoHeight - (TextRenderer.MeasureText("A", Font).Height * 1.125f); - _positionX = (_checkboxSize * 2) + 2; - int fWidth = Width - (int)_positionX; // Width between checkboxes' edges and the window edge - _keysX = _positionX + (fWidth / 4.4f); - _delayX = _positionX + (fWidth / 7.5f); - _typeEndX = _positionX + fWidth - (fWidth / 100f); - _typeX = _typeEndX - TextRenderer.MeasureText(Strings.PlayerType, Font).Width; - _voicesX = _positionX + (fWidth / 25f); - _row2ElementAdditionX = fWidth / 15f; - - _yMargin = Height / 200f; - _trackHeight = (Height - _yMargin) / ((_numTracksToDraw < 16 ? 16 : _numTracksToDraw) * 1.04f); - _row2Offset = _trackHeight / 2.5f; - _barHeight = (int)(Height / 30.3f); - _barStartX = (int)(_positionX + (fWidth / 2.35f)); - _barWidth = (int)(fWidth / 2.95f); - _bwd = _barWidth % 2; // Add/Subtract by 1 if the bar width is odd - _barRightBoundX = _barStartX + _barWidth - _bwd; - _barCenterX = _barStartX + (_barWidth / 2); - - _tempoX = _barCenterX - (TextRenderer.MeasureText(string.Format("{0} - 999", Strings.PlayerTempo), Font).Width / 2); - - if (_mutes != null) - { - int x1 = 3; - int x2 = _checkboxSize + 4; - int y = (int)_infoY + 3; - _mutes[SongInfo.MaxTracks].Location = new Point(x1, y); - _pianos[SongInfo.MaxTracks].Location = new Point(x2, y); - for (int i = 0; i < _numTracksToDraw; i++) - { - float r1y = _infoHeight + _yMargin + (i * _trackHeight); - y = (int)r1y + 4; - _mutes[i].Location = new Point(x1, y); - _pianos[i].Location = new Point(x2, y); - } - } - - base.OnResize(e); - } - protected override void OnPaint(PaintEventArgs e) - { - _solidBrush.Color = Theme.PlayerColor; - e.Graphics.FillRectangle(_solidBrush, e.ClipRectangle); - - e.Graphics.DrawString(Strings.PlayerPosition, Font, Brushes.Lime, _positionX, _infoY); - e.Graphics.DrawString(Strings.PlayerRest, Font, Brushes.Crimson, _delayX, _infoY); - e.Graphics.DrawString(Strings.PlayerNotes, Font, Brushes.Turquoise, _keysX, _infoY); - e.Graphics.DrawString("L", Font, Brushes.GreenYellow, _barStartX - 5, _infoY); - e.Graphics.DrawString(string.Format("{0} - ", Strings.PlayerTempo) + Info.Tempo, Font, Brushes.Cyan, _tempoX, _infoY); - e.Graphics.DrawString("R", Font, Brushes.GreenYellow, _barRightBoundX - 5, _infoY); - e.Graphics.DrawString(Strings.PlayerType, Font, Brushes.DeepPink, _typeX, _infoY); - e.Graphics.DrawLine(Pens.Gold, 0, _infoHeight, Width, _infoHeight); - - for (int i = 0; i < _numTracksToDraw; i++) - { - SongInfo.Track track = Info.Tracks[i]; - float r1y = _infoHeight + _yMargin + (i * _trackHeight); // Row 1 y - e.Graphics.DrawString(string.Format("0x{0:X}", track.Position), Font, Brushes.Lime, _positionX, r1y); - e.Graphics.DrawString(track.Rest.ToString(), Font, Brushes.Crimson, _delayX, r1y); - - float r2y = r1y + _row2Offset; // Row 2 y - e.Graphics.DrawString(track.Panpot.ToString(), Font, Brushes.OrangeRed, _voicesX + _row2ElementAdditionX, r2y); - e.Graphics.DrawString(track.Volume.ToString(), Font, Brushes.LightSeaGreen, _voicesX + (_row2ElementAdditionX * 2), r2y); - e.Graphics.DrawString(track.LFO.ToString(), Font, Brushes.SkyBlue, _voicesX + (_row2ElementAdditionX * 3), r2y); - e.Graphics.DrawString(track.PitchBend.ToString(), Font, Brushes.Purple, _voicesX + (_row2ElementAdditionX * 4), r2y); - e.Graphics.DrawString(track.Extra.ToString(), Font, Brushes.HotPink, _voicesX + (_row2ElementAdditionX * 5), r2y); - - int by = (int)(r1y + _yMargin); // Bar y - int byh = by + _barHeight; - e.Graphics.DrawString(track.Type, Font, Brushes.DeepPink, _typeEndX - e.Graphics.MeasureString(track.Type, Font).Width, by + (_row2Offset / (Font.Size / 2.5f))); - e.Graphics.DrawLine(Pens.GreenYellow, _barStartX, by, _barStartX, byh); // Left bar bound line - e.Graphics.DrawLine(Pens.GreenYellow, _barRightBoundX, by, _barRightBoundX, byh); // Right bar bound line - if (GlobalConfig.Instance.PanpotIndicators) - { - int pax = (int)(_barStartX + (_barWidth / 2) + (_barWidth / 2 * (track.Panpot / (float)0x40))); // Pan line x - e.Graphics.DrawLine(Pens.OrangeRed, pax, by, pax, byh); // Pan line - } - - { - Color color = GlobalConfig.Instance.Colors[track.Voice]; - _solidBrush.Color = color; - _pen.Color = color; - e.Graphics.DrawString(track.Voice.ToString(), Font, _solidBrush, _voicesX, r2y); - var rect = new Rectangle((int)(_barStartX + (_barWidth / 2) - (track.LeftVolume * _barWidth / 2)) + _bwd, - by, - (int)((track.LeftVolume + track.RightVolume) * _barWidth / 2), - _barHeight); - if (!rect.IsEmpty) - { - float velocity = (track.LeftVolume + track.RightVolume) * 2f; - if (velocity > 1f) - { - velocity = 1f; - } - _solidBrush.Color = Color.FromArgb((byte)(velocity * byte.MaxValue), color); - e.Graphics.FillRectangle(_solidBrush, rect); - e.Graphics.DrawRectangle(_pen, rect); - } - if (GlobalConfig.Instance.CenterIndicators) - { - e.Graphics.DrawLine(_pen, _barCenterX, by, _barCenterX, byh); // Center line - } - } - { - string keysString; - if (track.Keys[0] == byte.MaxValue) - { - if (track.PreviousKeysTime != 0) - { - track.PreviousKeysTime--; - keysString = track.PreviousKeys; - } - else - { - keysString = string.Empty; - } - } - else - { - keysString = string.Empty; - for (int nk = 0; nk < SongInfo.MaxKeys; nk++) - { - byte k = track.Keys[nk]; - if (k == byte.MaxValue) - { - break; - } - else - { - if (nk != 0) - { - keysString += ' '; - } - keysString += Utils.GetNoteName(k); - } - } - track.PreviousKeysTime = GlobalConfig.Instance.RefreshRate << 2; - track.PreviousKeys = keysString; - } - if (keysString != string.Empty) - { - e.Graphics.DrawString(keysString, Font, Brushes.Turquoise, _keysX, r1y); - } - } - } - base.OnPaint(e); - } - } -} diff --git a/WinForms/UI/Theme.cs b/WinForms/UI/Theme.cs deleted file mode 100644 index 0973e2a7..00000000 --- a/WinForms/UI/Theme.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - internal static class Theme - { - public static readonly Font Font = new Font("Segoe UI", 8f, FontStyle.Bold); - public static readonly Color - BackColor = Color.FromArgb(33, 33, 39), - BackColorDisabled = Color.FromArgb(35, 42, 47), - BackColorMouseOver = Color.FromArgb(32, 37, 47), - BorderColor = Color.FromArgb(25, 120, 186), - BorderColorDisabled = Color.FromArgb(47, 55, 60), - ForeColor = Color.FromArgb(94, 159, 230), - PlayerColor = Color.FromArgb(8, 8, 8), - SelectionColor = Color.FromArgb(7, 51, 141), - TitleBar = Color.FromArgb(16, 40, 63); - - public static HSLColor DrainColor(Color c) - { - var drained = new HSLColor(c); - drained.Saturation /= 2.5; - return drained; - } - } - - internal class ThemedButton : Button - { - public ThemedButton() : base() - { - FlatAppearance.MouseOverBackColor = Theme.BackColorMouseOver; - FlatStyle = FlatStyle.Flat; - Font = Theme.Font; - ForeColor = Theme.ForeColor; - } - protected override void OnEnabledChanged(EventArgs e) - { - base.OnEnabledChanged(e); - BackColor = Enabled ? Theme.BackColor : Theme.BackColorDisabled; - FlatAppearance.BorderColor = Enabled ? Theme.BorderColor : Theme.BorderColorDisabled; - } - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - if (!Enabled) - { - TextRenderer.DrawText(e.Graphics, Text, Font, ClientRectangle, Theme.DrainColor(ForeColor), BackColor); - } - } - protected override bool ShowFocusCues => false; - } - internal class ThemedLabel : Label - { - public ThemedLabel() : base() - { - Font = Theme.Font; - ForeColor = Theme.ForeColor; - } - } - internal class ThemedForm : Form - { - public ThemedForm() : base() - { - BackColor = Theme.BackColor; - Icon = Resources.Icon; - } - } - internal class ThemedPanel : Panel - { - public ThemedPanel() : base() - { - SetStyle(ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); - } - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - using (var b = new SolidBrush(BackColor)) - { - e.Graphics.FillRectangle(b, e.ClipRectangle); - } - using (var b = new SolidBrush(Theme.BorderColor)) - using (var p = new Pen(b, 2)) - { - e.Graphics.DrawRectangle(p, e.ClipRectangle); - } - } - private const int WM_PAINT = 0xF; - protected override void WndProc(ref Message m) - { - if (m.Msg == WM_PAINT) - { - Invalidate(); - } - base.WndProc(ref m); - } - } - internal class ThemedTextBox : TextBox - { - public ThemedTextBox() : base() - { - BackColor = Theme.BackColor; - Font = Theme.Font; - ForeColor = Theme.ForeColor; - } - [DllImport("user32.dll")] - private static extern IntPtr GetWindowDC(IntPtr hWnd); - [DllImport("user32.dll")] - private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); - [DllImport("user32.dll")] - private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprc, IntPtr hrgn, uint flags); - private const int WM_NCPAINT = 0x85; - private const uint RDW_INVALIDATE = 0x1; - private const uint RDW_IUPDATENOW = 0x100; - private const uint RDW_FRAME = 0x400; - protected override void WndProc(ref Message m) - { - base.WndProc(ref m); - if (m.Msg == WM_NCPAINT && BorderStyle == BorderStyle.Fixed3D) - { - IntPtr hdc = GetWindowDC(Handle); - using (var g = Graphics.FromHdcInternal(hdc)) - using (var p = new Pen(Theme.BorderColor)) - { - g.DrawRectangle(p, new Rectangle(0, 0, Width - 1, Height - 1)); - } - ReleaseDC(Handle, hdc); - } - } - protected override void OnSizeChanged(EventArgs e) - { - base.OnSizeChanged(e); - RedrawWindow(Handle, IntPtr.Zero, IntPtr.Zero, RDW_FRAME | RDW_IUPDATENOW | RDW_INVALIDATE); - } - } - internal class ThemedRichTextBox : RichTextBox - { - public ThemedRichTextBox() : base() - { - BackColor = Theme.BackColor; - Font = Theme.Font; - ForeColor = Theme.ForeColor; - SelectionColor = Theme.SelectionColor; - } - } - internal class ThemedNumeric : NumericUpDown - { - public ThemedNumeric() : base() - { - BackColor = Theme.BackColor; - Font = new Font(Theme.Font.FontFamily, 7.5f, Theme.Font.Style); - ForeColor = Theme.ForeColor; - TextAlign = HorizontalAlignment.Center; - } - protected override void OnPaint(PaintEventArgs e) - { - base.OnPaint(e); - ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Enabled ? Theme.BorderColor : Theme.BorderColorDisabled, ButtonBorderStyle.Solid); - } - } -} diff --git a/WinForms/UI/TrackViewer.cs b/WinForms/UI/TrackViewer.cs deleted file mode 100644 index 7aa83b97..00000000 --- a/WinForms/UI/TrackViewer.cs +++ /dev/null @@ -1,113 +0,0 @@ -using BrightIdeasSoftware; -using Kermalis.VGMusicStudio.Core; -using Kermalis.VGMusicStudio.Properties; -using Kermalis.VGMusicStudio.Util; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - [DesignerCategory("")] - internal class TrackViewer : ThemedForm - { - private List _events; - private readonly ObjectListView _listView; - private readonly ComboBox _tracksBox; - - public TrackViewer() - { - int w = (600 / 2) - 12 - 6, h = 400 - 12 - 11; - _listView = new ObjectListView - { - FullRowSelect = true, - HeaderStyle = ColumnHeaderStyle.Nonclickable, - HideSelection = false, - Location = new Point(12, 12), - MultiSelect = false, - RowFormatter = RowColorer, - ShowGroups = false, - Size = new Size(w, h), - UseFiltering = true, - UseFilterIndicator = true - }; - OLVColumn c1, c2, c3, c4; - c1 = new OLVColumn(Strings.TrackViewerEvent, "Command.Label"); - c2 = new OLVColumn(Strings.TrackViewerArguments, "Command.Arguments") { UseFiltering = false }; - c3 = new OLVColumn(Strings.TrackViewerOffset, "Offset") { AspectToStringFormat = "0x{0:X}", UseFiltering = false }; - c4 = new OLVColumn(Strings.TrackViewerTicks, "Ticks") { AspectGetter = (o) => string.Join(", ", ((SongEvent)o).Ticks), UseFiltering = false }; - c1.Width = c2.Width = c3.Width = 72; - c4.Width = 47; - c1.Hideable = c2.Hideable = c3.Hideable = c4.Hideable = false; - c1.TextAlign = c2.TextAlign = c3.TextAlign = c4.TextAlign = HorizontalAlignment.Center; - _listView.AllColumns.AddRange(new OLVColumn[] { c1, c2, c3, c4 }); - _listView.RebuildColumns(); - _listView.ItemActivate += ListView_ItemActivate; - - var panel1 = new ThemedPanel { Location = new Point(306, 12), Size = new Size(w, h) }; - _tracksBox = new ComboBox - { - Enabled = false, - Location = new Point(4, 4), - Size = new Size(100, 21) - }; - _tracksBox.SelectedIndexChanged += TracksBox_SelectedIndexChanged; - panel1.Controls.AddRange(new Control[] { _tracksBox }); - - ClientSize = new Size(600, 400); - Controls.AddRange(new Control[] { _listView, panel1 }); - FormBorderStyle = FormBorderStyle.FixedDialog; - MaximizeBox = false; - Text = $"{Utils.ProgramName} ― {Strings.TrackViewerTitle}"; - - UpdateTracks(); - } - - private void ListView_ItemActivate(object sender, EventArgs e) - { - List list = ((SongEvent)_listView.SelectedItem.RowObject).Ticks; - if (list.Count > 0) - { - Engine.Instance?.Player.SetCurrentPosition(list[0]); - MainForm.Instance.LetUIKnowPlayerIsPlaying(); - } - } - - private void RowColorer(OLVListItem item) - { - item.BackColor = ((SongEvent)item.RowObject).Command.Color; - } - - private void TracksBox_SelectedIndexChanged(object sender, EventArgs e) - { - int i = _tracksBox.SelectedIndex; - if (i != -1) - { - _events = Engine.Instance.Player.Events[i]; - _listView.SetObjects(_events); - } - else - { - _listView.Items.Clear(); - } - } - public void UpdateTracks() - { - int numTracks = (Engine.Instance?.Player.Events?.Length).GetValueOrDefault(); - bool tracks = numTracks > 0; - _tracksBox.Enabled = tracks; - if (tracks) - { - // Track 0, Track 1, ... - _tracksBox.DataSource = Enumerable.Range(0, numTracks).Select(i => string.Format(Strings.TrackViewerTrackX, i)).ToList(); - } - else - { - _tracksBox.DataSource = null; - } - } - } -} \ No newline at end of file diff --git a/WinForms/UI/ValueTextBox.cs b/WinForms/UI/ValueTextBox.cs deleted file mode 100644 index a83e3516..00000000 --- a/WinForms/UI/ValueTextBox.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Kermalis.VGMusicStudio.Util; -using System; -using System.Windows.Forms; - -namespace Kermalis.VGMusicStudio.UI -{ - internal class ValueTextBox : ThemedTextBox - { - private bool _hex = false; - public bool Hexadecimal - { - get => _hex; - set - { - _hex = value; - OnTextChanged(EventArgs.Empty); - SelectionStart = Text.Length; - } - } - private long _max = long.MaxValue; - public long Maximum - { - get => _max; - set - { - _max = value; - OnTextChanged(EventArgs.Empty); - } - } - private long _min = 0; - public long Minimum - { - get => _min; - set - { - _min = value; - OnTextChanged(EventArgs.Empty); - } - } - public long Value - { - get - { - if (TextLength > 0) - { - if (Utils.TryParseValue(Text, _min, _max, out long l)) - { - return l; - } - } - return _min; - } - set - { - int i = SelectionStart; - Text = Hexadecimal ? ("0x" + value.ToString("X")) : value.ToString(); - SelectionStart = i; - OnValueChanged(EventArgs.Empty); - } - } - - protected override void WndProc(ref Message m) - { - const int WM_NOTIFY = 0x0282; - if (m.Msg == WM_NOTIFY && m.WParam == new IntPtr(0xB)) - { - if (Hexadecimal && SelectionStart < 2) - { - SelectionStart = 2; - } - } - base.WndProc(ref m); - } - protected override void OnKeyPress(KeyPressEventArgs e) - { - e.Handled = true; // Don't pay attention to this event unless: - - if ((char.IsControl(e.KeyChar) && !(Hexadecimal && SelectionStart <= 2 && SelectionLength == 0 && e.KeyChar == (char)Keys.Back)) || // Backspace isn't used on the "0x" prefix - char.IsDigit(e.KeyChar) || // It is a digit - (e.KeyChar >= 'a' && e.KeyChar <= 'f') || // It is a letter that shows in hex - (e.KeyChar >= 'A' && e.KeyChar <= 'F')) - { - e.Handled = false; - } - base.OnKeyPress(e); - } - protected override void OnTextChanged(EventArgs e) - { - base.OnTextChanged(e); - Value = Value; - } - - private EventHandler _onValueChanged = null; - public event EventHandler ValueChanged - { - add => _onValueChanged += value; - remove => _onValueChanged -= value; - } - protected virtual void OnValueChanged(EventArgs e) - { - _onValueChanged?.Invoke(this, e); - } - } -} diff --git a/WinForms/Util/HSLColor.cs b/WinForms/Util/HSLColor.cs deleted file mode 100644 index 6dbda597..00000000 --- a/WinForms/Util/HSLColor.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Drawing; - -namespace Kermalis.VGMusicStudio.Util -{ - // https://richnewman.wordpress.com/about/code-listings-and-diagrams/hslcolor-class/ - class HSLColor - { - // Private data members below are on scale 0-1 - // They are scaled for use externally based on scale - private double hue = 1.0; - private double saturation = 1.0; - private double luminosity = 1.0; - - private const double scale = 240.0; - - public double Hue - { - get { return hue * scale; } - set { hue = CheckRange(value / scale); } - } - public double Saturation - { - get { return saturation * scale; } - set { saturation = CheckRange(value / scale); } - } - public double Luminosity - { - get { return luminosity * scale; } - set { luminosity = CheckRange(value / scale); } - } - - private double CheckRange(double value) - { - if (value < 0.0) - { - value = 0.0; - } - else if (value > 1.0) - { - value = 1.0; - } - return value; - } - - public override string ToString() - { - return string.Format("H: {0:#0.##} S: {1:#0.##} L: {2:#0.##}", Hue, Saturation, Luminosity); - } - - public string ToRGBString() - { - Color color = this; - return string.Format("R: {0:#0.##} G: {1:#0.##} B: {2:#0.##}", color.R, color.G, color.B); - } - - #region Casts to/from System.Drawing.Color - public static implicit operator Color(HSLColor hslColor) - { - double r = 0, g = 0, b = 0; - if (hslColor.luminosity != 0) - { - if (hslColor.saturation == 0) - { - r = g = b = hslColor.luminosity; - } - else - { - double temp2 = GetTemp2(hslColor); - double temp1 = 2.0 * hslColor.luminosity - temp2; - - r = GetColorComponent(temp1, temp2, hslColor.hue + 1.0 / 3.0); - g = GetColorComponent(temp1, temp2, hslColor.hue); - b = GetColorComponent(temp1, temp2, hslColor.hue - 1.0 / 3.0); - } - } - return Color.FromArgb((int)(255 * r), (int)(255 * g), (int)(255 * b)); - } - - private static double GetColorComponent(double temp1, double temp2, double temp3) - { - temp3 = MoveIntoRange(temp3); - if (temp3 < 1.0 / 6.0) - { - return temp1 + (temp2 - temp1) * 6.0 * temp3; - } - else if (temp3 < 0.5) - { - return temp2; - } - else if (temp3 < 2.0 / 3.0) - { - return temp1 + ((temp2 - temp1) * ((2.0 / 3.0) - temp3) * 6.0); - } - else - { - return temp1; - } - } - private static double MoveIntoRange(double temp3) - { - if (temp3 < 0.0) - { - temp3 += 1.0; - } - else if (temp3 > 1.0) - { - temp3 -= 1.0; - } - return temp3; - } - private static double GetTemp2(HSLColor hslColor) - { - double temp2; - if (hslColor.luminosity < 0.5) //<=?? - { - temp2 = hslColor.luminosity * (1.0 + hslColor.saturation); - } - else - { - temp2 = hslColor.luminosity + hslColor.saturation - (hslColor.luminosity * hslColor.saturation); - } - return temp2; - } - - public static implicit operator HSLColor(Color color) - { - HSLColor hslColor = new HSLColor - { - hue = color.GetHue() / 360.0, // We store hue as 0-1 as opposed to 0-360 - luminosity = color.GetBrightness(), - saturation = color.GetSaturation() - }; - return hslColor; - } - #endregion - - public void SetRGB(int red, int green, int blue) - { - HSLColor hslColor = Color.FromArgb(red, green, blue); - hue = hslColor.hue; - saturation = hslColor.saturation; - luminosity = hslColor.luminosity; - } - - public HSLColor() { } - public HSLColor(Color color) - { - SetRGB(color.R, color.G, color.B); - } - public HSLColor(int red, int green, int blue) - { - SetRGB(red, green, blue); - } - public HSLColor(double hue, double saturation, double luminosity) - { - Hue = hue; - Saturation = saturation; - Luminosity = luminosity; - } - } -} diff --git a/WinForms/WinForms.csproj b/WinForms/WinForms.csproj deleted file mode 100644 index ec3e0b7b..00000000 --- a/WinForms/WinForms.csproj +++ /dev/null @@ -1,57 +0,0 @@ - - - - WinExe - net6.0-windows - enable - true - enable - publish\ - true - Disk - - - - ..\Build\ - Off - - - - ..\Build\ - On - - - - Properties\Icon.ico - - - - Kermalis.VGMusicStudio.Program - - - - - - - - - Component - - - Component - - - - - - Designer - - - Designer - - - Designer - - - - \ No newline at end of file From 25745ead052efb2c84ed3ed9224c46c594985cc8 Mon Sep 17 00:00:00 2001 From: Davin Date: Wed, 31 Aug 2022 02:00:56 +1000 Subject: [PATCH 8/8] Updated Sanford.Multimedia.Midi.XML --- .../docs/Sanford.Multimedia.Midi.XML | 3482 +++++++++++++++-- 1 file changed, 3156 insertions(+), 326 deletions(-) diff --git a/Sanford.Multimedia.Midi.Core/docs/Sanford.Multimedia.Midi.XML b/Sanford.Multimedia.Midi.Core/docs/Sanford.Multimedia.Midi.XML index 8a659719..97fe04c2 100644 --- a/Sanford.Multimedia.Midi.Core/docs/Sanford.Multimedia.Midi.XML +++ b/Sanford.Multimedia.Midi.Core/docs/Sanford.Multimedia.Midi.XML @@ -324,6 +324,11 @@ A shallow copy of the Deque. + + + Gets and returns the Enumerator. + + Returns an enumerator that can iterate through the Deque. @@ -340,6 +345,21 @@ The type of elements in the list. + + + The undoable construction list. + + + + + The collection list of undoables. + + + + + The capacity list of undoables. + + Undoes the last operation. @@ -363,6 +383,136 @@ Clears the undo/redo history. + + + Searches the entire list for an element using the default comparer. + + + + + Searches the entire list for an element using a specified comparer. + + + + + Searches a range of elements in the sorted list for an element using a specified comparer. + + + + + Determines whenever the list contains the undo/redo option. + + + + + Converts all the data that is being read into the option chosen. + + + + + If the data exists and matches, it returns the value as true. + + + + + Initiates trying to find the data that matches. + + + + + Initiates trying to find all the data results that match. + + + + + Finds the index to the data. + + + + + Finds the index to the data based on the start of the index. + + + + + Finds the index to the data based on the start of the index and the count. + + + + + Searches for an element that matches the conditions defined by the T in Predicate, then returns the zero-based index of the last occurrence that matches the data if found, otherwise will be -1. + + + + + Searches for an element that matches the conditions defined by the T in Predicate and extended from the start of the index, then returns the zero-based index of the last occurrence that matches the data if found, otherwise will be -1. + + + + + Searches for an element that matches the conditions defined by the T in Predicate and extended from the start of the index and to show a specific number of options, then returns the zero-based index of the last occurrence that matches the data if found, otherwise will be -1. + + + + + Searches for an element that matches the conditions defined by T, then returns the last element that matches, otherwise it will be T by default. + + + + + Searches for the item, then returns the last occurrence of the item in the list. + + + + + Searches for the item, then returns the last occurrence of the item within the range of elements in the list. + + + + + Searches for the item, then returns the last occurrence of the item within the range of elements in the list and contains a specified number of elements and ends at the specific index. + + + + + Determines whenever every element in the list matches the conditions set by the Predicate. + + + + + Copies the elements of the list to a new array. + + + + + Sets the capacity to the actual number of elements in the list, if the number is less than a threshold value. + + + + + Adds a range of elements to insert from the list with the number of elements. + + + + + Inserts the range of elements from the list index into the undo/redo manager. + + + + + Removes a range of elements to insert from the list with the number of elements. + + + + + Reverts any added element or any removed element from the list. + + + + + Reverts any added element or any removed element from the list and shows the number of elements. + + The number of operations left to undo. @@ -373,6 +523,66 @@ The number of operations left to redo. + + + Searches for a list of undo/redo functions via an index. + + + + + Inserts the undo/redo listed options from the index. + + + + + Allows to remove the undo/redo options listed by command. + + + + + Gets or sets the undo/redo options from the list. + + + + + Adds an undo/redo option to the list of undo/redo options. + + + + + Clears an undo/redo option from the list of undo/redo options. + + + + + Copies an undo/redo option from the list to an array. + + + + + Counts the list of undo/redo options from the list. + + + + + Checks if the list is read only, and returns if it is false. + + + + + Removes an undo/redo option from the list. + + + + + Gets an enumerator and returns an enumerator that iterates through the list. + + + + + This is the main command that will test the UndoableList. + + Undoes the last operation. @@ -1503,6 +1713,36 @@ If queue is null. + + + Tests the methods in the Priority Queue. + + + + + Gets a value indicating whenever PriorityQueue is synchronized. + + + + + Gets the number of elements contained in PriorityQueue. + + + + + Copies the elements of the PriorityQueue to an array, starting at a particular array index. + + + + + Gets an object that can be used to synchronize access to the PriorityQueue. + + + + + Gets the enumerator for the Priority Queue, then returns the enumerator through a collection. + + Represents a collection of key-and-value pairs. @@ -1924,6 +2164,11 @@ Gets a value indicating whether the IClock is running. + + + Determines the number of ticks. + + Generates clock events internally. @@ -1934,6 +2179,14 @@ Initializes a new instance of the MidiInternalClock class. + + + Initializes a new instance of MidiInternalClock class with a specified base and a newly named integer. + + + The timer period in which the MidiInternalClock will use to determine the amount of time. + + Initializes a new instance of the MidiInternalClock class with the @@ -1958,11 +2211,46 @@ Stops the MidiInternalClock. + + + Sets the amount of ticks determined by the integer. + + + + + Processes with the meta message determined, along with the tempo. + + + + + Disposes of the MidiInternalClock when closed. + + Gets or sets the tempo in microseconds per beat. + + + Gets the ticks in microseconds per beat. + + + + + Initializes the Disposed event. + + + + + Initializes the Site functionality using ISite. + + + + + Performs the main Dispose functionality for when the application is closed. + + Provides basic functionality for generating tick events with pulses per @@ -1979,513 +2267,1743 @@ The minimum pulses per quarter note value. - + - Represents a MIDI device capable of receiving MIDI events. + Indicates whether the clock is running. - + - Initializes a new instance of the InputDevice class with the - specified device ID. + The PpqnClock determines how many ticks and timer period is used for the PPQN format. + + The timerPeriod integer determines the amount of time there is. + - + - Gets or sets a value indicating whether the midi events should be posted on the same synchronization context as the device constructor was called. - Default is true. If set to false the events are fired on the driver callback or the thread of the driver callback delegate queue, depending on the PostDriverCallbackToDelegateQueue property. + Gets the tempos per beat. - - true if midi events should be posted on the same synchronization context as the device constructor was called; otherwise, false. - - + - Occurs when any message was received. The underlying type of the message is as specific as possible. - Channel, Common, Realtime or SysEx. + Sets the tempos to be used per beat. - + - Gets or sets a value indicating whether the midi input driver callback should be posted on a delegate queue with its own thread. - Default is true. If set to false the driver callback directly calls the events for lowest possible latency. + Resets the amount of ticks. - - true if the midi input driver callback should be posted on a delegate queue with its own thread; otherwise, false. - - + - The exception that is thrown when a error occurs with the InputDevice - class. + Generates the amount of ticks. - + - Initializes a new instance of the InputDeviceException class with - the specified error code. + Calculates the amount of time that the timer will have. - - The error code. - - + - Gets a message that describes the current exception. + Calculates the amount of ticks per clock. - + - Represents MIDI input device capabilities. + An event that handles the ticks. - + - Manufacturer identifier of the device driver for the Midi output - device. + An event that starts the PPQN Clock. - + - Product identifier of the Midi output device. + An event that stops the PPQN Clock. - + - Version number of the device driver for the Midi output device. The - high-order byte is the major version number, and the low-order byte - is the minor version number. + An event that continues the PPQN Clock. - + - Product name. + An integer that gets and sets the PPQN Clock value. - + - Optional functionality supported by the device. + An abstract integer that gets the amount of ticks. - + - The base class for all MIDI devices. + An integer that determines the ticks per clock. + + The amount of ticks per clock. + - + - Connects a MIDI InputDevice to a MIDI thru or OutputDevice, or - connects a MIDI thru device to a MIDI OutputDevice. + This event occurs when PPQN Clock generates a tick. - - Handle to a MIDI InputDevice or a MIDI thru device (for thru - devices, this handle must belong to a MIDI OutputDevice). - - - Handle to the MIDI OutputDevice or thru device. - - - If an error occurred while connecting the two devices. - - + - Disconnects a MIDI InputDevice from a MIDI thru or OutputDevice, or - disconnects a MIDI thru device from a MIDI OutputDevice. + This event occurs when PPQN Clock is started and starts generating ticks. - - Handle to a MIDI InputDevice or a MIDI thru device. - - - Handle to the MIDI OutputDevice to be disconnected. - - - If an error occurred while disconnecting the two devices. - - + - The base class for all MIDI device exception classes. + This event occurs when PPQN Clock continues generating ticks. - + - Initializes a new instance of the DeviceException class with the - specified error code. + This event occurs when PPQN Clock has stopped. - - The error code. - - + - Represents the Windows Multimedia MIDIHDR structure. - - - - - Pointer to MIDI data. + Checks if PPQN Clock is running. - + - Size of the buffer. + Represents a MIDI device capable of receiving MIDI events. - + - Actual amount of data in the buffer. This value should be less than - or equal to the value given in the dwBufferLength member. + Initializes a new instance of the InputDevice class with the + specified device ID. - + - Custom user data. + The Input Device handler. - + - Flags giving information about the buffer. + Disposes the data when closed. - + - Reserved; do not use. + Gets or sets a value indicating whether the midi events should be posted on the same synchronization context as the device constructor was called. + Default is true. If set to false the events are fired on the driver callback or the thread of the driver callback delegate queue, depending on the PostDriverCallbackToDelegateQueue property. + + true if midi events should be posted on the same synchronization context as the device constructor was called; otherwise, false. + - + - Reserved; do not use. + Occurs when any message was received. The underlying type of the message is as specific as possible. + Channel, Common, Realtime or SysEx. - + - Offset into the buffer when a callback is performed. (This - callback is generated because the MEVT_F_CALLBACK flag is - set in the dwEvent member of the MidiEventArgs structure.) - This offset enables an application to determine which - event caused the callback. + Occurs when a short message was received. - + - Reserved; do not use. + Occurs when a channel message was received. - + - Builds a pointer to a MidiHeader structure. + Occurs when a system ex message was received. - + - Initializes a new instance of the MidiHeaderBuilder. + Occurs when a system common message was received. - + - Builds the pointer to the MidiHeader structure. + Occurs when a system realtime message was received. - + - Initializes the MidiHeaderBuilder with the specified SysExMessage. + Occurs when a invalid short message was received. - - The SysExMessage to use for initializing the MidiHeaderBuilder. - - + - Releases the resources associated with the built MidiHeader pointer. + Occurs when a invalid system ex message message was received. - + - Releases the resources associated with the specified MidiHeader pointer. + Occurs when a short message was sent. - - The MidiHeader pointer. - - + - The length of the system exclusive buffer. + Occurs when a message was received. - + - Gets the pointer to the MidiHeader. + Occurs when a channel message is received. - + - Represents MIDI output device capabilities. + Occurs when a system ex message is received. - + - Manufacturer identifier of the device driver for the Midi output - device. + Occurs when a system common message is received. - + - Product identifier of the Midi output device. + Occurs when a system realtime message is received. - + - Version number of the device driver for the Midi output device. The - high-order byte is the major version number, and the low-order byte - is the minor version number. + Occurs when an invalid short message is received. - + - Product name. + Occurs when an invalid system ex message is received. - + - Flags describing the type of the Midi output device. + Gets or sets a value indicating whether the midi input driver callback should be posted on a delegate queue with its own thread. + Default is true. If set to false the driver callback directly calls the events for lowest possible latency. + + true if the midi input driver callback should be posted on a delegate queue with its own thread; otherwise, false. + - + - Number of voices supported by an internal synthesizer device. If - the device is a port, this member is not meaningful and is set - to 0. + Creates a system ex buffer for MIDI headers. - + - Maximum number of simultaneous notes that can be played by an - internal synthesizer device. If the device is a port, this member - is not meaningful and is set to 0. + Gets the Input Device handle. - + - Channels that an internal synthesizer device responds to, where the - least significant bit refers to channel 0 and the most significant - bit to channel 15. Port devices that transmit on all channels set - this member to 0xFFFF. + Gets and sets the system ex buffer size. - + - Optional functionality supported by the device. + Determines how many Input Devices there are. - + - + Closes the MIDI input device. - + - Represents a device capable of sending MIDI messages. + Starts recording from the MIDI input device. - + - Initializes a new instance of the OutputDevice class. + Stops recording from the MIDI input device. - + - Closes the OutputDevice. + Resets the MIDI input device. - - If an error occurred while closing the OutputDevice. - - + - Resets the OutputDevice. + Initializes the MIDI input device capabilities. + + This will show the device ID for the MIDI input device. + - + - Gets or sets a value indicating whether the OutputDevice uses - a running status. + When closed, all connections to the MIDI input device are disposed. - + - The exception that is thrown when a error occurs with the OutputDevice + The exception that is thrown when a error occurs with the InputDevice class. - + - Initializes a new instance of the OutputDeviceException class with + Initializes a new instance of the InputDeviceException class with the specified error code. The error code. - + Gets a message that describes the current exception. - + - Defines constants representing the General MIDI instrument set. + Handles the MIDI Message Events. + + This provides the basic functionality for all MIDI messages. + - + - Defines constants for ChannelMessage types. + Represents MIDI input device capabilities. - + - Represents the note-off command type. + Manufacturer identifier of the device driver for the Midi output + device. - + - Represents the note-on command type. + Product identifier of the Midi output device. - + - Represents the poly pressure (aftertouch) command type. + Version number of the device driver for the Midi output device. The + high-order byte is the major version number, and the low-order byte + is the minor version number. - + - Represents the controller command type. + Product name. - + - Represents the program change command type. + Optional functionality supported by the device. - + - Represents the channel pressure (aftertouch) command - type. + The base abstract class for all MIDI devices. - + - Represents the pitch wheel command type. + Size of the MidiHeader structure. - + - Defines constants for controller types. + The main function for all MIDI devices. - + - The Bank Select coarse. + Connects a MIDI InputDevice to a MIDI thru or OutputDevice, or + connects a MIDI thru device to a MIDI OutputDevice. + + Handle to a MIDI InputDevice or a MIDI thru device (for thru + devices, this handle must belong to a MIDI OutputDevice). + + + Handle to the MIDI OutputDevice or thru device. + + + If an error occurred while connecting the two devices. + - + - The Modulation Wheel coarse. + Disconnects a MIDI InputDevice from a MIDI thru or OutputDevice, or + disconnects a MIDI thru device from a MIDI OutputDevice. + + Handle to a MIDI InputDevice or a MIDI thru device. + + + Handle to the MIDI OutputDevice to be disconnected. + + + If an error occurred while disconnecting the two devices. + - + - The Breath Control coarse. + The base class for all MIDI device exception classes. - + - The Foot Pedal coarse. + This error occurs when the header is not prepared. - + - The Portamento Time coarse. + This error occurs when the MIDI player is still playing something. - + - The Data Entry Slider coarse. + This error occurs when there are no configured instruments. - + - The Volume coarse. + This error occurs when the hardware is still busy. - + - The Balance coarse. + This error occurs when the port is no longer connected. - + - The Pan position coarse. + This error occurs when there is an invalid MIF. - + - The Expression coarse. + This error occurs when the operation is unsupported with open mode. - + - The Effect Control 1 coarse. + This error occurs when the through device is eating up a message. - + - The Effect Control 2 coarse. + This error is the last error in range. - + - The General Puprose Slider 1 + Initializes a new instance of the DeviceException class with the + specified error code. + + The error code. + - + - The General Puprose Slider 2 + Represents the Windows Multimedia MIDIHDR structure. - + - The General Puprose Slider 3 + Pointer to MIDI data. - + - The General Puprose Slider 4 + Size of the buffer. - + - The Bank Select fine. + Actual amount of data in the buffer. This value should be less than + or equal to the value given in the dwBufferLength member. - + - The Modulation Wheel fine. + Custom user data. - + - The Breath Control fine. + Flags giving information about the buffer. - + - The Foot Pedal fine. + Reserved; do not use. - + - The Portamento Time fine. + Reserved; do not use. + + + + + Offset into the buffer when a callback is performed. (This + callback is generated because the MEVT_F_CALLBACK flag is + set in the dwEvent member of the MidiEventArgs structure.) + This offset enables an application to determine which + event caused the callback. + + + + + Reserved; do not use. + + + + + Builds a pointer to a MidiHeader structure. + + + + + Initializes a new instance of the MidiHeaderBuilder. + + + + + Builds the pointer to the MidiHeader structure. + + + + + Initializes the MidiHeaderBuilder with the specified SysExMessage. + + + The SysExMessage to use for initializing the MidiHeaderBuilder. + + + + + Releases the resources associated with the built MidiHeader pointer. + + + + + Releases the resources associated with the specified MidiHeader pointer. + + + The MidiHeader pointer. + + + + + The length of the system exclusive buffer. + + + + + Gets the pointer to the MidiHeader. + + + + + Represents MIDI output device capabilities. + + + + + Manufacturer identifier of the device driver for the Midi output + device. + + + + + Product identifier of the Midi output device. + + + + + Version number of the device driver for the Midi output device. The + high-order byte is the major version number, and the low-order byte + is the minor version number. + + + + + Product name. + + + + + Flags describing the type of the Midi output device. + + + + + Number of voices supported by an internal synthesizer device. If + the device is a port, this member is not meaningful and is set + to 0. + + + + + Maximum number of simultaneous notes that can be played by an + internal synthesizer device. If the device is a port, this member + is not meaningful and is set to 0. + + + + + Channels that an internal synthesizer device responds to, where the + least significant bit refers to channel 0 and the most significant + bit to channel 15. Port devices that transmit on all channels set + this member to 0xFFFF. + + + + + Optional functionality supported by the device. + + + + + The event args class for no operations. + + + + + The function for the no operation events. + + + + + Gets and returns the data. + + + + + Represents a device capable of sending MIDI messages. + + + + + Initializes a new instance of the OutputDevice class. + + + + + When closed, disposes of the MIDI output device. + + + + + Closes the OutputDevice. + + + If an error occurred while closing the OutputDevice. + + + + + Resets the OutputDevice. + + + + + Sends the MIDI output channel device message. + + + + + Sends a system ex MIDI output device message. + + + + + Sends a system common MIDI output device message. + + + + + Gets or sets a value indicating whether the OutputDevice uses + a running status. + + + + + The exception that is thrown when a error occurs with the OutputDevice + class. + + + + + Initializes a new instance of the OutputDeviceException class with + the specified error code. + + + The error code. + + + + + Gets a message that describes the current exception. + + + + + This is an abstract class for MIDI output devices. + + + + + Handles resetting the MIDI output device. + + + + + Handles the MIDI output device short messages. + + + + + Handles preparing the headers for the MIDI output device. + + + + + Handles unpreparing the headers for the MIDI output device. + + + + + Handles the MIDI output device long message. + + + + + Obtains the MIDI output device caps. + + + + + Obtains the number of MIDI output devices. + + + + + A construct integer that tells the compiler that hexadecimal value 0x3C7 means MOM_OPEN. + + + + + A construct integer that tells the compiler that hexadecimal value 0x3C8 means MOM_CLOSE. + + + + + A construct integer that tells the compiler that hexadecimal value 0x3C9 means MOM_DONE. + + + + + This delegate is a generic delegate for the MIDI output devices. + + + + + Represents the method that handles messages from Windows. + + + + + For releasing buffers. + + + + + This object remains locked in place. + + + + + The number of buffers still in the queue. + + + + + Builds MidiHeader structures for sending system exclusive messages. + + + + + The device handle. + + + + + Base class for output devices with an integer. + + + Device ID is used here. + + + + + Disposes when it has been closed. + + + + + This dispose function will dispose all delegates that are queued when closed. + + + + + Sends the MIDI output channel device message. + + + + + Sends a short MIDI output channel device message. + + + + + Sends a system ex MIDI output channel device message. + + + + + Sends a system common MIDI output device message. + + + + + Sends a system realtime MIDI output device message. + + + + + Resets the MIDI output device. + + + + + Sends a MIDI output device message. + + + + + Initializes the MIDI output device capabilities. + + + + + Handles Windows messages. + + + + + Releases buffers. + + + + + When closed, disposes the object that is locked in place. + + + + + Handles the MIDI output device pointer. + + + + + Counts the number of MIDI output devices. + + + + + Sealed class stream for MIDI output devices. + + + + + Handles the event for no operations. + + + + + Stream for MIDI output devices. + + + + + Disposes the streams when closed. + + + + + When the application is closed, this will dispose of any streams. + + + + + Starts playing the stream. + + + + + Pauses playing the stream. + + + + + Stops playing the stream. + + + + + Resets the MIDI output device playing the stream. + + + + + Writes to the MIDI output device stream. + + + + + Writes the no operation for MIDI output device streams. + + + + + Clears out all the MIDI output device streams when done. + + + + + Initializes the amount of time for the MIDI output device stream. + + + + + Handles the messages for the MIDI output device streams. + + + + + Gets the size of the MIDI output device stream and sets the amount to be divided. + + + + + Gets the amount of tempo, then sets the tempo for the MIDI output device stream. + + + + + Defines constants representing the General MIDI instrument set. + + + + + Instrument sample: Acoustic Grand Piano. + + + + + Instrument sample: Bright Acoustic Piano. + + + + + Instrument sample: Electric Grand Piano. + + + + + Instrument sample: Honky Tonk Piano. + + + + + Instrument sample: Electric Piano 1. + + + + + Instrument sample: Electric Piano 2. + + + + + Instrument sample: Harpsichord. + + + + + Instrument sample: Clavinet. + + + + + Instrument sample: Celesta. + + + + + Instrument sample: Glockenspiel. + + + + + Instrument sample: Music Box. + + + + + Instrument sample: Vibraphone. + + + + + Instrument sample: Marimba. + + + + + Instrument sample: Xylophone. + + + + + Instrument sample: Tubular Bells. + + + + + Instrument sample: Dulcimer. + + + + + Instrument sample: Drawbar Organ. + + + + + Instrument sample: Percussive Organ. + + + + + Instrument sample: Rock Organ. + + + + + Instrument sample: Church Organ. + + + + + Instrument sample: Reed Organ. + + + + + Instrument sample: Accordion. + + + + + Instrument sample: Harmonica. + + + + + Instrument sample: Tango Accordion. + + + + + Instrument sample: Acoustic Guitar Nylon. + + + + + Instrument sample: Acoustic Guitar Steel. + + + + + Instrument sample: Electric Guitar Jazz. + + + + + Instrument sample: Electric Guitar Clean. + + + + + Instrument sample: Electric Guitar Muted. + + + + + Instrument sample: Overdriven Guitar. + + + + + Instrument sample: Distortion Guitar. + + + + + Instrument sample: Guitar Harmonics. + + + + + Instrument sample: Acoustic Bass. + + + + + Instrument sample: Electric Bass Finger. + + + + + Instrument sample: Electric Bass Pick. + + + + + Instrument sample: Fretless Bass. + + + + + Instrument sample: Slap Bass 1. + + + + + Instrument sample: Slap Bass 2. + + + + + Instrument sample: Synth Bass 1. + + + + + Instrument sample: Synth Bass 2. + + + + + Instrument sample: Violin. + + + + + Instrument sample: Viola. + + + + + Instrument sample: Cello. + + + + + Instrument sample: Contrabass. + + + + + Instrument sample: Tremolo Strings. + + + + + Instrument sample: Pizzicato Strings. + + + + + Instrument sample: Orchestral Harp. + + + + + Instrument sample: Timpani. + + + + + Instrument sample: String Ensemble 1. + + + + + Instrument sample: String Ensemble 2. + + + + + Instrument sample: Synth Strings 1. + + + + + Instrument sample: Synth Strings 2. + + + + + Instrument sample: Aah (Choir). + + + + + Instrument sample: Ooh (Voice). + + + + + Instrument sample: Synth Voice. + + + + + Instrument sample: Orchestra Hit. + + + + + Instrument sample: Trumpet. + + + + + Instrument sample: Trombone. + + + + + Instrument sample: Tuba. + + + + + Instrument sample: Muted Trumpet. + + + + + Instrument sample: French Horn. + + + + + Instrument sample: Brass Section. + + + + + Instrument sample: Synth Brass 1. + + + + + Instrument sample: Synth Brass 2. + + + + + Instrument sample: Soprano Saxophone. + + + + + Instrument sample: Alto Saxophone. + + + + + Instrument sample: Tenor Saxophone. + + + + + Instrument sample: Baritone Saxophone. + + + + + Instrument sample: Oboe. + + + + + Instrument sample: English Horn. + + + + + Instrument sample: Bassoon. + + + + + Instrument sample: Clarinet. + + + + + Instrument sample: Piccolo. + + + + + Instrument sample: Flute. + + + + + Instrument sample: Recorder. + + + + + Instrument sample: Pan Flute. + + + + + Instrument sample: Blown Bottle. + + + + + Instrument sample: Shakuhachi. + + + + + Instrument sample: Whistle. + + + + + Instrument sample: Ocarina. + + + + + Instrument sample: Lead 1 (Square). + + + + + Instrument sample: Lead 2 (Sawtooth). + + + + + Instrument sample: Lead 3 (Calliope). + + + + + Instrument sample: Lead 4 (Chiff). + + + + + Instrument sample: Lead 5 (Charang). + + + + + Instrument sample: Lead 6 (Voice). + + + + + Instrument sample: Lead 7 (Fifths). + + + + + Instrument sample: Lead 8 (Bass And Lead). + + + + + Instrument sample: Pad 1 (New Age). + + + + + Instrument sample: Pad 2 (Warm). + + + + + Instrument sample: Pad 3 (Polysynth). + + + + + Instrument sample: Pad 4 (Choir). + + + + + Instrument sample: Pad 5 (Bowed). + + + + + Instrument sample: Pad 6 (Metallic). + + + + + Instrument sample: Pad 7 (Halo). + + + + + Instrument sample: Pad 8 (Sweep). + + + + + Instrument sample: Fx 1 (Rain). + + + + + Instrument sample: Fx 2 (Soundtrack). + + + + + Instrument sample: Fx 3 (Crystal). + + + + + Instrument sample: Fx 4 (Atmosphere). + + + + + Instrument sample: Fx 5 (Brightness). + + + + + Instrument sample: Fx 6 (Goblins). + + + + + Instrument sample: Fx 7 (Echoes). + + + + + Instrument sample: Fx 8 (Sci-Fi). + + + + + Instrument sample: Sitar. + + + + + Instrument sample: Banjo. + + + + + Instrument sample: Shamisen. + + + + + Instrument sample: Koto. + + + + + Instrument sample: Kalimba. + + + + + Instrument sample: Bag Pipe. + + + + + Instrument sample: Fiddle. + + + + + Instrument sample: Shanai. + + + + + Instrument sample: Tinkle Bell. + + + + + Instrument sample: Agogo. + + + + + Instrument sample: Steel Drums. + + + + + Instrument sample: Woodblock. + + + + + Instrument sample: Taiko Drum. + + + + + Instrument sample: Melodic Tom. + + + + + Instrument sample: Synth Drum. + + + + + Instrument sample: Reverse Cymbal. + + + + + Instrument sample: Guitar Fret Noise. + + + + + Instrument sample: Breath Noise. + + + + + Instrument sample: Seashore. + + + + + Instrument sample: Bird Tweet. + + + + + Instrument sample: Telephone Ring. + + + + + Instrument sample: Helicopter. + + + + + Instrument sample: Applause. + + + + + Instrument sample: Gunshot. + + + + + Defines constants for ChannelMessage types. + + + + + Represents the note-off command type. + + + + + Represents the note-on command type. + + + + + Represents the poly pressure (aftertouch) command type. + + + + + Represents the controller command type. + + + + + Represents the program change command type. + + + + + Represents the channel pressure (aftertouch) command + type. + + + + + Represents the pitch wheel command type. + + + + + Defines constants for controller types. + + + + + The Bank Select coarse. + + + + + The Modulation Wheel coarse. + + + + + The Breath Control coarse. + + + + + The Foot Pedal coarse. + + + + + The Portamento Time coarse. + + + + + The Data Entry Slider coarse. + + + + + The Volume coarse. + + + + + The Balance coarse. + + + + + The Pan position coarse. + + + + + The Expression coarse. + + + + + The Effect Control 1 coarse. + + + + + The Effect Control 2 coarse. + + + + + The General Puprose Slider 1 + + + + + The General Puprose Slider 2 + + + + + The General Puprose Slider 3 + + + + + The General Puprose Slider 4 + + + + + The Bank Select fine. + + + + + The Modulation Wheel fine. + + + + + The Breath Control fine. + + + + + The Foot Pedal fine. + + + + + The Portamento Time fine. @@ -2882,16 +4400,226 @@ Gets the EventType. + + + The class that contains events for channel messages. + + + + + The function that contains events for channel messages. + + + + + Gets the channel messages. + + + + + This class declares invalid short message events. + + + + + Main function for when the invalid short message event is declared. + + + + + Gets and returns the message. + + + + + This class declares invalid exclusive system message events. + + + + + Main function for declared invalid exclusive system message events. + + + + + Gets and returns the message data. + + + + + Class for declaring metadata message events. + + + + + Main function for declaring metadata message events. + + + + + Gets and returns the message. + + + + + Class for MIDI events. + + + + + Gets and sets the ticks for the MIDI events. + + Raw short message as int or byte array, useful when working with VST. + + + A short message event that calculates the absolute ticks. + + + + + A short message event that uses a timestamp and calculates the absolute ticks. + + + + + A short message event that calculates the status byte, data 1 byte, data 2 byte, and absolute ticks. + + + + + Gets and returns the message. + + + + + Returns the channel message event. + + + + + Returns the common system message event. + + + + + Returns the realtime system message event. + + + + + Class for system common message events. + + + + + Main function for system common message events. + + + + + Gets and returns the message. + + + + + Class for exclusive system message events. + + + + + Main function for exclusive system message events. + + + + + Gets and returns the message. + + + + + Class for system realtime message events. + + + + + Requests the start for the system realtime message event. + + + + + Requests to continue for the system realtime message event. + + + + + Requests to stop for the system realtime message event. + + + + + Requests the clock for the system realtime message event. + + + + + Requests the ticks for the system realtime message event. + + + + + Requests the active sense for the system realtime message event. + + + + + Requests to restart for the system realtime message event. + + + + + Gets and returns the message. + + Defines constants representing MIDI message types. + + + Channel messages. + + + + + Exclusive system messages. + + + + + Common system messages. + + + + + Realtime system messages. + + + + + Metadata messages. + + + + + Short messages. + + Represents the basic functionality for all MIDI messages. @@ -3131,6 +4859,9 @@ The type of MetaMessage. + + The text string of MetaMessage. + If the MetaMessage type is not a text based type. @@ -3436,7 +5167,7 @@ Initialize a new instance of the TempoChangeBuilder class with the specified MetaMessage. - + The MetaMessage to use for initializing the TempoChangeBuilder class. @@ -3451,7 +5182,7 @@ Initializes the TempoChangeBuilder with the specified MetaMessage. - + The MetaMessage to use for initializing the TempoChangeBuilder. @@ -3534,41 +5265,94 @@ - Gets or sets the clocks per metronome click. + Gets or sets the clocks per metronome click. + + + Clocks per metronome click determines how many MIDI clocks occur + for each metronome click. + + + + + Gets or sets how many thirty second notes there are for each + quarter note. + + + + + Gets the built message. + + + + + Builds the time signature MetaMessage. + + + + + Dispatches IMidiMessages to their corresponding sink. + + + + + Handles dispatching the channel message. + + + + + Handles dispatching the system ex message. + + + + + Handles dispatching the system common message. + + + + + Handles dispatching the system realtime message. + + + + + Handles dispatching the metadata message. + + + + + Dispatches IMidiMessages to their corresponding sink. - - Clocks per metronome click determines how many MIDI clocks occur - for each metronome click. - + + The MidiEvent to dispatch. + + + The Track to dispatch. + - + - Gets or sets how many thirty second notes there are for each - quarter note. + Dispatches the channel message. - + - Gets the built message. + Dispatches the system ex message. - + - Builds the time signature MetaMessage. + Dispatches the system common message. - + - Dispatches IMidiMessages to their corresponding sink. + Dispatches the system realtime message. - + - Dispatches IMidiMessages to their corresponding sink. + Dispatches the metadata message. - - The IMidiMessage to dispatch. - @@ -3795,7 +5579,12 @@ - MidiSignal provides all midi events from an input device + MidiSignal provides all midi events from an input device. + + + + + Gets the device ID. @@ -3804,6 +5593,16 @@ + + + Disposes the input device when closed. + + + + + Initializes the MIDI events from device ID. + + All incoming midi messages in short format @@ -3839,6 +5638,56 @@ Takes a number of MidiEvents and merges them into a new single MidiEvent source + + + Gets the device ID and returns with a value of -3. + + + + + Merges the MIDI events. + + + + + Gets and returns the MIDI event sources from the events list. + + + + + Disposes of the MergeMidiEvents when closed. + + + + + Handles the event for when a MIDI message is received. + + + + + Handles the event for when a short message is received. + + + + + Handles the event for when a channel message is received. + + + + + Handles the event for when an exclusive system message is received. + + + + + Handles the event for when a common system message is received. + + + + + Handles the event for when a realtime system message is received. + + An event source that combines all possible midi events @@ -3886,11 +5735,31 @@ Event sink that sends midi messages to an output device + + + Gets the device ID and returns with a value of -1. + + + + + Initializes and registers the MIDI output device events. + + Disposes the underying output device and removes the events from the source + + + Sources and initializes the events for the MIDI output device. + + + + + Base abstract class for delta frames for MIDI messages. + + Delta samples when the event should be processed in the next audio buffer. @@ -3908,6 +5777,46 @@ realtime messages, and system common messages. + + + The maximum value for data. + + + + + The maximum value for statuses. + + + + + Bit manipulation constant for data mask. + + + + + The message is set at 0. + + + + + Gets and returns the bytes for the MIDI short message. + + + + + Main function for MIDI short messages. + + + + + Initializes the message for the MIDI short message function. + + + + + Initializes the short message based on status and two bytes of data. + + Gets the timestamp of the midi input driver in milliseconds since the midi input driver was started. @@ -3932,6 +5841,19 @@ Gets the messages's status value. + + + Gets the bytes for the MIDI short message. + + + The message for the short message. + + + + + Gets the message type and returns the message type with a short message. + + Defines constants representing the various system common message types. @@ -4083,6 +6005,29 @@ data, must have a value of 0xF0 or 0xF7. + + + Gets a byte array representation for the exclusive system message. + + + A clone of the byte array. + + + + + Copies the data to a byte array buffer and index. + + + + + Determines whenever the specified object is equal to the current object. + + + + + Returns the hash code for the current object. + + Gets the timestamp of the midi input driver in milliseconds since the midi input driver was started. @@ -4120,6 +6065,11 @@ Gets the MessageType. + + + Returns an enumerator for the exclusive system message. + + Defines constants representing the various system realtime message types. @@ -4272,24 +6222,239 @@ The ID of the note closest to the specified frequency. - + + + The class that contains the channel chaser functionality. + + + + + Handles the chased events. + + + + + The main functions for ChannelChaser. + + + + + For processing channel messages. + + + + + Chases messages to an array so that it can determine between the MIDI channel value and data value, then detect the program change messages, pitch bend messages, channel pressure messages and poly pressure messages. + + + + + Resets all the channel chaser values. + + + + + Handles the chased event. + + + + + The ChannelStopper class, which provides pedal messages and sustenuto messages. + + + + + Handles the stopped event. + + + + + This function contains the pedal messages and sustenuto messages. + + + + + Processes the channel message. + + + + + Switches all sound off when stopped. + + + + + Resets all the messages. + + + + + Handles the event when the channels are stopped. + + + + + A class for chased events. + + + + + Main function for chased events. + + + + + Gets and returns messages. + + + + + A class for stopped events. + + + + + Main function for stopped events. + + + + + Gets and returns messages. + + + + + A class for MIDI events. + + + + + Gets and returns the amount of absolute ticks. + + + + + Gets the amount of delta ticks from absolute ticks, subtracted from the previous absolute ticks, if the previous tick is not null; otherwise, obtains the amount of absolute ticks. + + + + + Gets and returns the MIDI message. + + + + + Defintes constants representing SMPTE frame rates. + + + + + 24 SMPTE Frames. + + + + + 25 SMPTE Frames. + + + + + 29 SMPTE Frames. + + + + + 30 SMPTE Frames. + + + + + The different types of sequences. + + + + + The PPQN Sequence Type. + + + + + The SMPTE Sequence Type. + + + + + Represents MIDI file properties. + + + + + MIDI File Exception handles errors relating to the application being unable to read or write to a MIDI or Sequence. + + + + + The message that will display when an error occurs with a MIDI or Sequence format + + + + + This class initializes the recording sessions. + + + + + Main function for the recording sessions. + + + + + Builds the tracks, sorts and compares between a buffer and a timestamp, then creates a timestamped message with the amount of ticks. + + + + + Removes all elements from the list. + + + + + Gets and returns the track result for the recording session. + + + + + Records a channel message if the clock is running. + + + + + Records an external system message if the clock is running. + + + + + Represents a collection of Tracks. + + + - Defintes constants representing SMPTE frame rates. + When the loading of the sequence is complete. - + - The different types of sequences. + When the loading of the sequence has changed. - + - Represents MIDI file properties. + When the sequence is saved. - + - Represents a collection of Tracks. + When the save progress for the sequence has changed. @@ -4339,6 +6504,16 @@ The MIDI file's stream. + + + Loads the sequence asynchronously. + + + + + Cancels loading the sequence asynchronously. + + Saves the Sequence as a MIDI file. @@ -4355,6 +6530,16 @@ The stream to use for saving the sequence. + + + Saves the sequence asynchronously. + + + + + Cancels saving the sequence asynchronously. + + Gets the length in ticks of the Sequence. @@ -4393,12 +6578,189 @@ Gets the Sequence's type. + + + If the loader is busy. + + + + + Adds an item to the sequence. + + + + + Removes all items from the sequence. + + + + + Determines whenever the sequence contains a specific value. + + + true, if the item is found in the sequence. Otherwise, it'll be false. + + + + + Copies the elements of the sequence to an array, starting at a particular array index. + + + + + Gets the number of elements contained in the sequence. + + + The number of elements in the sequence. + + + + + Gets a value indicating whenever the sequence is read-only. + + + true, if the sequence is read-only; otherwise, false. + + + + + Removes the first occurrence of a specific object from the sequence. + + + true, if the item was successfully removed from the sequence; otherwise false. This method also returns false if the item is not found in the original sequence. + + + + + Returns an enumerator that iterates through the sequence. + + + + + Handles disposing of the sequence when the application is closed. + + + + + Gets or sets the site associated with the sequence. + + + + + Disposes the load when the application is closed. + + + + + This sequencer class allows for the sequencing of sequences. + + + + + Handles the event when the sequencer has finished playing the sequence. + + + + + Handles the event when a channel message is displayed when a sequence is played. + + + + + Handles the event when a system ex message is displayed when a sequence is played. + + + + + Handles the event when a metadata message is displayed when a sequence is played. + + + + + Handles the chased event in the sequencer. + + + + + Handles the event when sequencer stops playing. + + + + + The main sequencer function. + + + + + The function in which checks if the sequencer has been disposed. + + + + + The method for disposing the sequencer when the application is closed. + + + + + Starts the sequencer. + + + + + Continues the sequencer. + + + + + Stops the sequencer. + + + + + Handles the event for when the sequencer is finished playing. + + + + + Handles the event for when the sequencer is disposed. + + + + + The sequencer's playing position of the sequence. + + + + + The loaded sequence that represents a series of tracks. + + + + + Handles the disposed event. + + + + + Gets the site and sets the site with a value. + + + + + The dispose function for when the application is closed. + + Represents a collection of MidiEvents and a MIDI track within a Sequence. + + + Main function that represents the end of track MIDI event. + + Inserts an IMidiMessage at the specified position in absolute ticks. @@ -4444,6 +6806,11 @@ The MidiEvent at the specified index. + + + A MIDI event that moves the track. + + Gets the number of MidiEvents in the Track. @@ -4461,114 +6828,239 @@ - Gets an object that can be used to synchronize access to the Track. + Gets an object that can be used to synchronize access to the Track. + + + + + Main function for the track iterator. + + + + + Dispatches the track iterator. + + + + + A track iterator for the amount of ticks. + + + + + Tests the tracks. + + + + + Reads a track from a stream. + + + + + Writes a Track to a Stream. + + + + + Gets or sets the Track to write to the Stream. + + + + + This provides the functionality for the timer. + + + + + Gets a value indicating whether the Timer is running. + + + + + Gets the timer mode. + + + If the timer has already been disposed. + + + + + Period between timer events in milliseconds. + + + + + Resolution of the timer in milliseconds. + + + + + Gets or sets the object used to marshal event-handler calls. + + + + + Occurs when the Timer has started; + + + + + Occurs when the Timer has stopped; + + + + + Occurs when the time period has elapsed. + + + + + Starts the timer. + + + The timer has already been disposed. + + + The timer failed to start. + + + + + Stops timer. + + + If the timer has already been disposed. + + + + + Replacement for the Windows multimedia timer that also runs on Mono + + + + + Gets or sets the object used to marshal event-handler calls. + + + + + Queues and executes timer events in an internal worker thread. + + + + + The thread to execute the timer events + + + + + Defines constants representing the timing format used by the Time struct. + + + + + Defined in milliseconds. + + + + + Defined in samples. - + - Reads a track from a stream. + Defined in bytes. - + - Writes a Track to a Stream. + Defined in SMPTE. - + - Gets or sets the Track to write to the Stream. + Defined in MIDI. - + - Gets a value indicating whether the Timer is running. + Defined in ticks. - + - Gets the timer mode. + Represents the Windows Multimedia MMTIME structure. - - If the timer has already been disposed. - - + - Period between timer events in milliseconds. + Type. - + - Resolution of the timer in milliseconds. + Milliseconds. - + - Gets or sets the object used to marshal event-handler calls. + Samples. - + - Occurs when the Timer has started; + Byte count. - + - Occurs when the Timer has stopped; + Ticks. - + - Occurs when the time period has elapsed. + SMPTE hours. - + - Starts the timer. + SMPTE minutes. - - The timer has already been disposed. - - - The timer failed to start. - - + - Stops timer. + SMPTE seconds. - - If the timer has already been disposed. - - + - Replacement for the Windows multimedia timer that also runs on Mono + SMPTE frames. - + - Gets or sets the object used to marshal event-handler calls. + SMPTE frames per second. - + - Queues and executes timer events in an internal worker thread. + SMPTE dummy. - + - The thread to execute the timer events + SMPTE pad 1. - + - Defines constants representing the timing format used by the Time struct. + SMPTE pad 2. - + - Represents the Windows Multimedia MMTIME structure. + MIDI song position pointer. @@ -4601,6 +7093,11 @@ Maximum supported period in milliseconds. + + + The default timer capabilities. + + Represents the Windows multimedia timer. @@ -4889,11 +7386,179 @@ Public integer for the error code. + + + This will handle any errors relating to Sanford.Multimedia. + + + + + This represents the error itself. + + + + + Displays the error. + + + The error that is associated with the issue. + + Defines constants for all major and minor keys. + + + The A♭ (A-Flat) Minor sequenced note. + + + + + The E♭ (E-Flat) Minor sequenced note. + + + + + The B♭ (B-Flat) Minor sequenced note. + + + + + The F Minor sequenced note. + + + + + The C Minor sequenced note. + + + + + The G Minor sequenced note. + + + + + The D Minor sequenced note. + + + + + The A Minor sequenced note. + + + + + The E Minor sequenced note. + + + + + The B Minor sequenced note. + + + + + The F♯ (F-Sharp) Minor sequenced note. + + + + + The C♯ (C-Sharp) Minor sequenced note. + + + + + The G♯ (G-Sharp) Minor sequenced note. + + + + + The D♯ (D-Sharp) Minor sequenced note. + + + + + The A♯ (A-Sharp) Minor sequenced note. + + + + + The C♭ (C-Flat) Major sequenced note. + + + + + The G♭ (G-Flat) Major sequenced note. + + + + + The D♭ (D-Flat) Major sequenced note. + + + + + The A♭ (A-Flat) Major sequenced note. + + + + + The E♭ (E-Flat) Major sequenced note. + + + + + The B♭ (B-Flat) Major sequenced note. + + + + + The F Major sequenced note. + + + + + The C Major sequenced note. + + + + + The G Major sequenced note. + + + + + The D Major sequenced note. + + + + + The A Major sequenced note. + + + + + The E Major sequenced note. + + + + + The B Major sequenced note. + + + + + The F♯ (F-Sharp) Major sequenced note. + + + + + The C♯ (C-Sharp) Major sequenced note. + + Defines constants representing the 12 Note of the chromatic scale. @@ -5017,6 +7682,26 @@ Gets the owner of this AsyncResult object. + + + This object provides the async state. + + + + + This handles the waiting time for the async. + + + + + Determines whenever the async completed synchronously or not. + + + + + Determines if the async has completed. + + Represents an asynchronous queue of delegates. @@ -5052,6 +7737,16 @@ The IContainer to which the DelegateQueue will add itself. + + + Checks if DelegateQueue has been disposed. + + + + + Disposes of DelegateQueue when closed. + + Executes the delegate on the main thread that this object executes on. @@ -5146,6 +7841,21 @@ The object passed to the delegate. + + + Raises the InvokeCompleted event. + + + + + Raises the PostCompleted event. + + + + + Raises the Disposed event. + + Dispatches a synchronous message to this synchronization context. @@ -5264,6 +7974,21 @@ Disposes of the DelegateQueue. + + + This class is used when the async events have been completed. + + + + + Main function for post completed events. + + + + + Gets and returns the callback. + + Provides functionality for timestamped delegate invocation. @@ -5290,6 +8015,16 @@ specified IContainer. + + + Checks if the DelegateScheduler has been disposed. + + + + + Disposes of DelegateScheduler when closed. + + Adds a delegate to the DelegateScheduler. @@ -5352,6 +8087,16 @@ If the DelegateScheduler has already been disposed. + + + Raises the Disposed event. + + + + + Raises the InvokeCompleted event. + + Gets or sets the interval in milliseconds in which the @@ -5369,10 +8114,95 @@ Gets or sets the object used to marshal event-handler calls and delegate invocations. + + + When the event has been disposed. + + + + + Gets and returns the site, sets the site with a value. + + + + + The main dispose function that occurs when disposed. + + + + + Indicates the tasks to be compared. + + + + + Initializes returns the arguments. + + + + + Gets and returns the next timeout. + + + + + Gets and returns the count. + + + + + Gets and returns the method. + + + + + Gets and returns the timeout in milliseconds. + + + + + Compares the current instance with another object of the same type and returns an integer indicates whenever the current instance precedes, follows, or occurs in the same position in the sort order as the other object. + + + Compares between the subtracted next timeout and the task. + + Represents information about the InvokeCompleted event. + + + Represents the delegate, objects and exceptions for the InvokeCompleted event. + + + Represents the delegate method used. + + + For any args to be used. + + + For any results that occur. + + + For any errors that may occur. + + + + + Initializes the args as an object. + + + + + Initializes method as a delegate. + + + + + Initializes result as an object. + +

Overview

Migration to PackageReference was completed successfully. Please build and run your solution to verify that all packages are available.
- Changed files and this report have been backed up here: - C:\Users\Silve\Source\Repos\VGMusicStudio\MigrationBackup\655cc60b\VG Music Studio

Packages processed

Top-level dependencies:

Package IdVersion
EndianBinaryIO - v1.1.2
Microsoft.Win32.Registry - v5.0.0
Microsoft.Win32.Registry.AccessControl - v5.0.0
Microsoft.Win32.SystemEvents - v5.0.0
Microsoft.Windows.Compatibility - v5.0.2
Microsoft.WindowsAPICodePack-Core - v1.1.0.2
Microsoft.WindowsAPICodePack-Shell - v1.1.0
NAudio - v2.0.1
NAudio.Asio - v2.0.0
NAudio.Core - v2.0.0
NAudio.Midi - v2.0.1
NAudio.Wasapi - v2.0.0
NAudio.WinForms - v2.0.1
NAudio.WinMM - v2.0.1
ObjectListView.Official - v2.9.1
System.CodeDom - v5.0.0
System.ComponentModel.Composition - v5.0.0
System.Configuration.ConfigurationManager - v5.0.0
System.Data.DataSetExtensions - v4.5.0
System.Data.Odbc - v5.0.0
System.Data.OleDb - v5.0.0
System.Data.SqlClient - v4.8.1
System.Diagnostics.EventLog - v5.0.1
System.Diagnostics.PerformanceCounter - v5.0.1
System.DirectoryServices - v5.0.0
System.DirectoryServices.AccountManagement - v5.0.0
System.DirectoryServices.Protocols - v5.0.0
System.Drawing.Common - v5.0.0
System.IO - v4.3.0
System.IO.FileSystem.AccessControl - v5.0.0
System.IO.FileSystem.Primitives - v4.3.0
System.IO.Packaging - v5.0.0
System.IO.Pipes.AccessControl - v5.0.0
System.IO.Ports - v5.0.0
System.Management - v5.0.0
System.Reflection.Emit - v4.7.0
System.Reflection.Emit.ILGeneration - v4.7.0
System.Reflection.Emit.Lightweight - v4.7.0
System.Runtime - v4.3.0
System.Runtime.Caching - v5.0.0
System.Runtime.CompilerServices.Unsafe - v5.0.0
System.Security.AccessControl - v5.0.0
System.Security.Cryptography.Algorithms - v4.3.1
System.Security.Cryptography.Cng - v5.0.0
System.Security.Cryptography.Encoding - v4.3.0
System.Security.Cryptography.Pkcs - v5.0.1
System.Security.Cryptography.Primitives - v4.3.0
System.Security.Cryptography.ProtectedData - v5.0.0
System.Security.Cryptography.Xml - v5.0.0
System.Security.Permissions - v5.0.0
System.Security.Principal.Windows - v5.0.0
System.ServiceModel.Duplex - v4.8.0
System.ServiceModel.Http - v4.8.0
System.ServiceModel.NetTcp - v4.8.0
System.ServiceModel.Primitives - v4.8.0
System.ServiceModel.Security - v4.8.0
System.ServiceModel.Syndication - v5.0.0
System.ServiceProcess.ServiceController - v5.0.0
System.Speech - v5.0.0
System.Text.Encoding.CodePages - v5.0.0
System.Threading.AccessControl - v5.0.0
YamlDotNet - v11.2.1

Transitive dependencies:

Package IdVersion
- No transitive dependencies found. -

Package compatibility issues

Description
- No issues were found. -