From cdad61d2bfc4af977b74c7d85467f6a625868f65 Mon Sep 17 00:00:00 2001 From: bstrr Date: Fri, 28 Dec 2012 02:05:33 +0100 Subject: [PATCH] Merge branch 'master' of https://github.com/kusma/rocket (squashing them altogether). Squashed commits: appveyor: build lib and examples Make sure we build both in Release and Debug Client modes, to cover a few combinations of build flags. (+174 squashed commits) Squashed commits: [19fd992] appveyor: x86 -> Win32 [b1dd199] vs2013: add project files While we're at it, rename VS2008 project files to *.vs2008.* also. [fd20f69] update .gitignore [cee4008] editor: eliminate x64 size_t <-> int warnings [042505a] README: add travis badge In addition to appveyor, let's also display the build status for travis-ci for the Linux and OSX builds. [542e2f1] add a simple travis config Build both libs and editor on Linux and OSX. No support for uploading builds yet, though. [86b9401] appveyor: clean up build-version number We don't want build-numbers to be confused with release-version numbers. So let's just make the appveyor-version be the build-number. [ca8bb0b] README: add build-status Since we're now using AppVeyor.com as a CI service for Windows, let's show the build-status. [a26d79e] add appveyor build-config This allows us to do automated builds of the editor using AppVeyor.com, and will package up the built editor on success. This allows people to download prebuilt editors instead of having to install Qt and all the jazz. [7054ce4] Compile fix: warnings for VS2012 (+x64) and up. [84defcb] example_bass: silence OSX deprecation warning Apple has marked gluPerspective and gluLookAt as deprecated, in favour of GLKit's similar functionality. So to prevent needless warnings, let's wrap those into an interface similar to the old GLU functions, so we can keep the code portable. [cf4b7c4] player: use ipv6 on linux and osx Linux and OSX both support IPv6, so let's enable it on both. [fc6828e] player: support IPv6 For IPv6, we need to use getaddrinfo instead of gethostbyname. This also fixes some deprecation-warnings on VS2015, so yay. Thanks to Niels J. de Wit for help with testing. [9794849] player: disable MSVCRT deprecations and security warnings Not everyone use our project files for building on MSVC, so let's make sure these are always defined. This also show that we forgot to define _CRT_NONSTDC_NO_DEPRECATE in some build-configurations, leading to the need for the strdup-hack. [579b073] player: prevent annoying warning on VS2012 and newer Visual Studio 2012 and newer seems to have a peculiarity with it's warning-generation for it's "strdup is reserved, use _strdup instead"-warning, where it warns even if we use _strdup through a macro called strdup. This seems a bit nasty to fix at the core, but it seems we can just side-step the issue alltogether, by using the NEED_STRDUP macro to define our own version instead. This has the side-effect of us using one less CRT function, which the intro-people may or may not be happy about. But let's let them weigh in on that if needed. [00e4a4f] player: only define snprintf on older CRTs Visual Studio 2012 and up provides snprintf under it's real name, and Visual Studio 2015 does not allow redefining snprintf. So let's just use the real name in those cases. [177b837] player: num_keys is int, not size_t num_keys is int, but is attempted read and written as size_t. This breaks pretty badly on 64-bit architectures, so let's write it as the integer it is. [ddb8b89] fix missing newline in .gitignore [3b71d6e] Gitignore for build directory under windows [1696b59] README: update notes about compiling the example This section was based on building the editor, but after the editor was ported to Qt, this description got out-of-date. Update it so it doesn't relate to qmake. [719884f] editor: resolve document once Mocking around with the document-getter many times in a method is just nasty. Get it once, and work on a local reference instead. [96502ee] editor: do not care about index when creating tracks The only thing we want to do at this point is to get the track itself, so let's go directly to the target. [93367c4] editor: add and use invalidateAll helper TrackView::invalidateAll is a bit different from viewport()->update() in that it doesn't invalidate the track-names nor row-numbers. So let's do that to get some more speed as well. [91810aa] editor: tighter invalidation A lot of these were written by a lazy coder, not bothering to use the proper bounds when invalidating. Let's remedy that. [12964bf] editor: simplify TrackView::invalidateTrack() It can be implemented in terms of invalidateRange, so let's just do that, as it's simpler. [73d4180] README -> README.md By using Markdown instead of simple text, we can get hyperlinks and other nice features on the GitHub frontpage. So yeah, let's do that. [e1100b7] TcpSocket doesn't need to know about the transport [32e7158] player: set sin_zero to 0 sin_zero is required to be cleared to zero on some architectures, so let's just follow what the spec says. Noticed by Coverity. [b0b7ab3] README: fix typo [9cf1a20] editor: create helpers for processing commands [2356e2e] editor: set constant stuff in TrackView constructor [4cb3d93] example_bass: use -framework OpenGL on OSX When linking on OS X, we need to use -framework OpenGL rather than the Unix-style -lGL -lGLU when linking to OpenGL. Not sure why, but yeah. This seems to work. [deea924] example_bass: only avoid SDLmain on Windows [d563701] example_bass: detect error in SDL_SetVideoMode [8fb8e70] editor: set application icon on mac contrib/logo.svg was simply converted using the online converter at iconverticons.com [b6d00d3] editor: separate between logical and physical coordinates [04ea66e] editor: per-pixel horizontal scrolling This feels a bit nicer. [c1f7aed] editor: make invalidateTrack invalidate track-names Previously, invalidateTrack only invalidated the track-data, but this isn't correct. Some other invalidation seems to have kept this problem under control, but this is about to change... [4254149] editor: do not track windowTracks [f53bfc1] editor: do not track width/height of TrackArea We can ask the viewport about it instead. [3d2d284] editor: use getTrackFromX helper Instead of sprinkling the knowledge of what apears where aound in the code, let's use a helper instead. This way, we can more easily change widths of single tracks. [e781e90] editor: use Qt's undo framework Instead of hand-rolling our own systen, let's use Qt's built-in one to reduce the amount of code. [dc12bf0] editor: store everything needed in undo-commands [6f9b867] editor: move document-implementation into source file [cab6f83] editor: use QRect for selection [e416b69] editor: simplify painting [eeb9425] editor: factor out interpolation-pen selection [d6d72b2] editor: factor out left-margin painting [b269493] editor: use QLineEdit instead of hand-rolling one We're editing a line of text, so it makes sense to use a QLineEdit instead of hacking in editing into the control ourselves. This gives the users a few neat extra-features, like copy-paste and undo/redo on the editing itself, yay! Work around QTBUG-4045, by stripping away group-separators. [270b06d] editor: factor out the painting of a single track [9acf92a] editor: factor out polynomial helper [0176377] editor: mark main-window as modified if document is modified [4a5dea4] editor: prevent editing when playing [df0ebc3] editor: stop snooping in the players headers We can't really change these due to alternative clients/editors anyway. So there's no added maintainance in duplicating these definitions - let's just do it to avoid having to care about source-level interopability. While we're at it, move the player-definitions to the only place using them. [f13d06c] player: remove sync_data structure the separation between sync_data and sync_device was only needed due to the reuse of the sync-data code in the editor. This isn't the case any more, so let's get rid of it. [0b2f29d] editor: use signals for internal communications Instead of all these objects having to know about each other, use Qt's signal/slot mechanism to notify each other. This allows us to unmangle some dependencies, yay! [c6f8765] editor: implement own structures There's plenty of good reasons to keep the data-structures separate between the editor and the player. For instance, it allows us to move them in different directions, making the editor-concepts more high-level and the player-concepts more low-level. Some ideas: - Switch to cubic polynomials on the wire and in the player. This allows us to add more curve-types in the editor, without having to bother alternative implementations of the player with the details. - Store multiple values "per row" in the editor to support incoming/outgoing values. This is useful for sudden changes at low row-per-beat settings. - Use signals/slots per track in the editor to communicate changes to client. Most of these ideas could probably be done without this change, but they certainly become easier with. [9b9c7e1] README: update mailing list We've moved to Google Groups, so let's have the readme-file point people in the right direction. [b029047] editor: superficial code-style clean-ups This should be entirely non-functional changes. [5ae18cc] editor: make space enter value Previously we were discarding it. But let's be consistent with the logic of the other keys instead. [0e59993] player: only include socket-stuff for client-builds While we're at it, move this to device.h instead of base.h to reduce global complexity a bit. [d6c7297] player: move stuff out from base.h These are only really used in device.c, so let's just move these helpers there. [e13402e] editor: pimp the statusbar Use OS-default layout, by adding permanent widgets and setting the message separately. Also go for the OS-default look, by not styling the panels. [2b6b107] editor: do not paint interpolation-mode into the previous row [2a1db67] player: build non-client variant from makefile [95f2dc9] player: do not emulate inline as static All our inline functions are already declared as static inline, and multiple static declarations gives compiler errors. So let's just define inline empty. [1dbe74f] editor: do not autoscroll with zero-width trackview This fixes a glitch where the edit-track is invisible when loading documents from the command line. [88858ea] licensing: do not track contributors Tracking indevidual contributors in the source-code is a fool's errand, and the code-repository already does this much more accurately. Replace individual names with "Contributors" to avoid having to keep track. We were already missing alot of people anyway. [2575a23] licensing: remove copyright statements from code Keeping these up-to-date is a pain, and it's strictly speaking not required. So let's just drop it, the git repository contains much more accurate author-information. [239982c] update .gitignore [e14fd62] editor: handle font-changes the same way as palette-changes [698cd56] editor: handle palette-changes [0e8a5fe] editor: use Qt's built-in window-title management This allows each platform to get the title styled in it's traditional way. Unfortunately, we also need to work around QTBUG-16507 for this to work properly. [5fc6a7e] editor: use icons from theme, if available [aec19fc] editor: use QKeySequence::StandardKey Each target-platform has it's own set of default key-bindings. Use QKeySequence::StandardKey to get the OS-default behaviour. [fcf4c89] player: add cast to quiet warning GCC isn't extatic about these implicit casts, due to FILE* -> void*. So let's be explicit to shut the compiler up a bit. [7ce91f2] player: include stddef for size_t [07bde3b] editor: remove recent-files that fail to load [5439866] editor: support shrinking the recent-files list [9e0af84] editor: create recent-file setter Lift the logic out of MainWindow::setCurrentFileName, so it can easily be reused. [89ea468] editor: make save-before-exit dialog a child of mainwindow [5bcce67] editor: do not swap tracks with ctrl+tab [fc14ecd] editor: fix shift+tab navigation This was broken when ported to Qt, as Qt has a separate key code for shift-tab (Key_Backtab). When using that instead, we also get the opportunity to clean up that nasty fakeEvent-hack. Yay. [aaf7862] player: close socket on handshake-mismatch [0ade8ba] editor: paint utf-8 track-names properly [d45e8a6] editor: prefer standard header-guards Pragmas are language extensions, so let's not depend on them. Use "normal" header-guards instead. [eadf051] editor: add missing include-guard [e46dcaf] editor: remove needless includes Now we only depend on Qt for the editor. No standard-lib includes at all. [4ea2d68] editor: use Q_ASSERT instead of assert [b65b9b6] editor: use QMap for clientTracks [b5cde44] editor: fixup a cast [d599ac4] editor: QString for track-names [01309e1] editor: use QByteArrays for WebSocket upgrade [10b2ff9] editor: use qMin/qMax/qBound [315c0ab] editor: use QByteArray for WebSocket buffer Instead of using std::string, let's use QByteArray. While we're at it, change some size_t's to int to avoid casts. [ef20023] editor: use QVector rather than std::vector Using the same implementation on all platforms leads to less platform-surprises. [15dfe6e] Bugfix TrackView::editPaste() to not read data from a dead object mimeData->data(...) returns a QByteArray. Previously this object was not stored anywhere, but instead a pointer to this array's raw data was obtained. As the returned QByteArray was destructed immediately after the pointer was fetched, the clipbuf pointer started to point to an unused memory area. This caused paste to crash when the memory area was overwritten by other parts of the program and garbage CopyEntry objects were read. [c793808] editor: use QByteArray instead of std::string While we're at it, clean up some cruft. [d4dc37d] editor: remove superfluous waitForReadyRead We already wait if we fail on the first getChar()-call, so there's no point in doing this here. [7b8daec] editor: wait for data in a loop The first packet of data might not be sufficient for our needs, so make sure we wait until enough data has arrived. If an error occurs, break out and let the reading code-do the clean-up. [4b780b6] Wait for greetings data to arrive Previously, if greetings data was not immediately readable from the opened socket, the connection failed because only partial greetings were received. [03f229a] editor: clean up includes a tad [09d72b4] editor: whoops, fix error-path [cb4e62a] editor: remove exceptions from xml loading/saving While we're at it, remove the double-reporting of failures. [f44fa14] editor: use QApplication::arguments instead of argc/argv Qt can take some debug-flags from the command-line. But if we also parse the same command-line, we'll get confused about what those flags were. So let's look at QApplication::arguments instead, as it has those flags removed. [fccf4bc] editor: use QList for bookmarks Keep a sorted list of bookmarks, using q(Upper/Lower)Bound to access it. This is a perfectly reasonable way of doing row-bookmarks without needing std::set. [9e4cd49] editor: fix valgrind-warning The initialization of the trackview did calculation based on undefined values, which gave a Valgrind warning. In this case it wasn't dangerous - the correct values would be recalculated once the widget got resized as a response to becoming visible. Let's just initialize these values to keep Valgrind happy. [dea6659] editor: prefer qt-ism over stl-ism Unfortunately, QSet doesn't provide upper/lower bounds, so let's keep using std::set<> for bookmarks for now. [becb073] editor: save settings to the right place on win32 [f0efcbb] DotRocket: remove superfluous semicolons [2da1435] DotRocket: clean up brackets [e563572] DotRocket: remove pointless forward-decls [7133f18] DotRocket: remove pointless cast [58d0985] DotRocket: put DeviceReference in the anonymous namespace [13d2d9e] DotRocket: mark wrapper-functions as static [c0e3d68] DotRocket: thread-safe callback-handling Using gcroot instead should be thread-safe, by creating a native wrapper-object that holds a reference, and using the data-channel already present. [d9e5862] DotRocket: silence a warning While we're at it, rename a variable that was a victim of copypasta. [15a7444] DotRocket: return double instead of float [cab4bb0] player: do not redefine NOMINMAX if defined It seems some versions of MinGW define NOMINMAX in the Windows headers. To prevent redefinition-warnings, only define it if not previously defined. [8f6b469] editor: use Qt's sized integer instead of stdint.h We get stdint.h included though include/base.h, but it's slightly cleaner to not make this assumption. Qt already provides suitable typedefs. [d430ca7] makefile: clean example_bass executable [459dc04] makefile: do not remove editor/Makefile twice [297e12e] makefile: run qmake from the editor-subdir Some versions/installations of qmake seems to have issues with running from a different director than the .pro file is located in when compiling resources, giving an error like: : File does not exist 'editor.qrc' So let's just be straight forward and run qmake from the editor subdir to side-step this. [6cef381] update .gitignore [9e7133b] makefile: build editor by default [27d484a] makefile: do not delete pass -rf to $(RM) The $(RM) macro already contains -f to prevent errors when a file doesn't exits. And this list does not include directories, so -r is just pointless. [57ae795] makefile: mark all and clean as phony [3f26cba] editor: fix gcc-warnings properly [c9c5832] editor: use Qt for endian-conversions [fdc6759] README: clarify that qmake is for the editor [a8cd496] editor: move waitForReadyRead into ClientSocket::recv We want our recv method to act like a blocking call, so let's wait if there's not sufficient data available. [72fb1f2] Prevent dereferencing NULL pointer in TcpSocket.disconnect [c37690a] Close socket properly when a client disconnects [2b4ceb0] Fix socket to wait for data after WebSocket upgrade [38f29a8] editor: move server-code to signals and slots This should make the editor spend less CPU time. [9d541fc] editor: port network-code to Qt [cee7ee9] editor: fixup recentfiles [365c943] editor: only invalidate tracks if edit-track changed [722c0f4] editor: port to Qt [d6bb103] editor: fix int/size_t mismatch As the editor currently only builds on 32-bit Windows, this is not a real-world problem, as int and size_t are the same size. However, this is about to change... [a0e8c24] editor: fix member initialization order [f83c802] editor: clean up statusbar-updates [9fea725] editor: clean up input [baa72fe] editor: wrap and merge row/track changed events [5c7ade5] editor: wrap up current-value dirtying [c3925a8] editor: drop custom window events [e5ec393] editor: create a helper to set statusbar-text [53ccde4] editor: use socket_poll-helper [b037174] editor: move invalidation to helpers Make it look like Qt to keep things simpler [312645c] player: fix gcc warnings [b962b71] editor: enable visual styles To get a modern window, it isn't enough to simply state that some manifest-dependencies exits, the manifest must be generated and shipped as well. [6ec866a] editor: clean up project [c0a7374] rename sync_player.lib to librocket[-player][d].lib This makes the UNIX version and the Windows version a bit more similar, and allows a single batch-build without any copying to create a complete binary release of the libs. [eaa1f79] move librocket-stuff to lib-folder While we're at it, move the sync_player.vcproj to lib/librocket.vcproj as well. [a82cb04] linux: build example_bass directly in it's source dir That's where it's data-files are anyway. [17ca559] add a gitignore-file [6633249] move library to the lib-folder [1616424] example: quiet warnings [30c66d6] README: remove hyphen from "feed-back" [b4c22e2] spelling: "key-frame" -> "key frame" [cb9b964] README: fix broken alignment [95f0bd0] editor: rename awkwardly named method Rrename SyncDocument::getTrackOrderCount to getTrackCount. While we're at it, move the implementation of some methods to the source file. [6e2be92] editor: remove commented-out code [43b401e] editor: move horizontally with tab / shift-tab Muscle memory is hard to overcome; support tracker-style tab-movement. [c77bbac] editor: do not pass a string to std::bad_alloc ctor The C++ standard does not contain a definition of std::bad_alloc that takes a string, this is something that older versions of Visual Studio did as an extension. This is no longer available in Visual Studio 2012, so let's just not pass any string to it. Thanks to Xeche for noticing. [2f921bc] sync: do not redefine inline as __inline for C++ Since the editor includes base.h, and the editor is written in C++, checking for C99-support is not the right thing to do. So let's just keep inline as inline in such cases. This is needed when building the editor with Visual Studio 2012. Thanks to Xeche for noticing. [0e0dd68] editor: implement websocket close [9b301cc] editor: implement websocket ping-pong [6573ad4] editor: implement websocket support [2cf28fb] editor: read a full line for the initial greeting Stop either at newlines or after an exclamation mark, so we can parse HTTP GET-requests for WebSockets. [03d48c2] editor: remove unused flags [9bf3489] editor: factor out tcp-code to a separate class [c85e9f1] editor: barf on malformed track-name [f892db9] Update sync/device.c size_t is not necessarily an int (on OSX it isn't), so reading back sizeof(size_t) as opposed to sizeof(int) won't work. I take it it's a copy/paste bug :) player: build non-client variant from makefile editor: do not paint interpolation-mode into the previous row editor: pimp the statusbar Use OS-default layout, by adding permanent widgets and setting the message separately. Also go for the OS-default look, by not styling the panels. player: move stuff out from base.h These are only really used in device.c, so let's just move these helpers there. player: only include socket-stuff for client-builds While we're at it, move this to device.h instead of base.h to reduce global complexity a bit. editor: make space enter value Previously we were discarding it. But let's be consistent with the logic of the other keys instead. editor: superficial code-style clean-ups This should be entirely non-functional changes. README: update mailing list We've moved to Google Groups, so let's have the readme-file point people in the right direction. editor: implement own structures There's plenty of good reasons to keep the data-structures separate between the editor and the player. For instance, it allows us to move them in different directions, making the editor-concepts more high-level and the player-concepts more low-level. Some ideas: - Switch to cubic polynomials on the wire and in the player. This allows us to add more curve-types in the editor, without having to bother alternative implementations of the player with the details. - Store multiple values "per row" in the editor to support incoming/outgoing values. This is useful for sudden changes at low row-per-beat settings. - Use signals/slots per track in the editor to communicate changes to client. Most of these ideas could probably be done without this change, but they certainly become easier with. editor: use signals for internal communications Instead of all these objects having to know about each other, use Qt's signal/slot mechanism to notify each other. This allows us to unmangle some dependencies, yay! player: remove sync_data structure the separation between sync_data and sync_device was only needed due to the reuse of the sync-data code in the editor. This isn't the case any more, so let's get rid of it. editor: stop snooping in the players headers We can't really change these due to alternative clients/editors anyway. So there's no added maintainance in duplicating these definitions - let's just do it to avoid having to care about source-level interopability. While we're at it, move the player-definitions to the only place using them. editor: prevent editing when playing editor: mark main-window as modified if document is modified editor: factor out polynomial helper editor: factor out the painting of a single track editor: use QLineEdit instead of hand-rolling one We're editing a line of text, so it makes sense to use a QLineEdit instead of hacking in editing into the control ourselves. This gives the users a few neat extra-features, like copy-paste and undo/redo on the editing itself, yay! Work around QTBUG-4045, by stripping away group-separators. editor: factor out left-margin painting editor: factor out interpolation-pen selection editor: simplify painting editor: use QRect for selection editor: move document-implementation into source file editor: store everything needed in undo-commands editor: use Qt's undo framework Instead of hand-rolling our own systen, let's use Qt's built-in one to reduce the amount of code. editor: use getTrackFromX helper Instead of sprinkling the knowledge of what apears where aound in the code, let's use a helper instead. This way, we can more easily change widths of single tracks. editor: do not track width/height of TrackArea We can ask the viewport about it instead. editor: do not track windowTracks editor: make invalidateTrack invalidate track-names Previously, invalidateTrack only invalidated the track-data, but this isn't correct. Some other invalidation seems to have kept this problem under control, but this is about to change... editor: per-pixel horizontal scrolling This feels a bit nicer. editor: separate between logical and physical coordinates editor: set application icon on mac contrib/logo.svg was simply converted using the online converter at iconverticons.com example_bass: detect error in SDL_SetVideoMode example_bass: only avoid SDLmain on Windows example_bass: use -framework OpenGL on OSX When linking on OS X, we need to use -framework OpenGL rather than the Unix-style -lGL -lGLU when linking to OpenGL. Not sure why, but yeah. This seems to work. editor: set constant stuff in TrackView constructor editor: create helpers for processing commands README: fix typo player: set sin_zero to 0 sin_zero is required to be cleared to zero on some architectures, so let's just follow what the spec says. Noticed by Coverity. TcpSocket doesn't need to know about the transport README -> README.md By using Markdown instead of simple text, we can get hyperlinks and other nice features on the GitHub frontpage. So yeah, let's do that. editor: simplify TrackView::invalidateTrack() It can be implemented in terms of invalidateRange, so let's just do that, as it's simpler. editor: tighter invalidation A lot of these were written by a lazy coder, not bothering to use the proper bounds when invalidating. Let's remedy that. editor: add and use invalidateAll helper TrackView::invalidateAll is a bit different from viewport()->update() in that it doesn't invalidate the track-names nor row-numbers. So let's do that to get some more speed as well. editor: do not care about index when creating tracks The only thing we want to do at this point is to get the track itself, so let's go directly to the target. editor: resolve document once Mocking around with the document-getter many times in a method is just nasty. Get it once, and work on a local reference instead. README: update notes about compiling the example This section was based on building the editor, but after the editor was ported to Qt, this description got out-of-date. Update it so it doesn't relate to qmake. Gitignore for build directory under windows fix missing newline in .gitignore player: num_keys is int, not size_t num_keys is int, but is attempted read and written as size_t. This breaks pretty badly on 64-bit architectures, so let's write it as the integer it is. player: only define snprintf on older CRTs Visual Studio 2012 and up provides snprintf under it's real name, and Visual Studio 2015 does not allow redefining snprintf. So let's just use the real name in those cases. player: prevent annoying warning on VS2012 and newer Visual Studio 2012 and newer seems to have a peculiarity with it's warning-generation for it's "strdup is reserved, use _strdup instead"-warning, where it warns even if we use _strdup through a macro called strdup. This seems a bit nasty to fix at the core, but it seems we can just side-step the issue alltogether, by using the NEED_STRDUP macro to define our own version instead. This has the side-effect of us using one less CRT function, which the intro-people may or may not be happy about. But let's let them weigh in on that if needed. player: disable MSVCRT deprecations and security warnings Not everyone use our project files for building on MSVC, so let's make sure these are always defined. This also show that we forgot to define _CRT_NONSTDC_NO_DEPRECATE in some build-configurations, leading to the need for the strdup-hack. player: support IPv6 For IPv6, we need to use getaddrinfo instead of gethostbyname. This also fixes some deprecation-warnings on VS2015, so yay. Thanks to Niels J. de Wit for help with testing. player: use ipv6 on linux and osx Linux and OSX both support IPv6, so let's enable it on both. example_bass: silence OSX deprecation warning Apple has marked gluPerspective and gluLookAt as deprecated, in favour of GLKit's similar functionality. So to prevent needless warnings, let's wrap those into an interface similar to the old GLU functions, so we can keep the code portable. Compile fix: warnings for VS2012 (+x64) and up. add appveyor build-config This allows us to do automated builds of the editor using AppVeyor.com, and will package up the built editor on success. This allows people to download prebuilt editors instead of having to install Qt and all the jazz. README: add build-status Since we're now using AppVeyor.com as a CI service for Windows, let's show the build-status. appveyor: clean up build-version number We don't want build-numbers to be confused with release-version numbers. So let's just make the appveyor-version be the build-number. add a simple travis config Build both libs and editor on Linux and OSX. No support for uploading builds yet, though. README: add travis badge In addition to appveyor, let's also display the build status for travis-ci for the Linux and OSX builds. editor: eliminate x64 size_t <-> int warnings update .gitignore vs2013: add project files While we're at it, rename VS2008 project files to *.vs2008.* also. appveyor: x86 -> Win32 appveyor: build lib and examples Make sure we build both in Release and Debug Client modes, to cover a few combinations of build flags. --- .gitignore | 38 + .travis.yml | 10 + COPYING | 2 +- Makefile | 60 +- README | 91 -- README.md | 92 ++ appveyor.yml | 48 + contrib/DotRocket/DotRocket/DotRocket.cpp | 11 +- contrib/DotRocket/DotRocket/DotRocket.h | 3 +- contrib/DotRocket/DotRocket/DotRocket.vcproj | 4 +- .../DotRocket/DotRocketClient/DotRocketClient.cpp | 53 +- .../DotRocket/DotRocketClient/DotRocketClient.h | 7 +- .../DotRocketClient/DotRocketClient.vcproj | 4 +- editor/afxres.h | 12 - editor/appicon.icns | Bin 0 -> 122040 bytes editor/clientsocket.cpp | 183 ++- editor/clientsocket.h | 177 +- editor/editor.cpp | 777 +-------- editor/editor.pro | 28 + editor/editor.qrc | 5 + editor/editor.rc | 193 --- editor/editor.vcproj | 269 ---- editor/mainwindow.cpp | 578 +++++++ editor/mainwindow.h | 73 + editor/recentfiles.cpp | 99 -- editor/recentfiles.h | 32 - editor/resource.h | 40 - editor/syncdocument.cpp | 431 +++-- editor/syncdocument.h | 354 +--- editor/synctrack.h | 152 ++ editor/trackview.cpp | 1687 +++++++++----------- editor/trackview.h | 271 ++-- example_bass/example_bass.cpp | 33 +- example_bass/example_bass.vcproj | 672 -------- example_bass/example_bass.vs2008.vcproj | 672 ++++++++ example_bass/example_bass.vs2013.vcxproj | 361 +++++ example_bass/example_bass.vs2013.vcxproj.filters | 25 + example_bass/packages.config | 5 + examples.sln | 59 - examples.vs2008.sln | 59 + examples.vs2013.sln | 58 + lib/base.h | 35 + lib/device.c | 475 ++++++ lib/device.h | 55 + lib/librocket.vs2008.vcproj | 565 +++++++ lib/librocket.vs2013.vcxproj | 292 ++++ lib/librocket.vs2013.vcxproj.filters | 39 + lib/sync.h | 46 + lib/track.c | 124 ++ lib/track.h | 47 + rocket.sln | 20 - sync/base.h | 124 -- sync/data.c | 33 - sync/data.h | 28 - sync/device.c | 359 ----- sync/device.h | 23 - sync/sync.h | 44 - sync/track.c | 128 -- sync/track.h | 51 - sync_player.vcproj | 570 ------- 60 files changed, 5511 insertions(+), 5275 deletions(-) create mode 100644 .gitignore create mode 100644 .travis.yml delete mode 100644 README create mode 100644 README.md create mode 100644 appveyor.yml delete mode 100644 editor/afxres.h create mode 100644 editor/appicon.icns create mode 100644 editor/editor.pro create mode 100644 editor/editor.qrc delete mode 100644 editor/editor.vcproj create mode 100644 editor/mainwindow.cpp create mode 100644 editor/mainwindow.h delete mode 100644 editor/recentfiles.cpp delete mode 100644 editor/recentfiles.h delete mode 100644 editor/resource.h create mode 100644 editor/synctrack.h delete mode 100644 example_bass/example_bass.vcproj create mode 100644 example_bass/example_bass.vs2008.vcproj create mode 100644 example_bass/example_bass.vs2013.vcxproj create mode 100644 example_bass/example_bass.vs2013.vcxproj.filters create mode 100644 example_bass/packages.config delete mode 100644 examples.sln create mode 100644 examples.vs2008.sln create mode 100644 examples.vs2013.sln create mode 100644 lib/base.h create mode 100644 lib/device.c create mode 100644 lib/device.h create mode 100644 lib/librocket.vs2008.vcproj create mode 100644 lib/librocket.vs2013.vcxproj create mode 100644 lib/librocket.vs2013.vcxproj.filters create mode 100644 lib/sync.h create mode 100644 lib/track.c create mode 100644 lib/track.h delete mode 100644 rocket.sln delete mode 100644 sync/base.h delete mode 100644 sync/data.c delete mode 100644 sync/data.h delete mode 100644 sync/device.c delete mode 100644 sync/device.h delete mode 100644 sync/sync.h delete mode 100644 sync/track.c delete mode 100644 sync/track.h delete mode 100644 sync_player.vcproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fcd67e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +*.o +*.a +*.lib +*.ncb +*.pdb +*.suo +*.idb +*.user +*.sdf +*.opensdf +moc_*.cpp +qrc_*.cpp +ui_*.h +.*.swp +.DS_Store +*~ +/editor/Makefile* +/editor/editor.vcproj +/editor/editor.sln +/editor/debug/ +/editor/release/ +/editor/editor/ +/editor/editor.app/ +/example_bass/example_bass +/example_bass/Debug Client/ +/example_bass/Debug/ +/example_bass/Release Client/ +/example_bass/Release/ +/example_bass/x64/ +/example_bass/include/ +/example_bass/lib/ +/lib/Debug Client/ +/lib/Debug/ +/lib/Release Client/ +/lib/Release/ +/lib/x64/ +/packages/ +build-* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fb36f62 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: c + +os: + - linux + - osx + +before_script: + - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew install qt; fi + +script: make diff --git a/COPYING b/COPYING index d46968d..a2d288f 100644 --- a/COPYING +++ b/COPYING @@ -1,4 +1,4 @@ -Copyright (C) 2007-2008 Erik Faye-Lund and Egbert Teeselink +Copyright (C) 2007 Contributors 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. diff --git a/Makefile b/Makefile index 0a97efa..efd88b4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,10 @@ # default target all: +.PHONY: all clean editor + +QMAKE ?= qmake + # default build flags CFLAGS = -g -O2 -Wall @@ -13,31 +17,57 @@ ifdef COMSPEC SDL_LIBS = -lSDL LDLIBS += -lws2_32 else - OPENGL_LIBS = -lGL -lGLU + UNAME_S := $(shell uname -s) + + ifeq ($(UNAME_S), Linux) + CPPFLAGS += -DUSE_GETADDRINFO + OPENGL_LIBS = -lGL -lGLU + else ifeq ($(UNAME_S), Darwin) + CPPFLAGS += -DUSE_GETADDRINFO + OPENGL_LIBS = -framework OpenGL + else + OPENGL_LIBS = -lGL -lGLU + endif + SDL_CFLAGS = $(shell sdl-config --cflags) SDL_LIBS = $(shell sdl-config --libs) LDLIBS += -lm endif -SYNC_OBJS = \ - sync/data.o \ - sync/device.o \ - sync/track.o +LIB_OBJS = \ + lib/device.o \ + lib/track.o -all: lib/librocket.a +all: lib/librocket.a lib/librocket-player.a editor -bin/example_bass$X: CPPFLAGS += -Iexample_bass/include -bin/example_bass$X: CXXFLAGS += $(SDL_CFLAGS) -bin/example_bass$X: LDLIBS += -Lexample_bass/lib -lbass -bin/example_bass$X: LDLIBS += $(OPENGL_LIBS) $(SDL_LIBS) +example_bass/%$X: CPPFLAGS += -Iexample_bass/include +example_bass/%$X: CXXFLAGS += $(SDL_CFLAGS) +example_bass/%$X: LDLIBS += -Lexample_bass/lib -lbass +example_bass/%$X: LDLIBS += $(OPENGL_LIBS) $(SDL_LIBS) clean: - $(RM) -rf $(SYNC_OBJS) lib bin + $(RM) $(LIB_OBJS) lib/librocket.a lib/librocket-player.a + $(RM) example_bass/example_bass$X example_bass/example_bass-player$X + if test -e editor/Makefile; then $(MAKE) -C editor clean; fi; + $(RM) editor/editor editor/Makefile -lib/librocket.a: $(SYNC_OBJS) - @mkdir -p lib +lib/librocket.a: $(LIB_OBJS) $(AR) $(ARFLAGS) $@ $^ -bin/example_bass$X: example_bass/example_bass.cpp lib/librocket.a - @mkdir -p bin +%.player.o : %.c + $(COMPILE.c) -DSYNC_PLAYER $(OUTPUT_OPTION) $< + +lib/librocket-player.a: $(LIB_OBJS:.o=.player.o) + $(AR) $(ARFLAGS) $@ $^ + +example_bass/example_bass$X: example_bass/example_bass.cpp lib/librocket.a $(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@ + +example_bass/example_bass-player$X: example_bass/example_bass.cpp lib/librocket-player.a + $(LINK.cpp) -DSYNC_PLAYER $^ $(LOADLIBES) $(LDLIBS) -o $@ + +editor/Makefile: editor/editor.pro + cd editor && $(QMAKE) editor.pro -o Makefile + +editor: editor/Makefile + $(MAKE) -C editor diff --git a/README b/README deleted file mode 100644 index 87d979f..0000000 --- a/README +++ /dev/null @@ -1,91 +0,0 @@ -GNU Rocket -========== -GNU Rocket is an intuitive new way of... bah, whatever. It's a sync-tracker, -a tool for synchronizing music and visuals in demoscene productions. It -consists of a GUI editor that runs on Microsoft Windows, and an ANSI C -library that can either communicate with the editor over a network socket, -or play back an exported data-set. - -Compile Editor --------------- -GNU Rocket compiles using Microsoft Visual Studio 2008. Open editor.sln and -select "Build" -> "Build Solution" from the menu to build the editor. - -Compile Example ---------------- -GNU Rocket contains an example client called example_bass. This is a simple -OpenGL, SDL 1.2 and BASS audio library application, that demonstrates how to -use the GNU Rocket API. - -Before compiling the example, you need to make sure you have recent -SDL and BASS libraries and includes. These can be downloaded from the -following web-sites: - -http://www.libsdl.org/ -http://www.un4seen.com/ - -The header files and libraries can be installed local to the project by -copying all .lib-files to the example_bass/lib/, all .h files to -example_bass/inclide/, and all .dll files to the example_bass/. - -Once the prerequisites are installed, the example can be compiled much like -the editor; by opening examples.sln and selecting "Build" -> "Build Solution" -from the menu. - -Using the editor ----------------- -The GNU Rocket editor is laid out like a music-tracker; tracks (or columns) -and rows. Each track represents a separate "variable" in the demo, over the -entire time-domain of the demo. Each row represents a specific point in time, -and consists of a set of key-frames. The key-frames are interpolated over time -according to their interpolation modes. - -Interpolation modes -------------------- -Each key-frame has an interpolation mode associated with it, and that -interpolation mode is valid until the next key-frame is reached. The different -interpolation modes are the following: - -* Step : This is the simplest mode, and always returns the key's value. -* Linear : This does a linear interpolation between the current and the next - key's values. -* Smooth : This interpolates in a smooth fashion, the exact function is what - is usually called "smoothstep". Do not confuse this mode with - splines; this only interpolates smoothly between two different - values, it does not try to calculate tangents or any such things. -* Ramp : This is similar to "Linear", but additionally applies an - exponentiation of the interpolation factor. - -Keyboard shortcuts -------------------- -Some of the GNU Rocket editor's features are available through the menu and -some keyboard shortcut. Here's a list of the supported keyboard shortcuts: - -Up/Down/Left/Right Move cursor -PgUp/PgDn Move cursor 16 rows up/down -Home/End Move cursor to begining/end -Ctrl+Left/Right Move track -Enter Enter key-frame value -Del Delete key-frame -i Enumerate interpolation mode -k Toggle bookmark -Alt+PgUp/PgDn Go to prev/next bookmark -Space Pause/Resume demo -Shift+Up/Down/Left/Right Select -Ctrl+C Copy -Ctrl+V Paste -Ctrl+Z Undo -Shift+Ctrl+Z Redo -Ctrl+B Bias keyframes -Shift+Ctrl+Up/Down Quick-bias by +/- 0.1 -Ctrl+Up/Down Quick-bias by +/- 1 -Ctrl+PgUp/PgDn Quick-bias by +/- 10 -Shift+Ctrl+PgUp/PgDn Quick-bias by +/- 100 - -Bugs and feed-back ------------------- -Please report bugs or other feed-back to the GNU Rocket mailing list: -rocket-users@lists.sourceforge.net - -Patches or technical questions can be sent to the developer-list: -rocket-developers@lists.sourceforge.net diff --git a/README.md b/README.md new file mode 100644 index 0000000..f119a1b --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +GNU Rocket +========== + +[![Build status](https://ci.appveyor.com/api/projects/status/dfq8qaedc6mtsefg/branch/master?svg=true)](https://ci.appveyor.com/project/kusma/rocket/branch/master) +[![Build Status](https://travis-ci.org/kusma/rocket.svg?branch=master)](https://travis-ci.org/kusma/rocket) + +GNU Rocket is an intuitive new way of... bah, whatever. It's a sync-tracker, +a tool for synchronizing music and visuals in demoscene productions. It +consists of a GUI editor (using Qt), and an ANSI C library that can either +communicate with the editor over a network socket, or play back an exported +data-set. + +Compile Editor +-------------- +The GNU Rocket editor uses qmake as a build-system abstraction, which can +be used to output Makefiles, Visual Studio project files or can be built +directly from QtCreator. See the qmake documentation for details. + +Compile Example +--------------- +GNU Rocket contains an example client called example\_bass. This is a simple +OpenGL, SDL 1.2 and BASS audio library application, that demonstrates how to +use the GNU Rocket API. + +Before compiling the example, you need to make sure you have recent [SDL](http://www.libsdl.org/) +and [BASS](http://www.un4seen.com/) libraries and includes. + +The header files and libraries can be installed local to the project by +copying all .lib-files to the example\_bass/lib/, all .h files to +example\_bass/include/, and all .dll files to the example\_bass/. + +Once the prerequisites are installed, the example can be compiled either by +opening examples.sln and selecting "Build" -> "Build Solution" from Visual +Studio 2008, or by doing "make example_bass/example_bass" on Unix-base +systems. + +Using the editor +---------------- +The GNU Rocket editor is laid out like a music-tracker; tracks (or columns) +and rows. Each track represents a separate "variable" in the demo, over the +entire time-domain of the demo. Each row represents a specific point in time, +and consists of a set of key frames. The key frames are interpolated over time +according to their interpolation modes. + +Interpolation modes +------------------- +Each key frame has an interpolation mode associated with it, and that +interpolation mode is valid until the next key frame is reached. The different +interpolation modes are the following: + +* Step : This is the simplest mode, and always returns the key's value. +* Linear : This does a linear interpolation between the current and the next + key's values. +* Smooth : This interpolates in a smooth fashion, the exact function is what + is usually called "smoothstep". Do not confuse this mode with + splines; this only interpolates smoothly between two different + values, it does not try to calculate tangents or any such things. +* Ramp : This is similar to "Linear", but additionally applies an + exponentiation of the interpolation factor. + +Keyboard shortcuts +------------------- +Some of the GNU Rocket editor's features are available through the menu and +some keyboard shortcut. Here's a list of the supported keyboard shortcuts: + +| Shortcut | Action | +|:-------------------------|:-----------------------------| +| Up/Down/Left/Right | Move cursor | +| PgUp/PgDn | Move cursor 16 rows up/down | +| Home/End | Move cursor to begining/end | +| Ctrl+Left/Right | Move track | +| Enter | Enter key frame value | +| Del | Delete key frame | +| i | Enumerate interpolation mode | +| k | Toggle bookmark | +| Alt+PgUp/PgDn | Go to prev/next bookmark | +| Space | Pause/Resume demo | +| Shift+Up/Down/Left/Right | Select | +| Ctrl+C | Copy | +| Ctrl+V | Paste | +| Ctrl+Z | Undo | +| Shift+Ctrl+Z | Redo | +| Ctrl+B | Bias key frames | +| Shift+Ctrl+Up/Down | Quick-bias by +/- 0.1 | +| Ctrl+Up/Down | Quick-bias by +/- 1 | +| Ctrl+PgUp/PgDn | Quick-bias by +/- 10 | +| Shift+Ctrl+PgUp/PgDn | Quick-bias by +/- 100 | + +Bugs and feedback +------------------ +Please report bugs or other feedback to the GNU Rocket mailing list: +gnu-rocket@googlegroups.com diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..89a0795 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,48 @@ +version: '{build}' + +environment: + matrix: + - platform: x64 + cc: VS2013 + QTDIR: C:\Qt\5.5\msvc2013_64 + - platform: Win32 + cc: VS2013 + QTDIR: C:\Qt\5.5\msvc2013 + +configuration: + - Release + +cache: + - packages -> **\packages.config + +install: + - nuget restore examples.vs2013.sln + # download and install bass + - curl -fsS -o bass24.zip http://www.un4seen.com/files/bass24.zip + - 7z x -obass24 bass24.zip > NUL + - mkdir example_bass\lib + - mkdir example_bass\lib64 + - mkdir example_bass\include + - copy bass24\c\bass.lib example_bass\lib + - copy bass24\c\x64\bass.lib example_bass\lib64 + - copy bass24\c\bass.h example_bass\include + +before_build: + - set PATH=%QTDIR%\bin;%PATH% + +build_script: + - msbuild examples.vs2013.sln + - msbuild examples.vs2013.sln /property:Configuration="Release Client" + - cd editor + - qmake -tp vc editor.pro + - msbuild + +after_build: + - mkdir staging + - cd staging + - copy ..\release\editor.exe . + - windeployqt --release editor.exe + +artifacts: + - path: editor\staging + name: editor diff --git a/contrib/DotRocket/DotRocket/DotRocket.cpp b/contrib/DotRocket/DotRocket/DotRocket.cpp index 5f2a913..79f9563 100644 --- a/contrib/DotRocket/DotRocket/DotRocket.cpp +++ b/contrib/DotRocket/DotRocket/DotRocket.cpp @@ -4,21 +4,22 @@ #include "DotRocket.h" -#include "../../../sync/sync.h" -#include "../../../sync/track.h" +#include "../../../lib/sync.h" +#include "../../../lib/track.h" using System::Runtime::InteropServices::Marshal; using DotRocket::Track; using DotRocket::PlayerDevice; -private ref class PlayerTrack: public Track { +private ref class PlayerTrack: public Track +{ const sync_track *track; public: PlayerTrack(const sync_track *track): track(track) {} - virtual float GetValue(double time) override + virtual double GetValue(double time) override { return sync_get_val(track, time); - }; + } }; PlayerDevice::PlayerDevice(System::String ^name) diff --git a/contrib/DotRocket/DotRocket/DotRocket.h b/contrib/DotRocket/DotRocket/DotRocket.h index 3625fd7..1df3d9c 100644 --- a/contrib/DotRocket/DotRocket/DotRocket.h +++ b/contrib/DotRocket/DotRocket/DotRocket.h @@ -3,7 +3,6 @@ #pragma once struct sync_device; -struct sync_track; using namespace System; using namespace System::Collections::Generic; @@ -13,7 +12,7 @@ namespace DotRocket public ref class Track abstract { public: - virtual float GetValue(double time) = 0; + virtual double GetValue(double time) = 0; }; public delegate void PauseEventHandler(bool flag); diff --git a/contrib/DotRocket/DotRocket/DotRocket.vcproj b/contrib/DotRocket/DotRocket/DotRocket.vcproj index f3a1cda..054e191 100644 --- a/contrib/DotRocket/DotRocket/DotRocket.vcproj +++ b/contrib/DotRocket/DotRocket/DotRocket.vcproj @@ -59,7 +59,7 @@ /> using DotRocket::Track; using DotRocket::Device; using DotRocket::ClientDevice; -private ref class ClientTrack: public Track { +private ref class ClientTrack: public Track +{ const sync_track *track; public: ClientTrack(const sync_track *track) : track(track) {} - virtual float GetValue(double time) override + virtual double GetValue(double time) override { return sync_get_val(track, time); + } +}; + +namespace +{ + class DeviceReference + { + public: + DeviceReference(Device ^dev) : dev(dev) {} + Device ^GetDevice() { return dev; } + private: + gcroot dev; }; -}; - -ref class Callbacks { -public: - static Device ^DeviceCurrenltyBeingProcessed = nullptr; -}; - -void cb_pause(void *arg, int row) -{ - Callbacks::DeviceCurrenltyBeingProcessed->Pause(row); } -void cb_set_row(void *arg, int row) +static void cb_pause(void *arg, int flag) { - Callbacks::DeviceCurrenltyBeingProcessed->SetRow(row); + ((DeviceReference *)arg)->GetDevice()->Pause(!!flag); } -int cb_is_playing(void *arg) +static void cb_set_row(void *arg, int row) { - return !!Callbacks::DeviceCurrenltyBeingProcessed->IsPlaying(); + ((DeviceReference *)arg)->GetDevice()->SetRow(row); } -sync_cb callbacks[] = { +static int cb_is_playing(void *arg) +{ + return ((DeviceReference *)arg)->GetDevice()->IsPlaying(); +} + +static sync_cb callbacks[] = { cb_pause, cb_set_row, cb_is_playing @@ -76,13 +85,13 @@ ClientDevice::!ClientDevice() bool ClientDevice::Connect(System::String^ host, unsigned short port) { char *chost = (char *)(void *)Marshal::StringToHGlobalAnsi(host); - int result = sync_connect((sync_device *)device, chost, port); + int result = sync_connect(device, chost, port); Marshal::FreeHGlobal((System::IntPtr)chost); return !result; } bool ClientDevice::Update(int row) { - Callbacks::DeviceCurrenltyBeingProcessed = this; - return !sync_update(device, row, callbacks, device); + DeviceReference devref(this); + return !sync_update(device, row, callbacks, &devref); } diff --git a/contrib/DotRocket/DotRocketClient/DotRocketClient.h b/contrib/DotRocket/DotRocketClient/DotRocketClient.h index d9ecf87..35f118e 100644 --- a/contrib/DotRocket/DotRocketClient/DotRocketClient.h +++ b/contrib/DotRocket/DotRocketClient/DotRocketClient.h @@ -3,13 +3,14 @@ #pragma once struct sync_device; -struct sync_track; using namespace System; using namespace System::Collections::Generic; -namespace DotRocket { - public ref class ClientDevice: public Device { +namespace DotRocket +{ + public ref class ClientDevice: public Device + { protected: sync_device *device; Dictionary ^tracks; diff --git a/contrib/DotRocket/DotRocketClient/DotRocketClient.vcproj b/contrib/DotRocket/DotRocketClient/DotRocketClient.vcproj index 660b426..c9c1175 100644 --- a/contrib/DotRocket/DotRocketClient/DotRocketClient.vcproj +++ b/contrib/DotRocket/DotRocketClient/DotRocketClient.vcproj @@ -59,7 +59,7 @@ /> -#define ID_FILE_NEW 0xE100 -#define ID_FILE_OPEN 0xE101 -#define ID_FILE_SAVE 0xE103 -#define ID_FILE_SAVE_AS 0xE104 -#define ID_EDIT_CLEAR 0xE120 -#define ID_EDIT_COPY 0xE122 -#define ID_EDIT_CUT 0xE123 -#define ID_EDIT_PASTE 0xE125 -#define ID_EDIT_SELECT_ALL 0xE12A -#define ID_EDIT_UNDO 0xE12B -#define ID_EDIT_REDO 0xE12C diff --git a/editor/appicon.icns b/editor/appicon.icns new file mode 100644 index 0000000000000000000000000000000000000000..fff9e920bf41383599eb143579c16d59b96791c3 GIT binary patch literal 122040 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

3{?$w`@aDOI89Q3{6^!V+clV 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%0Aquu-=70o(h`aYt0Ae#aNB+jY4G3v4G`ldD~ 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 bEZEzV5jJX+sBIB9S$pG!1+w(`Ul#rc&b#`y literal 0 HcmV?d00001 diff --git a/editor/clientsocket.cpp b/editor/clientsocket.cpp index 3e47624..95df10d 100644 --- a/editor/clientsocket.cpp +++ b/editor/clientsocket.cpp @@ -1,47 +1,177 @@ #include "clientsocket.h" -#include "../sync/track.h" +#include "syncdocument.h" -#include -#include +#include +#include -void ClientSocket::sendSetKeyCommand(const std::string &trackName, const struct track_key &key) +bool WebSocket::readFrame(QByteArray &buf) +{ + unsigned char header[2]; + if (!TcpSocket::recv((char *)header, 2)) + return false; + + // int flags = header[0] >> 4; + int opcode = header[0] & 0xF; + int masked = header[1] >> 7; + int payload_len = header[1] & 0x7f; + + if (payload_len == 126) { + quint16 tmp; + if (!TcpSocket::recv((char *)&tmp, 2)) + return false; + payload_len = qFromBigEndian(tmp); + } else if (payload_len == 127) { + // dude, that's one crazy big payload! let's bail! + return false; + } + + unsigned char mask[4] = { 0 }; + if (masked) { + if (!TcpSocket::recv((char *)mask, sizeof(mask))) + return false; + } + + buf.resize(payload_len); + if (payload_len > 0) { + if (!TcpSocket::recv(buf.data(), payload_len)) + return false; + } + + for (int i = 0; i < payload_len; ++i) + buf[i] = buf[i] ^ mask[i & 3]; + + switch (opcode) { + case 9: + // got ping, send pong! + sendFrame(10, buf.data(), buf.length(), true); + buf.clear(); + return true; + + case 8: + // close + disconnect(); + buf.clear(); + return false; + } + + return true; +} + +bool WebSocket::recv(char *buffer, int length) +{ + if (!connected()) + return false; + while (length) { + while (!buf.length() && !readFrame(buf)) + return false; + + int bytes = qMin(buf.length(), length); + memcpy(buffer, buf.data(), bytes); + buf.remove(0, bytes); + buffer += bytes; + length -= bytes; + } + return true; +} + +bool WebSocket::sendFrame(int opcode, const char *payloadData, size_t payloadLength, bool endOfMessage) +{ + unsigned char header[2]; + header[0] = (endOfMessage ? 0x80 : 0) | (unsigned char)opcode; + header[1] = payloadLength < 126 ? (unsigned char)(payloadLength) : 126; + if (!TcpSocket::send((const char *)header, 2, false)) + return false; + + if (payloadLength >= 126) { + Q_ASSERT(payloadLength < 0xffff); + quint16 tmp = qToBigEndian((quint16)(payloadLength)); + if (!TcpSocket::send((const char *)&tmp, 2, false)) + return false; + } + + firstFrame = endOfMessage; + return TcpSocket::send(payloadData, payloadLength, endOfMessage); +} + +WebSocket *WebSocket::upgradeFromHttp(QTcpSocket *socket) +{ + QByteArray key; + for (;;) { + QByteArray line; + for (;;) { + char ch; + if (socket->read(&ch, 1) != 1) + return NULL; + + if (ch == '\n') + break; + if (ch != '\r') + line.push_back(ch); + } + + const char *prefix = "Sec-WebSocket-Key: "; + if (line.startsWith(prefix)) + key = line.right(line.length() - int(strlen(prefix))); + else if (!line.length()) + break; + } + + if (!key.length()) + return NULL; + + key.append("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(key.data(), key.size()); + + QString response = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "; + response.append(hash.result().toBase64()); + response.append("\r\n\r\n"); + + socket->write(response.toUtf8().constData(), response.length()); + + return new WebSocket(socket); +} + + +void ClientSocket::sendSetKeyCommand(const QString &trackName, const SyncTrack::TrackKey &key) { if (!connected() || clientTracks.count(trackName) == 0) return; - uint32_t track = htonl(clientTracks[trackName]); - uint32_t row = htonl(key.row); + quint32 track = qToBigEndian((quint32)clientTracks[trackName]); + quint32 row = qToBigEndian((quint32)key.row); union { float f; - uint32_t i; + quint32 i; } v; v.f = key.value; - v.i = htonl(v.i); + v.i = qToBigEndian(v.i); - assert(key.type < KEY_TYPE_COUNT); + Q_ASSERT(key.type < SyncTrack::TrackKey::KEY_TYPE_COUNT); unsigned char cmd = SET_KEY; - send((char *)&cmd, 1, 0); - send((char *)&track, sizeof(track), 0); - send((char *)&row, sizeof(row), 0); - send((char *)&v.i, sizeof(v.i), 0); - send((char *)&key.type, 1, 0); + send((char *)&cmd, 1, false); + send((char *)&track, sizeof(track), false); + send((char *)&row, sizeof(row), false); + send((char *)&v.i, sizeof(v.i), false); + send((char *)&key.type, 1, true); } -void ClientSocket::sendDeleteKeyCommand(const std::string &trackName, int row) +void ClientSocket::sendDeleteKeyCommand(const QString &trackName, int row) { if (!connected() || clientTracks.count(trackName) == 0) return; - uint32_t track = htonl(int(clientTracks[trackName])); - row = htonl(row); + quint32 track = qToBigEndian((quint32)clientTracks[trackName]); + row = qToBigEndian((quint32)row); unsigned char cmd = DELETE_KEY; - send((char *)&cmd, 1, 0); - send((char *)&track, sizeof(int), 0); - send((char *)&row, sizeof(int), 0); + send((char *)&cmd, 1, false); + send((char *)&track, sizeof(int), false); + send((char *)&row, sizeof(int), true); } void ClientSocket::sendSetRowCommand(int row) @@ -50,9 +180,9 @@ void ClientSocket::sendSetRowCommand(int row) return; unsigned char cmd = SET_ROW; - row = htonl(row); - send((char *)&cmd, 1, 0); - send((char *)&row, sizeof(int), 0); + row = qToBigEndian((quint32)row); + send((char *)&cmd, 1, false); + send((char *)&row, sizeof(int), true); } void ClientSocket::sendPauseCommand(bool pause) @@ -61,9 +191,8 @@ void ClientSocket::sendPauseCommand(bool pause) return; unsigned char cmd = PAUSE, flag = pause; - send((char *)&cmd, 1, 0); - send((char *)&flag, 1, 0); - clientPaused = pause; + send((char *)&cmd, 1, false); + send((char *)&flag, 1, true); } void ClientSocket::sendSaveCommand() @@ -72,5 +201,5 @@ void ClientSocket::sendSaveCommand() return; unsigned char cmd = SAVE_TRACKS; - send((char *)&cmd, 1, 0); + send((char *)&cmd, 1, true); } diff --git a/editor/clientsocket.h b/editor/clientsocket.h index f8f82e6..b3f9d36 100644 --- a/editor/clientsocket.h +++ b/editor/clientsocket.h @@ -1,63 +1,178 @@ -#include "../sync/base.h" -#include +#ifndef CLIENTSOCKET_H +#define CLIENTSOCKET_H -class ClientSocket { +#include +#include +#include + +#include "synctrack.h" + +#define CLIENT_GREET "hello, synctracker!" +#define SERVER_GREET "hello, demo!" + +enum { + SET_KEY = 0, + DELETE_KEY = 1, + GET_TRACK = 2, + SET_ROW = 3, + PAUSE = 4, + SAVE_TRACKS = 5 +}; + +class TcpSocket { public: - ClientSocket() : socket(INVALID_SOCKET) {} - explicit ClientSocket(SOCKET socket) : socket(socket), clientPaused(true) {} + explicit TcpSocket(QAbstractSocket *socket) : socket(socket) {} bool connected() const { - return INVALID_SOCKET != socket; + return socket != NULL; + } + + virtual void disconnect() + { + if (connected()) + socket->close(); + socket = NULL; + } + + virtual bool recv(char *buffer, int length) + { + if (!connected()) + return false; + + // wait for enough data to arrive + while (socket->bytesAvailable() < int(length)) { + if (!socket->waitForReadyRead(-1)) + break; + } + + qint64 ret = socket->read(buffer, length); + if (ret != int(length)) { + TcpSocket::disconnect(); + return false; + } + return true; + } + + virtual bool send(const char *buffer, size_t length, bool endOfMessage) + { + (void)endOfMessage; + if (!connected()) + return false; + int ret = socket->write(buffer, length); + if (ret != int(length)) { + TcpSocket::disconnect(); + return false; + } + return true; + } + + virtual bool pollRead() + { + if (!connected()) + return false; + return socket->bytesAvailable() > 0; + } + + QAbstractSocket *socket; +}; + +class WebSocket : public TcpSocket { +public: + explicit WebSocket(QTcpSocket *socket) : TcpSocket(socket), firstFrame(true) {} + + bool recv(char *buffer, int length); + bool send(const char *buffer, size_t length, bool endOfMessage) + { + return sendFrame(firstFrame ? 2 : 0, buffer, length, endOfMessage); + } + + + virtual void disconnect() + { + sendFrame(8, NULL, 0, true); + TcpSocket::disconnect(); + } + + bool pollRead() + { + if (buf.length() > 0) + return true; + return TcpSocket::pollRead(); + } + + // helpers + bool readFrame(QByteArray &buf); + bool sendFrame(int opcode, const char *payloadData, size_t payloadLength, bool endOfMessage); + static WebSocket *upgradeFromHttp(QTcpSocket *socket); + +private: + bool firstFrame; + QByteArray buf; +}; + +class ClientSocket : public QObject { + Q_OBJECT +public: + ClientSocket() : socket(NULL) {} + + bool connected() const + { + if (!socket) + return false; + return socket->connected(); } void disconnect() { - closesocket(socket); - socket = INVALID_SOCKET; + if (socket) + socket->disconnect(); clientTracks.clear(); } - bool recv(char *buffer, size_t length, int flags) + bool recv(char *buffer, int length) { - if (!connected()) + if (!socket) return false; - int ret = ::recv(socket, buffer, int(length), flags); - if (ret != int(length)) { - disconnect(); - return false; - } - return true; + return socket->recv(buffer, length); } - bool send(const char *buffer, size_t length, int flags) + bool send(const char *buffer, size_t length, bool endOfMessage) { - if (!connected()) + if (!socket) return false; - int ret = ::send(socket, buffer, int(length), flags); - if (ret != int(length)) { - disconnect(); - return false; - } - return true; + return socket->send(buffer, length, endOfMessage); } bool pollRead() { if (!connected()) return false; - return !!socket_poll(socket); + return socket->pollRead(); } - void sendSetKeyCommand(const std::string &trackName, const struct track_key &key); - void sendDeleteKeyCommand(const std::string &trackName, int row); + void sendSetKeyCommand(const QString &trackName, const SyncTrack::TrackKey &key); + void sendDeleteKeyCommand(const QString &trackName, int row); void sendSetRowCommand(int row); void sendPauseCommand(bool pause); void sendSaveCommand(); - bool clientPaused; - std::map clientTracks; + QMap clientTracks; + TcpSocket *socket; -private: - SOCKET socket; +public slots: + void onPauseChanged(bool paused) + { + sendPauseCommand(paused); + } + + void onKeyFrameChanged(const SyncTrack &track, int row) + { + if (track.isKeyFrame(row)) + sendSetKeyCommand(track.name, track.getKeyFrame(row)); + else + sendDeleteKeyCommand(track.name, row); + } }; + +#endif // !defined(CLIENTSOCKET_H) diff --git a/editor/editor.cpp b/editor/editor.cpp index cce7426..ea829e7 100644 --- a/editor/editor.cpp +++ b/editor/editor.cpp @@ -1,765 +1,34 @@ -/* Copyright (C) 2007-2008 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ +#include +#include +#include +#include "mainwindow.h" -#include "../sync/base.h" -#include "afxres.h" -#include "resource.h" - -#include -#include -#include -#include -#include -#include - -// Windows XP look and feel. Seems to enable Vista look as well. -#pragma comment(linker, \ - "\"/manifestdependency:type='Win32' " \ - "name='Microsoft.Windows.Common-Controls' " \ - "version='6.0.0.0' " \ - "processorArchitecture='*' " \ - "publicKeyToken='6595b64144ccf1df' " \ - "language='*'\"") - -#include "trackview.h" -#include "recentfiles.h" - -#include -static const wchar_t *mainWindowClassName = L"MainWindow"; -static const char *mainWindowTitle = "GNU Rocket System"; -static const wchar_t *mainWindowTitleW = L"GNU Rocket System"; -static const char *keyName = "SOFTWARE\\GNU Rocket"; - -static void verror(const char *fmt, va_list va) +int main(int argc, char *argv[]) { - char temp[4096]; - vsnprintf(temp, sizeof(temp), fmt, va); - MessageBox(NULL, temp, mainWindowTitle, MB_OK | MB_ICONERROR); -} + QApplication app(argc, argv); + app.setOrganizationName("GNU Rocket Foundation"); + app.setApplicationName("GNU Rocket Editor"); + app.setWindowIcon(QIcon(":appicon.ico")); -static void error(const char *fmt, ...) -{ - va_list va; - va_start(va, fmt); - verror(fmt, va); - va_end(va); -} - -static void die(const char *fmt, ...) -{ - va_list va; - va_start(va, fmt); - verror(fmt, va); - va_end(va); - exit(EXIT_FAILURE); -} - -static HINSTANCE hInst; -static HWND hwnd = NULL; -static TrackView *trackView = NULL; -static HWND trackViewWin = NULL; -static HWND statusBarWin = NULL; -static HKEY regConfigKey = NULL; -static RecentFiles mruFileList(NULL); - -#define WM_SETROWS (WM_USER+1) -#define WM_BIASSELECTION (WM_USER+2) - -#include "../sync/sync.h" - -static LRESULT CALLBACK setRowsDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) -{ - switch (message) - { - case WM_INITDIALOG: - { - size_t *rows = (size_t *)lParam; - SetDlgItemInt(hDlg, IDC_SETROWS_EDIT, *rows, FALSE); - return TRUE; - } - break; - - case WM_COMMAND: - if (LOWORD(wParam) == IDOK) { - /* get value */ - size_t result = GetDlgItemInt(hDlg, IDC_SETROWS_EDIT, NULL, FALSE); - - /* update editor */ - SendMessage(GetParent(hDlg), WM_SETROWS, 0, result); - - /* end dialog */ - return EndDialog(hDlg, LOWORD(wParam)); - } else if(LOWORD(wParam) == IDCANCEL) - return EndDialog( hDlg, LOWORD(wParam)); - break; - - case WM_CLOSE: - EndDialog(hDlg, LOWORD(wParam)); - return TRUE; - } - - return FALSE; -} - -static LRESULT CALLBACK biasSelectionDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) -{ - switch (message) - { - case WM_INITDIALOG: - { - int *intialBias = (int*)lParam; - SetDlgItemInt(hDlg, IDC_BIASSELECTION_EDIT, *intialBias, TRUE); - return TRUE; - } - break; - - case WM_COMMAND: - if (LOWORD(wParam) == IDOK) { - /* get value */ - int bias = GetDlgItemInt(hDlg, IDC_BIASSELECTION_EDIT, NULL, FALSE); - - /* update editor */ - SendMessage(GetParent(hDlg), WM_BIASSELECTION, 0, LPARAM(bias)); - - /* end dialog */ - return EndDialog(hDlg, LOWORD(wParam)); - } else if(LOWORD(wParam) == IDCANCEL) - return EndDialog( hDlg, LOWORD(wParam)); - break; - - case WM_CLOSE: - EndDialog(hDlg, LOWORD(wParam)); - return TRUE; - } - - return FALSE; -} - -static void setWindowFileName(std::wstring fileName) -{ - wchar_t drive[_MAX_DRIVE],dir[_MAX_DIR],fname[_MAX_FNAME],ext[_MAX_EXT]; - _wsplitpath(fileName.c_str(), drive, dir, fname, ext); - std::wstring windowTitle = std::wstring(fname) + std::wstring(L" - ") + std::wstring(mainWindowTitleW); - SetWindowTextW(hwnd, windowTitle.c_str()); -} - -static HMENU findSubMenuContaining(HMENU menu, UINT id) -{ - for (int i = 0; i < GetMenuItemCount(menu); ++i) - { - if (GetMenuItemID(menu, i) == id) return menu; - else - { - HMENU subMenu = GetSubMenu(menu, i); - if ((HMENU)0 != subMenu) - { - HMENU ret = findSubMenuContaining(subMenu, id); - if ((HMENU)0 != ret) return ret; - } - } - } - return (HMENU)0; -} - -static void setDocument(SyncDocument *newDoc) -{ - SyncDocument *oldDoc = trackView->getDocument(); - - if (oldDoc && oldDoc->clientSocket.connected()) { - // delete old key-frames - for (size_t i = 0; i < oldDoc->num_tracks; ++i) { - sync_track *t = oldDoc->tracks[i]; - for (int j = 0; j < t->num_keys; ++j) - oldDoc->clientSocket.sendDeleteKeyCommand(t->name, t->keys[j].row); - } - - if (newDoc) { - // add back missing client-tracks - std::map::const_iterator it; - for (it = oldDoc->clientSocket.clientTracks.begin(); it != oldDoc->clientSocket.clientTracks.end(); ++it) { - int trackIndex = sync_find_track(newDoc, it->first.c_str()); - if (0 > trackIndex) - trackIndex = int(newDoc->createTrack(it->first.c_str())); - } - - // copy socket and update client - newDoc->clientSocket = oldDoc->clientSocket; - - for (size_t i = 0; i < newDoc->num_tracks; ++i) { - sync_track *t = newDoc->tracks[i]; - for (int j = 0; j < t->num_keys; ++j) - newDoc->clientSocket.sendSetKeyCommand(t->name, t->keys[j]); - } - } + QTcpServer serverSocket; + if (!serverSocket.listen(QHostAddress::Any, 1338)) { + QMessageBox::critical(NULL, NULL, QString("Could not start server:\n%1").arg(serverSocket.errorString()), QMessageBox::Ok); + exit(EXIT_FAILURE); } - trackView->setDocument(newDoc); - SendMessage(hwnd, WM_CURRVALDIRTY, 0, 0); - InvalidateRect(trackViewWin, NULL, FALSE); + MainWindow mainWindow(&serverSocket); - if (oldDoc) - delete oldDoc; -} - -static void fileNew() -{ - setDocument(new SyncDocument); - setWindowFileName(L"Untitled"); -} - -static void loadDocument(const std::wstring &_fileName) -{ - SyncDocument *newDoc = SyncDocument::load(_fileName); - if (newDoc) { - // update MRU list - mruFileList.insert(_fileName); - mruFileList.update(); - DrawMenuBar(hwnd); - - // set new document - setDocument(newDoc); - setWindowFileName(_fileName.c_str()); - } - else - error("failed to open file"); -} - -static void fileOpen() -{ - wchar_t temp[_MAX_FNAME + 1]; - temp[0] = L'\0'; // clear string - - OPENFILENAMEW ofn; - ZeroMemory(&ofn, sizeof(ofn)); - ofn.lStructSize = sizeof(ofn); - ofn.lpstrFile = temp; - ofn.nMaxFile = _MAX_FNAME; - ofn.lpstrDefExt = L"rocket"; - ofn.lpstrFilter = L"ROCKET File (*.rocket)\0*.rocket\0All Files (*.*)\0*.*\0\0"; - ofn.Flags = OFN_SHOWHELP | OFN_FILEMUSTEXIST; - if (GetOpenFileNameW(&ofn)) - { - loadDocument(temp); - } -} - -static bool fileSaveAs() -{ - wchar_t temp[_MAX_FNAME + 1]; - temp[0] = '\0'; - - OPENFILENAMEW ofn; - ZeroMemory(&ofn, sizeof(ofn)); - ofn.lStructSize = sizeof(ofn); - ofn.lpstrFile = temp; - ofn.nMaxFile = _MAX_FNAME; - ofn.lpstrDefExt = L"rocket"; - ofn.lpstrFilter = L"ROCKET File (*.rocket)\0*.rocket\0All Files (*.*)\0*.*\0\0"; - ofn.Flags = OFN_SHOWHELP | OFN_OVERWRITEPROMPT; - - if (GetSaveFileNameW(&ofn)) { - SyncDocument *doc = trackView->getDocument(); - if (doc->save(temp)) { - doc->clientSocket.sendSaveCommand(); - setWindowFileName(temp); - doc->fileName = temp; - - mruFileList.insert(temp); - mruFileList.update(); - DrawMenuBar(hwnd); - return true; - } else - error("Failed to save file"); - } - return false; -} - -static bool fileSave() -{ - SyncDocument *doc = trackView->getDocument(); - if (doc->fileName.empty()) - return fileSaveAs(); - - if (!doc->save(doc->fileName)) { - doc->clientSocket.sendSaveCommand(); - error("Failed to save file"); - return false; - } - return true; -} - -static void attemptQuit() -{ - SyncDocument *doc = trackView->getDocument(); - if (doc->modified()) { - UINT res = MessageBox(hwnd, "Save before exit?", mainWindowTitle, MB_YESNOCANCEL | MB_ICONQUESTION); - if ((IDYES == res && fileSave()) || (IDNO == res)) - DestroyWindow(hwnd); - } - else DestroyWindow(hwnd); -} - -static HWND createStatusBar(HINSTANCE hInstance, HWND hpwnd) -{ - HWND hwnd = CreateWindowEx( - 0, // no extended styles - STATUSCLASSNAME, // status bar - (LPCTSTR)NULL, // no text - SBARS_SIZEGRIP | WS_VISIBLE | WS_CHILD, // styles - 0, 0, 0, 0, // x, y, cx, cy - hpwnd, // parent window - NULL, // menu - hInstance, // instance - NULL // window data - ); - - int statwidths[] = { 150, 150 + 32, 150 + 32 * 2, 150 + 32 * 4, 150 + 32 * 6}; - SendMessage(hwnd, SB_SETPARTS, sizeof(statwidths) / sizeof(int), (LPARAM)statwidths); - SendMessage(hwnd, SB_SETTEXT, 0, (LPARAM)"Not connected"); - SendMessage(hwnd, SB_SETTEXT, 1, (LPARAM)"0"); - SendMessage(hwnd, SB_SETTEXT, 2, (LPARAM)"0"); - SendMessage(hwnd, SB_SETTEXT, 3, (LPARAM)"---"); - return hwnd; -} - -static LRESULT CALLBACK mainWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - SyncDocument *doc = trackView ? trackView->getDocument() : NULL; - switch(msg) - { - case WM_CREATE: - { - trackViewWin = trackView->create(hInst, hwnd); - InitCommonControls(); - statusBarWin = createStatusBar(hInst, hwnd); - - if (ERROR_SUCCESS != RegOpenKey(HKEY_CURRENT_USER, keyName, ®ConfigKey)) - { - if (ERROR_SUCCESS != RegCreateKey(HKEY_CURRENT_USER, keyName, ®ConfigKey)) - die("failed to create registry key"); - } - - /* Recent Files menu */ - mruFileList = RecentFiles(findSubMenuContaining(GetMenu(hwnd), ID_RECENTFILES_NORECENTFILES)); - mruFileList.load(regConfigKey); + if (app.arguments().size() > 1) { + if (app.arguments().size() > 2) { + QMessageBox::critical(&mainWindow, NULL, QString("usage: %1 [filename.rocket]").arg(argv[0]), QMessageBox::Ok); + exit(EXIT_FAILURE); } - break; + mainWindow.loadDocument(app.arguments()[1]); + } else + mainWindow.fileNew(); - case WM_CLOSE: - attemptQuit(); - break; - - case WM_DESTROY: - mruFileList.save(regConfigKey); - RegCloseKey(regConfigKey); - regConfigKey = NULL; - PostQuitMessage(0); - break; - - case WM_SIZE: - { - int width = LOWORD(lParam); - int height = HIWORD(lParam); - - RECT statusBarRect; - GetClientRect(statusBarWin, &statusBarRect); - int statusBarHeight = statusBarRect.bottom - statusBarRect.top; - - MoveWindow(trackViewWin, 0, 0, width, height - statusBarHeight, TRUE); - MoveWindow(statusBarWin, 0, height - statusBarHeight, width, statusBarHeight, TRUE); - } - break; - - case WM_SETFOCUS: - SetFocus(trackViewWin); // needed to forward keyboard input - break; - - case WM_SETROWS: - trackView->setRows(int(lParam)); - break; - - case WM_BIASSELECTION: - trackView->editBiasValue(float(lParam)); - break; - - case WM_COMMAND: - switch (LOWORD(wParam)) - { - case ID_FILE_NEW: - fileNew(); - InvalidateRect(trackViewWin, NULL, FALSE); - break; - - case ID_FILE_OPEN: - fileOpen(); - break; - - case ID_FILE_SAVE_AS: - fileSaveAs(); - break; - - case ID_FILE_SAVE: - fileSave(); - break; - - case ID_EDIT_SELECTALL: - trackView->selectAll(); - break; - - case ID_EDIT_SELECTTRACK: - trackView->selectTrack(trackView->getEditTrack()); - break; - - case ID_EDIT_SELECTROW: - trackView->selectRow(trackView->getEditRow()); - break; - - case ID_FILE_REMOTEEXPORT: - doc->clientSocket.sendSaveCommand(); - break; - - case ID_RECENTFILES_FILE1: - case ID_RECENTFILES_FILE2: - case ID_RECENTFILES_FILE3: - case ID_RECENTFILES_FILE4: - case ID_RECENTFILES_FILE5: - { - int index = LOWORD(wParam) - ID_RECENTFILES_FILE1; - std::wstring fileName; - if (mruFileList.getEntry(index, fileName)) - { - loadDocument(fileName); - } - } - break; - - case ID_FILE_EXIT: - attemptQuit(); - break; - - case ID_EDIT_UNDO: SendMessage(trackViewWin, WM_UNDO, 0, 0); break; - case ID_EDIT_REDO: SendMessage(trackViewWin, WM_REDO, 0, 0); break; - case ID_EDIT_COPY: SendMessage(trackViewWin, WM_COPY, 0, 0); break; - case ID_EDIT_CUT: SendMessage(trackViewWin, WM_CUT, 0, 0); break; - case ID_EDIT_PASTE: SendMessage(trackViewWin, WM_PASTE, 0, 0); break; + mainWindow.show(); + app.exec(); - case ID_EDIT_BOOKMARK_PREV: - { - int row = doc->prevRowBookmark(trackView->getEditRow()); - if (row >= 0) - trackView->setEditRow(row); - } - break; - - case ID_EDIT_BOOKMARK_NEXT: - { - int row = doc->nextRowBookmark(trackView->getEditRow()); - if (row >= 0) - trackView->setEditRow(row); - } - break; - - - case ID_EDIT_SETROWS: - { - size_t rows = trackView->getRows(); - INT_PTR result = DialogBoxParam(hInst, MAKEINTRESOURCE(IDD_SETROWS), hwnd, (DLGPROC)setRowsDialogProc, (LPARAM)&rows); - if (FAILED(result)) - error("unable to create dialog box"); - } - break; - - case ID_EDIT_BIAS: - { - int initialBias = 0; - INT_PTR result = DialogBoxParam(hInst, MAKEINTRESOURCE(IDD_BIASSELECTION), hwnd, (DLGPROC)biasSelectionDialogProc, (LPARAM)&initialBias); - if (FAILED(result)) - error("unable to create dialog box"); - } - break; - } - break; - - case WM_ROWCHANGED: - { - char temp[256]; - snprintf(temp, 256, "%d", lParam ); - SendMessage(statusBarWin, SB_SETTEXT, 1, (LPARAM)temp); - } - break; - - case WM_TRACKCHANGED: - { - char temp[256]; - snprintf(temp, 256, "%d", lParam); - SendMessage(statusBarWin, SB_SETTEXT, 2, (LPARAM)temp); - } - break; - - case WM_CURRVALDIRTY: - { - char temp[256]; - if (doc->num_tracks > 0) { - const sync_track *t = doc->tracks[doc->getTrackIndexFromPos(trackView->getEditTrack())]; - int row = trackView->getEditRow(); - int idx = key_idx_floor(t, row); - snprintf(temp, 256, "%f", sync_get_val(t, row)); - const char *str = "---"; - if (idx >= 0) { - switch (t->keys[idx].type) { - case KEY_STEP: str = "step"; break; - case KEY_LINEAR: str = "linear"; break; - case KEY_SMOOTH: str = "smooth"; break; - case KEY_RAMP: str = "ramp"; break; - } - } - SendMessage(statusBarWin, SB_SETTEXT, 4, (LPARAM)str); - } else - snprintf(temp, 256, "---"); - SendMessage(statusBarWin, SB_SETTEXT, 3, (LPARAM)temp); - } - break; - - default: - return DefWindowProcW(hwnd, msg, wParam, lParam); - } return 0; } - -static ATOM registerMainWindowClass(HINSTANCE hInstance) -{ - WNDCLASSEXW wc; - - wc.cbSize = sizeof(wc); - wc.style = 0; - wc.lpfnWndProc = mainWindowProc; - wc.cbClsExtra = 0; - wc.cbWndExtra = 0; - wc.hInstance = hInstance; - wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION); - wc.hCursor = LoadCursor(NULL, IDC_ARROW); - wc.hbrBackground = (HBRUSH)0; - wc.lpszMenuName = MAKEINTRESOURCEW(IDR_MENU); - wc.lpszClassName = mainWindowClassName; - wc.hIconSm = wc.hIcon; - - return RegisterClassExW(&wc); -} - -static SOCKET clientConnect(SOCKET serverSocket, sockaddr_in *host) -{ - sockaddr_in hostTemp; - int hostSize = sizeof(sockaddr_in); - SOCKET clientSocket = accept(serverSocket, (sockaddr*)&hostTemp, &hostSize); - if (INVALID_SOCKET == clientSocket) return INVALID_SOCKET; - - const char *expectedGreeting = CLIENT_GREET; - char recievedGreeting[128]; - - recv(clientSocket, recievedGreeting, int(strlen(expectedGreeting)), 0); - - if (strncmp(expectedGreeting, recievedGreeting, strlen(expectedGreeting)) != 0) - { - closesocket(clientSocket); - return INVALID_SOCKET; - } - - const char *greeting = SERVER_GREET; - send(clientSocket, greeting, int(strlen(greeting)), 0); - - if (NULL != host) *host = hostTemp; - return clientSocket; -} - -static size_t clientIndex; -static void processCommand(ClientSocket &sock) -{ - SyncDocument *doc = trackView->getDocument(); - int strLen, serverIndex, newRow; - std::string trackName; - const sync_track *t; - unsigned char cmd = 0; - if (sock.recv((char*)&cmd, 1, 0)) { - switch (cmd) { - case GET_TRACK: - // read data - sock.recv((char *)&strLen, sizeof(int), 0); - strLen = ntohl(strLen); - if (!sock.connected()) - return; - - trackName.resize(strLen); - if (!sock.recv(&trackName[0], strLen, 0)) - return; - - // find track - serverIndex = sync_find_track(doc, - trackName.c_str()); - if (0 > serverIndex) - serverIndex = - int(doc->createTrack(trackName)); - - // setup remap - doc->clientSocket.clientTracks[trackName] = clientIndex++; - - // send key-frames - t = doc->tracks[serverIndex]; - for (int i = 0; i < (int)t->num_keys; ++i) - doc->clientSocket.sendSetKeyCommand(trackName, - t->keys[i]); - - InvalidateRect(trackViewWin, NULL, FALSE); - break; - - case SET_ROW: - sock.recv((char*)&newRow, sizeof(int), 0); - trackView->setEditRow(ntohl(newRow)); - break; - } - } -} - -int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, - LPSTR /*lpCmdLine*/, int /*nShowCmd*/) -{ -#ifdef _DEBUG - _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); -/* _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); - _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG); - _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG); */ -// _CrtSetBreakAlloc(254); -#endif - - hInst = hInstance; - CoInitialize(NULL); - - WSADATA wsa; - if (0 != WSAStartup(MAKEWORD(2, 0), &wsa)) - die("Failed to init network"); - - SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0); - - struct sockaddr_in sin; - memset(&sin, 0, sizeof sin); - - sin.sin_family = AF_INET; - sin.sin_addr.s_addr = INADDR_ANY; - sin.sin_port = htons(1338); - - if (SOCKET_ERROR == bind(serverSocket, (struct sockaddr *)&sin, - sizeof(sin))) - die("Could not start server"); - - while (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) - ; /* nothing */ - - ATOM mainClass = registerMainWindowClass(hInstance); - ATOM trackViewClass = registerTrackViewWindowClass(hInstance); - if (!mainClass || !trackViewClass) - die("Window Registration Failed!"); - - trackView = new TrackView(); - - hwnd = CreateWindowExW( - 0, - mainWindowClassName, - mainWindowTitleW, - WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, - CW_USEDEFAULT, CW_USEDEFAULT, // x, y - CW_USEDEFAULT, CW_USEDEFAULT, // width, height - NULL, NULL, hInstance, NULL - ); - - if (NULL == hwnd) - die("Window Creation Failed!"); - - int argc; - LPWSTR *argv = argv = CommandLineToArgvW(GetCommandLineW(), &argc); - if (argv && argc > 1) { - if (argc > 2) { - char prog[MAX_PATH]; - GetModuleFileNameA(NULL, prog, sizeof(prog)); - die("usage: %s [filename.rocket]", prog); - } - loadDocument(argv[1]); - } else - fileNew(); - - HACCEL accel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR)); - - ShowWindow(hwnd, TRUE); - UpdateWindow(hwnd); - - bool done = false; - MSG msg; - bool guiConnected = false; - while (!done) { - SyncDocument *doc = trackView->getDocument(); - if (!doc->clientSocket.connected()) { - SOCKET clientSocket = INVALID_SOCKET; - fd_set fds; - FD_ZERO(&fds); -#pragma warning(suppress: 4127) - FD_SET(serverSocket, &fds); - struct timeval timeout; - timeout.tv_sec = 0; - timeout.tv_usec = 0; - - // look for new clients - if (select(0, &fds, NULL, NULL, &timeout) > 0) - { - SendMessage(statusBarWin, SB_SETTEXT, 0, (LPARAM)"Accepting..."); - sockaddr_in client; - clientSocket = clientConnect(serverSocket, &client); - if (INVALID_SOCKET != clientSocket) - { - char temp[256]; - snprintf(temp, 256, "Connected to %s", inet_ntoa(client.sin_addr)); - SendMessage(statusBarWin, SB_SETTEXT, 0, (LPARAM)temp); - doc->clientSocket = ClientSocket(clientSocket); - clientIndex = 0; - doc->clientSocket.sendPauseCommand(true); - doc->clientSocket.sendSetRowCommand(trackView->getEditRow()); - guiConnected = true; - } - else SendMessage(statusBarWin, SB_SETTEXT, 0, (LPARAM)"Not Connected."); - } - } - - if (doc->clientSocket.connected()) { - ClientSocket &clientSocket = doc->clientSocket; - - // look for new commands - while (clientSocket.pollRead()) - processCommand(clientSocket); - } - - if (!doc->clientSocket.connected() && guiConnected) { - doc->clientSocket.clientPaused = true; - InvalidateRect(trackViewWin, NULL, FALSE); - SendMessage(statusBarWin, SB_SETTEXT, 0, (LPARAM)"Not Connected."); - guiConnected = false; - } - - while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) - { - if (!TranslateAccelerator(hwnd, accel, &msg)) - { - TranslateMessage(&msg); - DispatchMessage(&msg); - if (WM_QUIT == msg.message) done = true; - } - } - Sleep(1); - } - - closesocket(serverSocket); - WSACleanup(); - - delete trackView; - trackView = NULL; - - UnregisterClassW(mainWindowClassName, hInstance); - return int(msg.wParam); -} diff --git a/editor/editor.pro b/editor/editor.pro new file mode 100644 index 0000000..625c254 --- /dev/null +++ b/editor/editor.pro @@ -0,0 +1,28 @@ +TEMPLATE = app +TARGET = editor +DEPENDPATH += . +INCLUDEPATH += . + +QT = core gui xml network + +greaterThan(QT_MAJOR_VERSION, 4) { + QT += widgets +} + +# Input +HEADERS += clientsocket.h \ + mainwindow.h \ + syncdocument.h \ + synctrack.h \ + trackview.h + +SOURCES += clientsocket.cpp \ + editor.cpp \ + mainwindow.cpp \ + syncdocument.cpp \ + trackview.cpp + +RESOURCES += editor.qrc + +RC_FILE = editor.rc +ICON = appicon.icns diff --git a/editor/editor.qrc b/editor/editor.qrc new file mode 100644 index 0000000..8021387 --- /dev/null +++ b/editor/editor.qrc @@ -0,0 +1,5 @@ + + + appicon.ico + + diff --git a/editor/editor.rc b/editor/editor.rc index a80935e..341a8ac 100644 --- a/editor/editor.rc +++ b/editor/editor.rc @@ -1,194 +1 @@ -// Microsoft Visual C++ generated resource script. -// -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "afxres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// Norwegian (Bokmal) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_NOR) -#ifdef _WIN32 -LANGUAGE LANG_NORWEGIAN, SUBLANG_NORWEGIAN_BOKMAL -#pragma code_page(1252) -#endif //_WIN32 - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""afxres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Accelerator -// - -IDR_ACCELERATOR ACCELERATORS -BEGIN - "B", ID_EDIT_BIAS, VIRTKEY, CONTROL, NOINVERT - "C", ID_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT - "X", ID_EDIT_CUT, VIRTKEY, CONTROL, NOINVERT - "V", ID_EDIT_PASTE, VIRTKEY, CONTROL, NOINVERT - "Z", ID_EDIT_REDO, VIRTKEY, SHIFT, CONTROL, NOINVERT - "Z", ID_EDIT_UNDO, VIRTKEY, CONTROL, NOINVERT - "N", ID_FILE_NEW, VIRTKEY, CONTROL, NOINVERT - "O", ID_FILE_OPEN, VIRTKEY, CONTROL, NOINVERT - "S", ID_FILE_SAVE, VIRTKEY, CONTROL, NOINVERT - "E", ID_FILE_REMOTEEXPORT, VIRTKEY, CONTROL, NOINVERT - "A", ID_EDIT_SELECT_ALL, VIRTKEY, CONTROL, NOINVERT - "T", ID_EDIT_SELECTTRACK, VIRTKEY, CONTROL, NOINVERT - "R", ID_EDIT_SETROWS, VIRTKEY, CONTROL, NOINVERT - VK_PRIOR, ID_EDIT_BOOKMARK_PREV, VIRTKEY, ALT, NOINVERT - VK_NEXT, ID_EDIT_BOOKMARK_NEXT, VIRTKEY, ALT, NOINVERT -END - - -///////////////////////////////////////////////////////////////////////////// -// -// Menu -// - -IDR_MENU MENU -BEGIN - POPUP "&File" - BEGIN - MENUITEM "New\tCtrl+N", ID_FILE_NEW - MENUITEM "&Open\tCtrl+O", ID_FILE_OPEN - MENUITEM "&Save\tCtrl+S", ID_FILE_SAVE - MENUITEM "Save &As", ID_FILE_SAVE_AS - MENUITEM SEPARATOR - MENUITEM "Remote &Export\tCtrl+E", ID_FILE_REMOTEEXPORT - POPUP "Recent &Files" - BEGIN - MENUITEM "No recent files", ID_RECENTFILES_NORECENTFILES, GRAYED - END - MENUITEM SEPARATOR - MENUITEM "E&xit", ID_FILE_EXIT - END - POPUP "&Edit" - BEGIN - MENUITEM "Undo\tCtrl+Z", ID_EDIT_UNDO - MENUITEM "Redo\tCtrl+Shift+Z", ID_EDIT_REDO - MENUITEM SEPARATOR - MENUITEM "&Copy\tCtrl+C", ID_EDIT_COPY - MENUITEM "Cu&t\tCtrl+X", ID_EDIT_CUT - MENUITEM "&Paste\tCtrl+V", ID_EDIT_PASTE - MENUITEM "Clear\tDel", ID_EDIT_CLEAR - MENUITEM SEPARATOR - MENUITEM "Select All\tCtrl+A", ID_EDIT_SELECTALL - MENUITEM "Select Track\tCtrl+T", ID_EDIT_SELECTTRACK - MENUITEM "Select Row", ID_EDIT_SELECTROW - MENUITEM SEPARATOR - MENUITEM "&Bias Selection\tCtrl+B", ID_EDIT_BIAS - MENUITEM SEPARATOR - MENUITEM "Set Rows\tCtrl+R", ID_EDIT_SETROWS - MENUITEM SEPARATOR - MENUITEM "Previous Bookmark\tAlt+PgDn", ID_EDIT_BOOKMARK_PREV - MENUITEM "Next Bookmark\tAlt+PgUp", ID_EDIT_BOOKMARK_NEXT - END -END - - -///////////////////////////////////////////////////////////////////////////// -// -// Dialog -// - -IDD_SETROWS DIALOGEX 0, 0, 129, 27 -STYLE DS_SETFONT | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "Set Rows" -FONT 8, "MS Shell Dlg", 400, 0, 0x1 -BEGIN - EDITTEXT IDC_SETROWS_EDIT,7,6,59,12,ES_AUTOHSCROLL | ES_NUMBER - DEFPUSHBUTTON "OK",IDOK,72,6,50,14 -END - -IDD_BIASSELECTION DIALOGEX 0, 0, 129, 27 -STYLE DS_SETFONT | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU -CAPTION "Bias Selection" -FONT 8, "MS Shell Dlg", 400, 0, 0x1 -BEGIN - EDITTEXT IDC_BIASSELECTION_EDIT,7,6,59,12,ES_AUTOHSCROLL | ES_NUMBER - DEFPUSHBUTTON "OK",IDOK,72,6,50,14 -END - - -///////////////////////////////////////////////////////////////////////////// -// -// DESIGNINFO -// - -#ifdef APSTUDIO_INVOKED -GUIDELINES DESIGNINFO -BEGIN - IDD_SETROWS, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 122 - TOPMARGIN, 7 - BOTTOMMARGIN, 20 - END - - IDD_BIASSELECTION, DIALOG - BEGIN - LEFTMARGIN, 7 - RIGHTMARGIN, 122 - TOPMARGIN, 7 - BOTTOMMARGIN, 20 - END -END -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. IDI_APPLICATION ICON "appicon.ico" -#endif // Norwegian (Bokmal) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - diff --git a/editor/editor.vcproj b/editor/editor.vcproj deleted file mode 100644 index e4a8135..0000000 --- a/editor/editor.vcproj +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/editor/mainwindow.cpp b/editor/mainwindow.cpp new file mode 100644 index 0000000..aa1cfc8 --- /dev/null +++ b/editor/mainwindow.cpp @@ -0,0 +1,578 @@ +#include "mainwindow.h" +#include "trackview.h" +#include "syncdocument.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QTcpServer *serverSocket) : + QMainWindow(), + serverSocket(serverSocket), + clientIndex(0) +{ + trackView = new TrackView(this); + setCentralWidget(trackView); + + connect(trackView, SIGNAL(posChanged(int, int)), + this, SLOT(onPosChanged(int, int))); + connect(trackView, SIGNAL(pauseChanged(bool)), + &clientSocket, SLOT(onPauseChanged(bool))); + connect(trackView, SIGNAL(currValDirty()), + this, SLOT(onCurrValDirty())); + + createMenuBar(); + createStatusBar(); + + connect(serverSocket, SIGNAL(newConnection()), + this, SLOT(onNewConnection())); +} + +void MainWindow::showEvent(QShowEvent *event) +{ + QMainWindow::showEvent(event); + + // workaround for QTBUG-16507 + QString filePath = windowFilePath(); + setWindowFilePath(filePath + "foo"); + setWindowFilePath(filePath); +} + + +void MainWindow::createMenuBar() +{ + fileMenu = menuBar()->addMenu("&File"); + fileMenu->addAction(QIcon::fromTheme("document-new"), "New", this, SLOT(fileNew()), QKeySequence::New); + fileMenu->addAction(QIcon::fromTheme("document-open"), "&Open", this, SLOT(fileOpen()), QKeySequence::Open); + fileMenu->addAction(QIcon::fromTheme("document-save"), "&Save", this, SLOT(fileSave()), QKeySequence::Save); + fileMenu->addAction(QIcon::fromTheme("document-save-as"),"Save &As", this, SLOT(fileSaveAs()), QKeySequence::SaveAs); + fileMenu->addSeparator(); + fileMenu->addAction("Remote &Export", this, SLOT(fileRemoteExport()), Qt::CTRL + Qt::Key_E); + recentFilesMenu = fileMenu->addMenu(QIcon::fromTheme("document-open-recent"), "Recent &Files"); + for (int i = 0; i < 5; ++i) { + recentFileActions[i] = recentFilesMenu->addAction(QIcon::fromTheme("document-open-recent"), ""); + recentFileActions[i]->setVisible(false); + connect(recentFileActions[i], SIGNAL(triggered()), + this, SLOT(openRecentFile())); + } + updateRecentFiles(); + fileMenu->addSeparator(); + fileMenu->addAction(QIcon::fromTheme("application-exit"), "E&xit", this, SLOT(fileQuit()), QKeySequence::Quit); + + editMenu = menuBar()->addMenu("&Edit"); + editMenu->addAction(QIcon::fromTheme("edit-undo"), "Undo", trackView, SLOT(editUndo()), QKeySequence::Undo); + editMenu->addAction(QIcon::fromTheme("edit-redo"), "Redo", trackView, SLOT(editRedo()), QKeySequence::Redo); + editMenu->addSeparator(); + editMenu->addAction(QIcon::fromTheme("edit-copy"), "&Copy", trackView, SLOT(editCopy()), QKeySequence::Copy); + editMenu->addAction(QIcon::fromTheme("edit-cut"), "Cu&t", trackView, SLOT(editCut()), QKeySequence::Cut); + editMenu->addAction(QIcon::fromTheme("edit-paste"), "&Paste", trackView, SLOT(editPaste()), QKeySequence::Paste); + editMenu->addAction(QIcon::fromTheme("edit-clear"), "Clear", trackView, SLOT(editClear()), QKeySequence::Delete); + editMenu->addSeparator(); + editMenu->addAction(QIcon::fromTheme("edit-select-all"), "Select All", trackView, SLOT(selectAll()), QKeySequence::SelectAll); + editMenu->addAction("Select Track", trackView, SLOT(selectTrack()), Qt::CTRL + Qt::Key_T); + editMenu->addAction("Select Row", trackView, SLOT(selectRow())); + editMenu->addSeparator(); + editMenu->addAction("Bias Selection", this, SLOT(editBiasSelection()), Qt::CTRL + Qt::Key_B); + editMenu->addSeparator(); + editMenu->addAction("Set Rows", this, SLOT(editSetRows()), Qt::CTRL + Qt::Key_R); + editMenu->addSeparator(); + editMenu->addAction("Previous Bookmark", this, SLOT(editPreviousBookmark()), Qt::ALT + Qt::Key_PageUp); + editMenu->addAction("Next Bookmark", this, SLOT(editNextBookmark()), Qt::ALT + Qt::Key_PageDown); +} + +void MainWindow::createStatusBar() +{ + statusPos = new QLabel; + statusValue = new QLabel; + statusKeyType = new QLabel; + + statusBar()->addPermanentWidget(statusPos); + statusBar()->addPermanentWidget(statusValue); + statusBar()->addPermanentWidget(statusKeyType); + + statusBar()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + + setStatusText("Not connected"); + setStatusPosition(0, 0); + setStatusValue(0.0f, false); + setStatusKeyType(SyncTrack::TrackKey::STEP, false); +} + +static QStringList getRecentFiles() +{ +#ifdef Q_OS_WIN32 + QSettings settings("HKEY_CURRENT_USER\\Software\\GNU Rocket", + QSettings::NativeFormat); +#else + QSettings settings; +#endif + QStringList list; + for (int i = 0; i < 5; ++i) { + QVariant string = settings.value(QString("RecentFile%1").arg(i)); + if (string.isValid()) + list.push_back(string.toString()); + } + return list; +} + +static void setRecentFiles(const QStringList &files) +{ +#ifdef Q_OS_WIN32 + QSettings settings("HKEY_CURRENT_USER\\Software\\GNU Rocket", + QSettings::NativeFormat); +#else + QSettings settings; +#endif + + for (int i = 0; i < files.size(); ++i) + settings.setValue(QString("RecentFile%1").arg(i), files[i]); + + // remove keys not in the list + for (int i = files.size(); ;++i) { + QString key = QString("RecentFile%1").arg(i); + + if (!settings.contains(key)) + break; + + settings.remove(key); + } +} + +void MainWindow::updateRecentFiles() +{ + QStringList files = getRecentFiles(); + + if (!files.size()) { + recentFilesMenu->setEnabled(false); + return; + } + + Q_ASSERT(files.size() <= 5); + for (int i = 0; i < files.size(); ++i) { + QFileInfo info(files[i]); + QString text = QString("&%1 %2").arg(i + 1).arg(info.fileName()); + + recentFileActions[i]->setText(text); + recentFileActions[i]->setData(info.absoluteFilePath()); + recentFileActions[i]->setVisible(true); + } + for (int i = files.size(); i < 5; ++i) + recentFileActions[i]->setVisible(false); + recentFilesMenu->setEnabled(true); +} + +void MainWindow::setCurrentFileName(const QString &fileName) +{ + QFileInfo info(fileName); + + QStringList files = getRecentFiles(); + files.removeAll(info.absoluteFilePath()); + files.prepend(info.absoluteFilePath()); + while (files.size() > 5) + files.removeLast(); + setRecentFiles(files); + + updateRecentFiles(); + setWindowFilePath(fileName); +} + +void MainWindow::setStatusText(const QString &text) +{ + statusBar()->showMessage(text); +} + +void MainWindow::setStatusPosition(int col, int row) +{ + statusPos->setText(QString("Row %1, Col %2").arg(row).arg(col)); +} + +void MainWindow::setStatusValue(double val, bool valid) +{ + if (valid) + statusValue->setText(QString::number(val, 'f', 3)); + else + statusValue->setText("---"); +} + +void MainWindow::setStatusKeyType(SyncTrack::TrackKey::KeyType keyType, bool valid) +{ + if (!valid) { + statusKeyType->setText("---"); + return; + } + + switch (keyType) { + case SyncTrack::TrackKey::STEP: statusKeyType->setText("step"); break; + case SyncTrack::TrackKey::LINEAR: statusKeyType->setText("linear"); break; + case SyncTrack::TrackKey::SMOOTH: statusKeyType->setText("smooth"); break; + case SyncTrack::TrackKey::RAMP: statusKeyType->setText("ramp"); break; + default: Q_ASSERT(false); + } +} + +void MainWindow::setDocument(SyncDocument *newDoc) +{ + SyncDocument *oldDoc = trackView->getDocument(); + + if (oldDoc) + QObject::disconnect(oldDoc, SIGNAL(modifiedChanged(bool)), + this, SLOT(setWindowModified(bool))); + + if (oldDoc && clientSocket.connected()) { + // delete old key frames + for (int i = 0; i < oldDoc->getTrackCount(); ++i) { + SyncTrack *t = oldDoc->getTrack(i); + QMap keyMap = t->getKeyMap(); + QMap::const_iterator it; + for (it = keyMap.constBegin(); it != keyMap.constEnd(); ++it) + t->removeKey(it.key()); + QObject::disconnect(t, SIGNAL(keyFrameChanged(const SyncTrack &, int)), + &clientSocket, SLOT(onKeyFrameChanged(const SyncTrack &, int))); + } + + if (newDoc) { + // add back missing client-tracks + QMap::const_iterator it; + for (it = clientSocket.clientTracks.begin(); it != clientSocket.clientTracks.end(); ++it) { + SyncTrack *t = newDoc->findTrack(it.key()); + if (!t) + newDoc->createTrack(it.key()); + } + + for (int i = 0; i < newDoc->getTrackCount(); ++i) { + SyncTrack *t = newDoc->getTrack(i); + QMap keyMap = t->getKeyMap(); + QMap::const_iterator it; + for (it = keyMap.constBegin(); it != keyMap.constEnd(); ++it) + clientSocket.sendSetKeyCommand(t->name.toUtf8().constData(), *it); + QObject::connect(t, SIGNAL(keyFrameChanged(const SyncTrack &, int)), + &clientSocket, SLOT(onKeyFrameChanged(const SyncTrack &, int))); + } + } + } + + trackView->setDocument(newDoc); + trackView->dirtyCurrentValue(); + trackView->viewport()->update(); + + QObject::connect(newDoc, SIGNAL(modifiedChanged(bool)), + this, SLOT(setWindowModified(bool))); + + if (oldDoc) + delete oldDoc; +} + +void MainWindow::fileNew() +{ + setDocument(new SyncDocument); + setWindowFilePath("Untitled"); +} + +bool MainWindow::loadDocument(const QString &path) +{ + SyncDocument *newDoc = SyncDocument::load(path); + if (newDoc) { + // set new document + setDocument(newDoc); + setCurrentFileName(path); + return true; + } + return false; +} + +void MainWindow::fileOpen() +{ + QString fileName = QFileDialog::getOpenFileName(this, "Open File", "", "ROCKET File (*.rocket);;All Files (*.*)"); + if (fileName.length()) { + loadDocument(fileName); + } +} + +void MainWindow::fileSaveAs() +{ + QString fileName = QFileDialog::getSaveFileName(this, "Save File", "", "ROCKET File (*.rocket);;All Files (*.*)"); + if (fileName.length()) { + SyncDocument *doc = trackView->getDocument(); + if (doc->save(fileName)) { + clientSocket.sendSaveCommand(); + setCurrentFileName(fileName); + doc->fileName = fileName; + } + } +} + +void MainWindow::fileSave() +{ + SyncDocument *doc = trackView->getDocument(); + if (doc->fileName.isEmpty()) + return fileSaveAs(); + + if (!doc->save(doc->fileName)) + clientSocket.sendSaveCommand(); +} + +void MainWindow::fileRemoteExport() +{ + clientSocket.sendSaveCommand(); +} + +void MainWindow::openRecentFile() +{ + QAction *action = qobject_cast(sender()); + if (action) { + QString fileName = action->data().toString(); + if (!loadDocument(fileName)) { + QStringList files = getRecentFiles(); + files.removeAll(fileName); + setRecentFiles(files); + updateRecentFiles(); + } + } +} + +void MainWindow::fileQuit() +{ + SyncDocument *doc = trackView->getDocument(); + if (doc->isModified()) { + QMessageBox::StandardButton res = QMessageBox::question( + this, "GNU Rocket", "Save before exit?", + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (res == QMessageBox::Yes) { + fileSave(); + QApplication::quit(); + } else if (res == QMessageBox::No) + QApplication::quit(); + } + else QApplication::quit(); +} + +void MainWindow::editBiasSelection() +{ + bool ok = false; + float bias = QInputDialog::getDouble(this, "Bias Selection", "", 0, INT_MIN, INT_MAX, 1, &ok); + if (ok) + trackView->editBiasValue(bias); +} + +void MainWindow::editSetRows() +{ + bool ok = false; + int rows = QInputDialog::getInt(this, "Set Rows", "", trackView->getRows(), 0, INT_MAX, 1, &ok); + if (ok) + trackView->setRows(rows); +} + +void MainWindow::editPreviousBookmark() +{ + int row = trackView->getDocument()->prevRowBookmark(trackView->getEditRow()); + if (row >= 0) + trackView->setEditRow(row); +} + +void MainWindow::editNextBookmark() +{ + int row = trackView->getDocument()->nextRowBookmark(trackView->getEditRow()); + if (row >= 0) + trackView->setEditRow(row); +} + +void MainWindow::onPosChanged(int col, int row) +{ + setStatusPosition(col, row); + if (trackView->paused && clientSocket.connected()) + clientSocket.sendSetRowCommand(row); +} + +void MainWindow::onCurrValDirty() +{ + SyncDocument *doc = trackView->getDocument(); + if (doc && doc->getTrackCount() > 0) { + const SyncTrack *t = doc->getTrack(doc->getTrackIndexFromPos(trackView->getEditTrack())); + int row = trackView->getEditRow(); + + setStatusValue(t->getValue(row), true); + + const SyncTrack::TrackKey *k = t->getPrevKeyFrame(row); + if (k) + setStatusKeyType(k->type, true); + else + setStatusKeyType(SyncTrack::TrackKey::STEP, false); + } else { + setStatusValue(0.0f, false); + setStatusKeyType(SyncTrack::TrackKey::STEP, false); + } +} + +void MainWindow::processCommand(ClientSocket &sock) +{ + unsigned char cmd = 0; + if (sock.recv((char*)&cmd, 1)) { + switch (cmd) { + case GET_TRACK: + processGetTrack(sock); + break; + + case SET_ROW: + processSetRow(sock); + break; + } + } +} + +void MainWindow::processGetTrack(ClientSocket &sock) +{ + SyncDocument *doc = trackView->getDocument(); + + // read data + int strLen; + sock.recv((char *)&strLen, sizeof(int)); + strLen = qFromBigEndian((quint32)strLen); + if (!sock.connected()) + return; + + if (!strLen) { + sock.disconnect(); + trackView->update(); + return; + } + + QByteArray trackNameBuffer; + trackNameBuffer.resize(strLen); + if (!sock.recv(trackNameBuffer.data(), strLen)) + return; + + if (trackNameBuffer.contains('\0')) { + sock.disconnect(); + trackView->update(); + return; + } + + QString trackName = QString::fromUtf8(trackNameBuffer); + + // find track + const SyncTrack *t = doc->findTrack(trackName.toUtf8()); + if (!t) + t = doc->createTrack(trackName); + + // hook up signals to slots + QObject::connect(t, SIGNAL(keyFrameChanged(const SyncTrack &, int)), + &clientSocket, SLOT(onKeyFrameChanged(const SyncTrack &, int))); + + // setup remap + clientSocket.clientTracks[trackName] = clientIndex++; + + // send key frames + QMap keyMap = t->getKeyMap(); + QMap::const_iterator it; + for (it = keyMap.constBegin(); it != keyMap.constEnd(); ++it) + clientSocket.sendSetKeyCommand(t->name.toUtf8().constData(), *it); + + trackView->update(); +} + +void MainWindow::processSetRow(ClientSocket &sock) +{ + int newRow; + sock.recv((char*)&newRow, sizeof(int)); + trackView->setEditRow(qToBigEndian((quint32)newRow)); +} + +static TcpSocket *clientConnect(QTcpServer *serverSocket, QHostAddress *host) +{ + QTcpSocket *clientSocket = serverSocket->nextPendingConnection(); + Q_ASSERT(clientSocket != NULL); + + QByteArray line; + + // Read greetings or WebSocket upgrade + // command from the socket + for (;;) { + char ch; + if (!clientSocket->getChar(&ch)) { + // Read failed; wait for data and try again + clientSocket->waitForReadyRead(); + if(!clientSocket->getChar(&ch)) { + clientSocket->close(); + return NULL; + } + } + + if (ch == '\n') + break; + if (ch != '\r') + line.push_back(ch); + if (ch == '!') + break; + } + + TcpSocket *ret = NULL; + if (line.startsWith("GET ")) { + ret = WebSocket::upgradeFromHttp(clientSocket); + line.resize(int(strlen(CLIENT_GREET))); + if (!ret || !ret->recv(line.data(), line.size())) { + clientSocket->close(); + return NULL; + } + } else + ret = new TcpSocket(clientSocket); + + if (!line.startsWith(CLIENT_GREET) || + !ret->send(SERVER_GREET, strlen(SERVER_GREET), true)) { + ret->disconnect(); + return NULL; + } + + if (NULL != host) + *host = clientSocket->peerAddress(); + return ret; +} + +void MainWindow::onReadyRead() +{ + while (clientSocket.pollRead()) + processCommand(clientSocket); +} + +void MainWindow::onNewConnection() +{ + if (!clientSocket.connected()) { + setStatusText("Accepting..."); + QHostAddress client; + TcpSocket *socket = clientConnect(serverSocket, &client); + if (socket) { + setStatusText(QString("Connected to %1").arg(client.toString())); + clientSocket.socket = socket; + connect(socket->socket, SIGNAL(readyRead()), this, SLOT(onReadyRead())); + connect(socket->socket, SIGNAL(disconnected()), this, SLOT(onDisconnected())); + clientIndex = 0; + clientSocket.sendPauseCommand(trackView->paused); + clientSocket.sendSetRowCommand(trackView->getEditRow()); + trackView->connected = true; + } else + setStatusText(QString("Not Connected: %1").arg(serverSocket->errorString())); + } +} + +void MainWindow::onDisconnected() +{ + trackView->paused = true; + clientSocket.disconnect(); + + // disconnect track-signals + SyncDocument *doc = trackView->getDocument(); + for (int i = 0; i < doc->getTrackCount(); ++i) + QObject::disconnect(doc->getTrack(i), SIGNAL(keyFrameChanged(const SyncTrack &, int)), + &clientSocket, SLOT(onKeyFrameChanged(const SyncTrack &, int))); + + trackView->update(); + setStatusText("Not Connected."); + trackView->connected = false; +} diff --git a/editor/mainwindow.h b/editor/mainwindow.h new file mode 100644 index 0000000..a3a49dc --- /dev/null +++ b/editor/mainwindow.h @@ -0,0 +1,73 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include "synctrack.h" +#include "clientsocket.h" + +class QLabel; +class QAction; +class QTcpServer; + +class SyncDocument; +class TrackView; +class ClientSocket; + +class MainWindow : public QMainWindow { + Q_OBJECT + +public: + MainWindow(QTcpServer *serverSocket); + void showEvent(QShowEvent *event); + + void createMenuBar(); + void createStatusBar(); + void updateRecentFiles(); + void setCurrentFileName(const QString &fileName); + bool loadDocument(const QString &path); + void setDocument(SyncDocument *newDoc); + + void processCommand(ClientSocket &sock); + void processGetTrack(ClientSocket &sock); + void processSetRow(ClientSocket &sock); + + void setStatusPosition(int row, int col); + void setStatusText(const QString &text); + void setStatusValue(double val, bool valid); + void setStatusKeyType(SyncTrack::TrackKey::KeyType keyType, bool valid); + + QTcpServer *serverSocket; + ClientSocket clientSocket; + size_t clientIndex; + + TrackView *trackView; + QLabel *statusPos, *statusValue, *statusKeyType; + QMenu *fileMenu, *recentFilesMenu, *editMenu; + QAction *recentFileActions[5]; + +public slots: + void fileNew(); + void fileOpen(); + void fileSave(); + void fileSaveAs(); + void fileRemoteExport(); + void openRecentFile(); + void fileQuit(); + + void editBiasSelection(); + + void editSetRows(); + + void editPreviousBookmark(); + void editNextBookmark(); + + void onPosChanged(int col, int row); + void onCurrValDirty(); + +private slots: + void onReadyRead(); + void onNewConnection(); + void onDisconnected(); +}; + +#endif // MAINWINDOW_H diff --git a/editor/recentfiles.cpp b/editor/recentfiles.cpp deleted file mode 100644 index e1a754f..0000000 --- a/editor/recentfiles.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#include "../sync/base.h" -#include "recentfiles.h" -#include "resource.h" -#include - -#define MAX_DIR_LEN 64 - -static bool setRegString(HKEY key, const std::wstring &name, const std::wstring &value) -{ - return ERROR_SUCCESS == RegSetValueExW(key, name.c_str(), 0, REG_SZ, (BYTE *)value.c_str(), (DWORD)(value.size() + 1) * 2); -} - -static bool getRegString(HKEY key, const std::wstring &name, std::wstring &out) -{ - DWORD size = 0; - DWORD type = 0; - if (ERROR_SUCCESS != RegQueryValueExW(key, name.c_str(), 0, &type, (LPBYTE)NULL, &size)) return false; - if (REG_SZ != type) return false; - - assert(!(size % 1)); - out.resize(size / 2); - DWORD ret = RegQueryValueExW(key, name.c_str(), 0, &type, (LPBYTE)&out[0], &size); - while (out.size() > 0 && out[out.size() - 1] == L'\0') out.resize(out.size() - 1); - - assert(ret == ERROR_SUCCESS); - assert(REG_SZ == type); - assert(size == (out.size() + 1) * 2); - - return true; -} - -void RecentFiles::load(HKEY key) -{ - for (size_t i = 0; i < 5; ++i) - { - std::wstring fileName; - if (getRegString(key, getEntryName(i), fileName)) - { - mruList.push_back(fileName); - } - } - - if (mruList.size() > 0) update(); -} - -void RecentFiles::save(HKEY key) -{ - std::list::const_iterator it; - size_t i; - for (i = 0, it = mruList.begin(); it != mruList.end(); ++it, ++i) - { - assert(i <= 5); - setRegString(key, getEntryName(i), *it); - } -} - -void RecentFiles::insert(const std::wstring &fileName) -{ - mruList.remove(fileName); // remove, if present - mruList.push_front(fileName); // add to front - while (mruList.size() > 5) mruList.pop_back(); // remove old entries -} - -void RecentFiles::update() -{ - while (0 != RemoveMenu(mruFileMenu, 0, MF_BYPOSITION)); - std::list::const_iterator it; - size_t i; - for (i = 0, it = mruList.begin(); it != mruList.end(); ++it, ++i) - { - assert(i <= 5); - std::wstring menuEntry = std::wstring(L"&"); - menuEntry += wchar_t(L'1' + i); - menuEntry += L" "; - - wchar_t path[_MAX_PATH], drive[_MAX_DRIVE], dir[_MAX_DIR], fname[_MAX_FNAME], ext[_MAX_EXT]; - _wsplitpath(it->c_str(), drive, dir, fname, ext); - if (wcslen(dir) > MAX_DIR_LEN) wcscpy(dir, L"\\..."); - _wmakepath(path, drive, dir, fname, ext); - menuEntry += std::wstring(path); - - AppendMenuW(mruFileMenu, MF_STRING, ID_RECENTFILES_FILE1 + i, menuEntry.c_str()); - } -} - -bool RecentFiles::getEntry(size_t index, std::wstring &out) const -{ - std::list::const_iterator it; - size_t i; - for (i = 0, it = mruList.begin(); it != mruList.end(); ++it, ++i) - { - if (i == index) - { - out = *it; - return true; - } - } - return false; -} diff --git a/editor/recentfiles.h b/editor/recentfiles.h deleted file mode 100644 index 251f3fe..0000000 --- a/editor/recentfiles.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include -#include - -class RecentFiles -{ -public: - RecentFiles(HMENU menu) : mruFileMenu(menu) { } - - void load(HKEY key); - void save(HKEY key); - void insert(const std::wstring &fileName); - void update(); - - size_t getEntryCount() const - { - return mruList.size(); - } - - bool getEntry(size_t index, std::wstring &out) const; - -private: - static std::wstring getEntryName(size_t i) - { - std::wstring temp = std::wstring(L"RecentFile"); - temp += char(L'0' + i); - return temp; - } - - std::list mruList; - HMENU mruFileMenu; -}; diff --git a/editor/resource.h b/editor/resource.h deleted file mode 100644 index 69770d2..0000000 --- a/editor/resource.h +++ /dev/null @@ -1,40 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by editor.rc -// -#define IDR_ACCELERATOR 101 -#define IDR_MENU 102 -#define IDD_SETROWS 103 -#define IDD_BIASSELECTION 104 -#define IDC_EDIT 1002 -#define IDC_SETROWS_EDIT 1002 -#define IDC_BIASSELECTION_EDIT 1003 -#define ID_FILE 40001 -#define ID_FILE_EXIT 40002 -#define ID_FILE_REMOTEEXPORT 40003 -#define ID_FILE_RECENTFILES 40004 -#define ID_RECENTFILES_NORECENTFILES 40010 -#define ID_RECENTFILES_FILE1 40011 -#define ID_RECENTFILES_FILE2 40012 -#define ID_RECENTFILES_FILE3 40013 -#define ID_RECENTFILES_FILE4 40014 -#define ID_RECENTFILES_FILE5 40015 -#define ID_EDIT 40020 -#define ID_EDIT_SETROWS 40021 -#define ID_EDIT_BIAS 40022 -#define ID_EDIT_SELECTALL 40023 -#define ID_EDIT_SELECTROW 40024 -#define ID_EDIT_SELECTTRACK 40025 -#define ID_EDIT_BOOKMARK_NEXT 40026 -#define ID_EDIT_BOOKMARK_PREV 40027 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 105 -#define _APS_NEXT_COMMAND_VALUE 40028 -#define _APS_NEXT_CONTROL_VALUE 1004 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/editor/syncdocument.cpp b/editor/syncdocument.cpp index 39e16c7..496373e 100644 --- a/editor/syncdocument.cpp +++ b/editor/syncdocument.cpp @@ -1,167 +1,324 @@ #include "syncdocument.h" -#include +#include +#include +#include +#include + SyncDocument::~SyncDocument() { - sync_data_deinit(this); - clearUndoStack(); - clearRedoStack(); + for (int i = 0; i < tracks.size(); ++i) + delete tracks[i]; } -#import named_guids - -SyncDocument *SyncDocument::load(const std::wstring &fileName) +SyncDocument *SyncDocument::load(const QString &fileName) { SyncDocument *ret = new SyncDocument; ret->fileName = fileName; - MSXML2::IXMLDOMDocumentPtr doc(MSXML2::CLSID_DOMDocument); - try { - doc->load(fileName.c_str()); - MSXML2::IXMLDOMNamedNodeMapPtr attribs = doc->documentElement->Getattributes(); - - MSXML2::IXMLDOMNodePtr rowsParam = attribs->getNamedItem("rows"); - if (rowsParam) { - std::string rowsString = rowsParam->Gettext(); - ret->setRows(atoi(rowsString.c_str())); - } - - MSXML2::IXMLDOMNodeListPtr trackNodes = - doc->documentElement->selectNodes("//track"); - for (int i = 0; i < trackNodes->Getlength(); ++i) { - MSXML2::IXMLDOMNodePtr trackNode = trackNodes->Getitem(i); - MSXML2::IXMLDOMNamedNodeMapPtr attribs = trackNode->Getattributes(); - - std::string name = attribs->getNamedItem("name")->Gettext(); - - // look up track-name, create it if it doesn't exist - int trackIndex = sync_find_track(ret, name.c_str()); - if (0 > trackIndex) trackIndex = int(ret->createTrack(name)); - - MSXML2::IXMLDOMNodeListPtr rowNodes = trackNode->GetchildNodes(); - for (int i = 0; i < rowNodes->Getlength(); ++i) { - MSXML2::IXMLDOMNodePtr keyNode = rowNodes->Getitem(i); - std::string baseName = keyNode->GetbaseName(); - if (baseName == "key") { - MSXML2::IXMLDOMNamedNodeMapPtr rowAttribs = keyNode->Getattributes(); - std::string rowString = rowAttribs->getNamedItem("row")->Gettext(); - std::string valueString = rowAttribs->getNamedItem("value")->Gettext(); - std::string interpolationString = rowAttribs->getNamedItem("interpolation")->Gettext(); - - track_key k; - k.row = atoi(rowString.c_str()); - k.value = float(atof(valueString.c_str())); - k.type = key_type(atoi(interpolationString.c_str())); - - assert(!is_key_frame(ret->tracks[trackIndex], k.row)); - if (sync_set_key(ret->tracks[trackIndex], &k)) - throw std::bad_alloc("sync_set_key"); - } - } - } - - MSXML2::IXMLDOMNodeListPtr bookmarkNodes = - doc->documentElement->selectNodes( - "/sync/bookmarks/bookmark"); - for (int i = 0; i < bookmarkNodes->Getlength(); ++i) { - MSXML2::IXMLDOMNodePtr bookmarkNode = - bookmarkNodes->Getitem(i); - MSXML2::IXMLDOMNamedNodeMapPtr bookmarkAttribs = - bookmarkNode->Getattributes(); - std::string str = - bookmarkAttribs->getNamedItem("row")->Gettext(); - int row = atoi(str.c_str()); - ret->toggleRowBookmark(row); - } - } - catch(_com_error &e) - { - char temp[256]; - _snprintf(temp, 256, "Error loading: %s\n", (const char*)_bstr_t(e.Description())); - MessageBox(NULL, temp, NULL, MB_OK | MB_ICONERROR | MB_SETFOREGROUND); + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::critical(NULL, "Error", file.errorString()); return NULL; } - + + QDomDocument doc; + QString err; + if (!doc.setContent(&file, &err)) { + file.close(); + QMessageBox::critical(NULL, "Error", err); + return NULL; + } + file.close(); + + QDomNamedNodeMap attribs = doc.documentElement().attributes(); + QDomNode rowsParam = attribs.namedItem("rows"); + if (!rowsParam.isNull()) { + QString rowsString = rowsParam.nodeValue(); + ret->setRows(rowsString.toInt()); + } + + QDomNodeList trackNodes = + doc.documentElement().elementsByTagName("track"); + for (int i = 0; i < int(trackNodes.length()); ++i) { + QDomNode trackNode = trackNodes.item(i); + QDomNamedNodeMap attribs = trackNode.attributes(); + + QString name = attribs.namedItem("name").nodeValue(); + + // look up track-name, create it if it doesn't exist + SyncTrack *t = ret->findTrack(name.toUtf8()); + if (!t) + t = ret->createTrack(name.toUtf8().constData()); + + QDomNodeList rowNodes = trackNode.childNodes(); + for (int i = 0; i < int(rowNodes.length()); ++i) { + QDomNode keyNode = rowNodes.item(i); + QString baseName = keyNode.nodeName(); + if (baseName == "key") { + QDomNamedNodeMap rowAttribs = keyNode.attributes(); + QString rowString = rowAttribs.namedItem("row").nodeValue(); + QString valueString = rowAttribs.namedItem("value").nodeValue(); + QString interpolationString = rowAttribs.namedItem("interpolation").nodeValue(); + + SyncTrack::TrackKey k; + k.row = rowString.toInt(); + k.value = valueString.toFloat(); + k.type = SyncTrack::TrackKey::KeyType(interpolationString.toInt()); + + Q_ASSERT(!t->isKeyFrame(k.row)); + t->setKey(k); + } + } + } + + // YUCK: gathers from entire document + QDomNodeList bookmarkNodes = + doc.documentElement().elementsByTagName("bookmark"); + for (int i = 0; i < int(bookmarkNodes.length()); ++i) { + QDomNode bookmarkNode = + bookmarkNodes.item(i); + QDomNamedNodeMap bookmarkAttribs = + bookmarkNode.attributes(); + QString str = + bookmarkAttribs.namedItem("row").nodeValue(); + int row = str.toInt(); + ret->toggleRowBookmark(row); + } + return ret; } -bool SyncDocument::save(const std::wstring &fileName) +bool SyncDocument::save(const QString &fileName) { - MSXML2::IXMLDOMDocumentPtr doc(MSXML2::CLSID_DOMDocument); - try { - MSXML2::IXMLDOMElementPtr rootNode = doc->createElement("sync"); - rootNode->setAttribute("rows", getRows()); - doc->appendChild(rootNode); - rootNode->appendChild(doc->createTextNode("\n\t")); + QDomDocument doc; + QDomElement rootNode = doc.createElement("sync"); + rootNode.setAttribute("rows", int(getRows())); + doc.appendChild(rootNode); - MSXML2::IXMLDOMElementPtr tracksNode = - doc->createElement("tracks"); - for (size_t i = 0; i < num_tracks; ++i) { - const sync_track *t = tracks[trackOrder[i]]; + rootNode.appendChild(doc.createTextNode("\n\t")); + QDomElement tracksNode = + doc.createElement("tracks"); + for (int i = 0; i < getTrackCount(); ++i) { + const SyncTrack *t = getTrack(trackOrder[i]); - MSXML2::IXMLDOMElementPtr trackElem = - doc->createElement("track"); - trackElem->setAttribute("name", t->name); + QDomElement trackElem = + doc.createElement("track"); + trackElem.setAttribute("name", t->name); - for (int i = 0; i < (int)t->num_keys; ++i) { - size_t row = t->keys[i].row; - float value = t->keys[i].value; - char interpolationType = char(t->keys[i].type); + QMap keyMap = t->getKeyMap(); + QMap::const_iterator it; + for (it = keyMap.constBegin(); it != keyMap.constEnd(); ++it) { + int row = it.key(); + float value = it->value; + char interpolationType = char(it->type); - MSXML2::IXMLDOMElementPtr keyElem = - doc->createElement("key"); + QDomElement keyElem = + doc.createElement("key"); - keyElem->setAttribute("row", row); - keyElem->setAttribute("value", value); - keyElem->setAttribute("interpolation", - (int)interpolationType); + keyElem.setAttribute("row", row); + keyElem.setAttribute("value", value); + keyElem.setAttribute("interpolation", + (int)interpolationType); - trackElem->appendChild( - doc->createTextNode("\n\t\t\t")); - trackElem->appendChild(keyElem); - } - if (t->num_keys) - trackElem->appendChild( - doc->createTextNode("\n\t\t")); - - tracksNode->appendChild(doc->createTextNode("\n\t\t")); - tracksNode->appendChild(trackElem); + trackElem.appendChild( + doc.createTextNode("\n\t\t\t")); + trackElem.appendChild(keyElem); } - if (0 != num_tracks) - tracksNode->appendChild(doc->createTextNode("\n\t")); - rootNode->appendChild(tracksNode); - rootNode->appendChild(doc->createTextNode("\n\t")); + if (keyMap.size()) + trackElem.appendChild( + doc.createTextNode("\n\t\t")); - MSXML2::IXMLDOMElementPtr bookmarksNode = - doc->createElement("bookmarks"); - std::set::const_iterator it; - for (it = rowBookmarks.begin(); it != rowBookmarks.end(); ++it) { - MSXML2::IXMLDOMElementPtr bookmarkElem = - doc->createElement("bookmark"); - bookmarkElem->setAttribute("row", *it); - - bookmarksNode->appendChild( - doc->createTextNode("\n\t\t")); - bookmarksNode->appendChild(bookmarkElem); - } - if (0 != rowBookmarks.size()) - bookmarksNode->appendChild( - doc->createTextNode("\n\t")); - rootNode->appendChild(bookmarksNode); - rootNode->appendChild(doc->createTextNode("\n")); - - doc->save(fileName.c_str()); - - savePointDelta = 0; - savePointUnreachable = false; + tracksNode.appendChild(doc.createTextNode("\n\t\t")); + tracksNode.appendChild(trackElem); } - catch(_com_error &e) - { - char temp[256]; - _snprintf(temp, 256, "Error saving: %s\n", (const char*)_bstr_t(e.Description())); - MessageBox(NULL, temp, NULL, MB_OK | MB_ICONERROR | MB_SETFOREGROUND); + if (getTrackCount()) + tracksNode.appendChild(doc.createTextNode("\n\t")); + rootNode.appendChild(tracksNode); + rootNode.appendChild(doc.createTextNode("\n\t")); + + QDomElement bookmarksNode = + doc.createElement("bookmarks"); + QList::const_iterator it; + for (it = rowBookmarks.begin(); it != rowBookmarks.end(); ++it) { + QDomElement bookmarkElem = + doc.createElement("bookmark"); + bookmarkElem.setAttribute("row", *it); + + bookmarksNode.appendChild( + doc.createTextNode("\n\t\t")); + bookmarksNode.appendChild(bookmarkElem); + } + if (0 != rowBookmarks.size()) + bookmarksNode.appendChild( + doc.createTextNode("\n\t")); + rootNode.appendChild(bookmarksNode); + rootNode.appendChild(doc.createTextNode("\n")); + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(NULL, "Error", file.errorString()); return false; } + QTextStream streamFileOut(&file); + streamFileOut.setCodec("UTF-8"); + streamFileOut << doc.toString(); + streamFileOut.flush(); + file.close(); + + undoStack.setClean(); return true; } +int SyncDocument::getTrackIndexFromPos(int track) const +{ + Q_ASSERT(track < trackOrder.size()); + return trackOrder[track]; +} + +void SyncDocument::swapTrackOrder(int t1, int t2) +{ + Q_ASSERT(t1 < trackOrder.size()); + Q_ASSERT(t2 < trackOrder.size()); + std::swap(trackOrder[t1], trackOrder[t2]); +} + +bool SyncDocument::isRowBookmark(int row) const +{ + QList::const_iterator it = qLowerBound(rowBookmarks.begin(), rowBookmarks.end(), row); + return it != rowBookmarks.end() && *it == row; +} + +void SyncDocument::toggleRowBookmark(int row) +{ + QList::iterator it = qLowerBound(rowBookmarks.begin(), rowBookmarks.end(), row); + if (it == rowBookmarks.end() || *it != row) + rowBookmarks.insert(it, row); + else + rowBookmarks.erase(it); +} + +int SyncDocument::nextRowBookmark(int row) const +{ + QList::const_iterator it = qLowerBound(rowBookmarks.begin(), rowBookmarks.end(), row); + if (it == rowBookmarks.end()) + return -1; + return *it; +} + +int SyncDocument::prevRowBookmark(int row) const +{ + QList::const_iterator it = qLowerBound(rowBookmarks.begin(), rowBookmarks.end(), row); + if (it == rowBookmarks.end()) { + + // this can only really happen if the list is empty + if (it == rowBookmarks.begin()) + return -1; + + // reached the end, pick the last bookmark if it's after the current row + it--; + return *it < row ? *it : -1; + } + + // pick the previous key (if any) + return it != rowBookmarks.begin() ? *(--it) : -1; +} + +class InsertCommand : public QUndoCommand +{ +public: + InsertCommand(SyncTrack *track, const SyncTrack::TrackKey &key, QUndoCommand *parent = 0) : + QUndoCommand("insert", parent), + track(track), + key(key) + {} + + void redo() + { + Q_ASSERT(!track->isKeyFrame(key.row)); + track->setKey(key); + } + + void undo() + { + Q_ASSERT(track->isKeyFrame(key.row)); + track->removeKey(key.row); + } + +private: + SyncTrack *track; + SyncTrack::TrackKey key; +}; + +class DeleteCommand : public QUndoCommand +{ +public: + DeleteCommand(SyncTrack *track, int row, QUndoCommand *parent = 0) : + QUndoCommand("delete", parent), + track(track), + row(row) + {} + + void redo() + { + Q_ASSERT(track->isKeyFrame(row)); + oldKey = track->getKeyFrame(row); + Q_ASSERT(oldKey.row == row); + track->removeKey(row); + } + + void undo() + { + Q_ASSERT(!track->isKeyFrame(row)); + Q_ASSERT(oldKey.row == row); + track->setKey(oldKey); + } + +private: + SyncTrack *track; + int row; + SyncTrack::TrackKey oldKey; +}; + + +class EditCommand : public QUndoCommand +{ +public: + EditCommand(SyncTrack *track, const SyncTrack::TrackKey &key, QUndoCommand *parent = 0) : + QUndoCommand("edit", parent), + track(track), + key(key) + {} + + void redo() + { + Q_ASSERT(track->isKeyFrame(key.row)); + oldKey = track->getKeyFrame(key.row); + Q_ASSERT(key.row == oldKey.row); + track->setKey(key); + } + + void undo() + { + Q_ASSERT(track->isKeyFrame(oldKey.row)); + Q_ASSERT(key.row == oldKey.row); + track->setKey(oldKey); + } + +private: + SyncTrack *track; + SyncTrack::TrackKey oldKey, key; +}; + +void SyncDocument::setKeyFrame(SyncTrack *track, const SyncTrack::TrackKey &key) +{ + if (track->isKeyFrame(key.row)) + undoStack.push(new EditCommand(track, key)); + else + undoStack.push(new InsertCommand(track, key)); +} + +void SyncDocument::deleteKeyFrame(SyncTrack *track, int row) +{ + undoStack.push(new DeleteCommand(track, row)); +} diff --git a/editor/syncdocument.h b/editor/syncdocument.h index bade169..edb71ca 100644 --- a/editor/syncdocument.h +++ b/editor/syncdocument.h @@ -1,324 +1,106 @@ -/* Copyright (C) 2007-2008 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ +#ifndef SYNCDOCUMENT_H +#define SYNCDOCUMENT_H -#pragma once - -extern "C" { -#include "../sync/data.h" -} - -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include "clientsocket.h" +#include "synctrack.h" -class SyncDocument : public sync_data { +class SyncDocument : public QObject { + Q_OBJECT public: SyncDocument() : - rows(128), savePointDelta(0), savePointUnreachable(false) + rows(128) { - this->tracks = NULL; - this->num_tracks = 0; + QObject::connect(&undoStack, SIGNAL(cleanChanged(bool)), + this, SLOT(cleanChanged(bool))); } ~SyncDocument(); - size_t createTrack(const std::string &name) + SyncTrack *createTrack(const QString &name) { - size_t index = sync_create_track(this, name.c_str()); + SyncTrack *t = new SyncTrack(name); + tracks.append(t); + + int index = tracks.size() - 1; trackOrder.push_back(index); - return index; + Q_ASSERT(trackOrder.size() == tracks.size()); + return t; } - class Command + SyncTrack *getTrack(int index) { - public: - virtual ~Command() {} - virtual void exec(SyncDocument *data) = 0; - virtual void undo(SyncDocument *data) = 0; - }; - - class InsertCommand : public Command - { - public: - InsertCommand(int track, const track_key &key) : track(track), key(key) {} - ~InsertCommand() {} - - void exec(SyncDocument *data) - { - sync_track *t = data->tracks[track]; - assert(!is_key_frame(t, key.row)); - if (sync_set_key(t, &key)) - throw std::bad_alloc("sync_set_key"); - data->clientSocket.sendSetKeyCommand(t->name, key); // update clients - } - - void undo(SyncDocument *data) - { - sync_track *t = data->tracks[track]; - assert(is_key_frame(t, key.row)); - if (sync_del_key(t, key.row)) - throw std::bad_alloc("sync_del_key"); - data->clientSocket.sendDeleteKeyCommand(t->name, key.row); // update clients - } - - private: - int track; - track_key key; - }; - - class DeleteCommand : public Command - { - public: - DeleteCommand(int track, int row) : track(track), row(row) {} - ~DeleteCommand() {} - - void exec(SyncDocument *data) - { - sync_track *t = data->tracks[track]; - int idx = sync_find_key(t, row); - assert(idx >= 0); - oldKey = t->keys[idx]; - if (sync_del_key(t, row)) - throw std::bad_alloc("sync_del_key"); - data->clientSocket.sendDeleteKeyCommand(t->name, row); // update clients - } - - void undo(SyncDocument *data) - { - sync_track *t = data->tracks[track]; - assert(!is_key_frame(t, row)); - if (sync_set_key(t, &oldKey)) - throw std::bad_alloc("sync_set_key"); - data->clientSocket.sendSetKeyCommand(t->name, oldKey); // update clients - } - - private: - int track, row; - struct track_key oldKey; - }; - - - class EditCommand : public Command - { - public: - EditCommand(int track, const track_key &key) : track(track), key(key) {} - ~EditCommand() {} - - void exec(SyncDocument *data) - { - sync_track *t = data->tracks[track]; - int idx = sync_find_key(t, key.row); - assert(idx >= 0); - oldKey = t->keys[idx]; - if (sync_set_key(t, &key)) - throw std::bad_alloc("sync_set_key"); - data->clientSocket.sendSetKeyCommand(t->name, key); // update clients - } - - void undo(SyncDocument *data) - { - sync_track *t = data->tracks[track]; - assert(is_key_frame(t, key.row)); - if (sync_set_key(t, &oldKey)) - throw std::bad_alloc("sync_set_key"); - data->clientSocket.sendSetKeyCommand(t->name, oldKey); // update clients - } - - private: - int track; - track_key oldKey, key; - }; - - class MultiCommand : public Command - { - public: - ~MultiCommand() - { - std::list::iterator it; - for (it = commands.begin(); it != commands.end(); ++it) - { - delete *it; - } - commands.clear(); - } - - void addCommand(Command *cmd) - { - commands.push_back(cmd); - } - - size_t getSize() const { return commands.size(); } - - void exec(SyncDocument *data) - { - std::list::iterator it; - for (it = commands.begin(); it != commands.end(); ++it) (*it)->exec(data); - } - - void undo(SyncDocument *data) - { - std::list::reverse_iterator it; - for (it = commands.rbegin(); it != commands.rend(); ++it) (*it)->undo(data); - } - - private: - std::list commands; - }; - - void exec(Command *cmd) - { - undoStack.push(cmd); - cmd->exec(this); - clearRedoStack(); - - if (savePointDelta < 0) savePointUnreachable = true; - savePointDelta++; - } - - bool undo() - { - if (undoStack.size() == 0) return false; - - Command *cmd = undoStack.top(); - undoStack.pop(); - - redoStack.push(cmd); - cmd->undo(this); - - savePointDelta--; - - return true; - } - - bool redo() - { - if (redoStack.size() == 0) return false; - - Command *cmd = redoStack.top(); - redoStack.pop(); - - undoStack.push(cmd); - cmd->exec(this); - - savePointDelta++; - - return true; - } - - void clearUndoStack() - { - while (!undoStack.empty()) - { - Command *cmd = undoStack.top(); - undoStack.pop(); - delete cmd; - } - } - - void clearRedoStack() - { - while (!redoStack.empty()) - { - Command *cmd = redoStack.top(); - redoStack.pop(); - delete cmd; - } - } - - Command *getSetKeyFrameCommand(int track, const track_key &key) - { - sync_track *t = tracks[track]; - SyncDocument::Command *cmd; - if (is_key_frame(t, key.row)) cmd = new EditCommand(track, key); - else cmd = new InsertCommand(track, key); - return cmd; + Q_ASSERT(index >= 0 && index < tracks.size()); + return tracks[index]; } - size_t getTrackOrderCount() const + const SyncTrack *getTrack(int index) const { - return trackOrder.size(); - } - - size_t getTrackIndexFromPos(size_t track) const - { - assert(track < trackOrder.size()); - return trackOrder[track]; + Q_ASSERT(index >= 0 && index < tracks.size()); + return tracks[index]; } - void swapTrackOrder(size_t t1, size_t t2) + SyncTrack *findTrack(const QString &name) { - assert(t1 < trackOrder.size()); - assert(t2 < trackOrder.size()); - std::swap(trackOrder[t1], trackOrder[t2]); + for (int i = 0; i < tracks.size(); ++i) + if (name == tracks[i]->name) + return tracks[i]; + return NULL; } - static SyncDocument *load(const std::wstring &fileName); - bool save(const std::wstring &fileName); - - bool modified() const + int getTrackCount() const { - if (savePointUnreachable) return true; - return 0 != savePointDelta; + Q_ASSERT(trackOrder.size() == tracks.size()); + return tracks.size(); } - bool isRowBookmark(int row) const - { - return !!rowBookmarks.count(row); - } + void undo() { undoStack.undo(); } + void redo() { undoStack.redo(); } + bool isModified() const { return !undoStack.isClean(); } + bool canUndo () const { return undoStack.canUndo(); } + bool canRedo () const { return undoStack.canRedo(); } - void toggleRowBookmark(int row) - { - if (isRowBookmark(row)) - rowBookmarks.erase(row); - else - rowBookmarks.insert(row); - } + void beginMacro(const QString &text) { undoStack.beginMacro(text); } + void setKeyFrame(SyncTrack *track, const SyncTrack::TrackKey &key); + void deleteKeyFrame(SyncTrack *track, int row); + void endMacro() { undoStack.endMacro(); } - ClientSocket clientSocket; + int getTrackIndexFromPos(int track) const; + void swapTrackOrder(int t1, int t2); - size_t getRows() const { return rows; } - void setRows(size_t rows) { this->rows = rows; } + static SyncDocument *load(const QString &fileName); + bool save(const QString &fileName); - std::wstring fileName; + bool isRowBookmark(int row) const; + void toggleRowBookmark(int row); - int nextRowBookmark(int row) const - { - std::set::const_iterator it = rowBookmarks.upper_bound(row); - if (it == rowBookmarks.end()) - return -1; - return *it; - } + int getRows() const { return rows; } + void setRows(int rows) { this->rows = rows; } - int prevRowBookmark(int row) const - { - std::set::const_iterator it = rowBookmarks.lower_bound(row); - if (it == rowBookmarks.end()) { - std::set::const_reverse_iterator it = rowBookmarks.rbegin(); - if (it == rowBookmarks.rend()) - return -1; - return *it; - } else - it--; - if (it == rowBookmarks.end()) - return -1; - return *it; - } + QString fileName; + + int nextRowBookmark(int row) const; + int prevRowBookmark(int row) const; private: - std::set rowBookmarks; - std::vector trackOrder; - size_t rows; + QList tracks; + QList rowBookmarks; + QVector trackOrder; + int rows; - // undo / redo functionality - std::stack undoStack; - std::stack redoStack; - int savePointDelta; // how many undos must be done to get to the last saved state - bool savePointUnreachable; // is the save-point reachable? + QUndoStack undoStack; +signals: + void modifiedChanged(bool modified); + +private slots: + void cleanChanged(bool clean) { emit modifiedChanged(!clean); } }; + +#endif // !defined(SYNCDOCUMENT_H) diff --git a/editor/synctrack.h b/editor/synctrack.h new file mode 100644 index 0000000..03c8c3e --- /dev/null +++ b/editor/synctrack.h @@ -0,0 +1,152 @@ +#ifndef SYNCTRACK_H +#define SYNCTRACK_H + +#include +#include + +class SyncTrack : public QObject { + Q_OBJECT +public: + SyncTrack(const QString &name) : + name(name) + { + } + + struct TrackKey { + int row; + float value; + enum KeyType { + STEP, /* stay constant */ + LINEAR, /* lerp to the next value */ + SMOOTH, /* smooth curve to the next value */ + RAMP, /* ramp up */ + KEY_TYPE_COUNT + } type; + }; + + void setKey(const TrackKey &key) + { + keys[key.row] = key; + emit keyFrameChanged(*this, key.row); + } + + void removeKey(int row) + { + Q_ASSERT(keys.find(row) != keys.end()); + keys.remove(row); + emit keyFrameChanged(*this, row); + } + + bool isKeyFrame(int row) const + { + QMap::const_iterator it = keys.lowerBound(row); + return it != keys.end() && it.key() == row; + } + + TrackKey getKeyFrame(int row) const + { + Q_ASSERT(isKeyFrame(row)); + QMap::const_iterator it = keys.lowerBound(row); + return it.value(); + } + + const TrackKey *getPrevKeyFrame(int row) const + { + QMap::const_iterator it = keys.lowerBound(row); + if (it != keys.constBegin() && (it == keys.constEnd() || it.key() != row)) + --it; + + if (it == keys.constEnd() || it.key() > row) + return NULL; + + return &it.value(); + } + + const TrackKey *getNextKeyFrame(int row) const + { + QMap::const_iterator it = keys.lowerBound(row); + + if (it == keys.constEnd() || it.key() < row) + return NULL; + + return &it.value(); + } + + static void getPolynomial(float coeffs[4], const TrackKey *key) + { + coeffs[0] = key->value; + switch (key->type) { + case TrackKey::STEP: + coeffs[1] = coeffs[2] = coeffs[3] = 0.0f; + break; + + case TrackKey::LINEAR: + coeffs[1] = 1.0f; + coeffs[2] = coeffs[3] = 0.0f; + break; + + case TrackKey::SMOOTH: + coeffs[1] = 0.0f; + coeffs[2] = 3.0f; + coeffs[3] = -2.0f; + break; + + case TrackKey::RAMP: + coeffs[1] = coeffs[3] = 0.0f; + coeffs[2] = 1.0f; + break; + + default: + Q_ASSERT(0); + coeffs[0] = 0.0f; + coeffs[1] = 0.0f; + coeffs[2] = 0.0f; + coeffs[3] = 0.0f; + } + } + + double getValue(int row) const + { + if (!keys.size()) + return 0.0; + + const TrackKey *prevKey = getPrevKeyFrame(row); + const TrackKey *nextKey = getNextKeyFrame(row); + + Q_ASSERT(prevKey != NULL || nextKey != NULL); + + if (!prevKey) + return nextKey->value; + if (!nextKey) + return prevKey->value; + if (prevKey == nextKey) + return prevKey->value; + + float coeffs[4]; + getPolynomial(coeffs, prevKey); + + float x = double(row - prevKey->row) / + double(nextKey->row - prevKey->row); + float mag = nextKey->value - prevKey->value; + return coeffs[0] + (coeffs[1] + (coeffs[2] + coeffs[3] * x) * x) * x * mag; + } + + const QMap getKeyMap() const + { + return keys; + } + + bool isActive() const + { + return receivers(SIGNAL(keyFrameChanged(const SyncTrack &, int))) > 0; + } + + QString name; +private: + QMap keys; + +signals: + void keyFrameChanged(const SyncTrack &track, int row); +}; + +#endif // !defined(SYNCTRACK_H) diff --git a/editor/trackview.cpp b/editor/trackview.cpp index db62e39..d40b29a 100644 --- a/editor/trackview.cpp +++ b/editor/trackview.cpp @@ -1,121 +1,120 @@ -/* Copyright (C) 2007-2008 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ - #include "trackview.h" -#include -#include -#include +#include "syncdocument.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include -using std::min; -using std::max; - -static const char *trackViewWindowClassName = "TrackView"; - -static DWORD darken(DWORD col, float amt) +TrackView::TrackView(QWidget *parent) : + QAbstractScrollArea(parent), + paused(true), + connected(false), + windowRows(0), + document(NULL), + dragging(false) { - return RGB(GetRValue(col) * amt, GetGValue(col) * amt, GetBValue(col) * amt); -} +#ifdef Q_OS_WIN + setFont(QFont("Fixedsys")); +#else + QFont font("Monospace"); + font.setStyleHint(QFont::TypeWriter); + setFont(font); +#endif -static int getMaxCharacterWidth(HDC hdc, const char *chars, size_t len) -{ - int maxDigitWidth = 0; - for (size_t i = 0; i < len; ++i) - { - SIZE size; - GetTextExtentPoint32(hdc, &chars[i], 1, &size); - maxDigitWidth = max(maxDigitWidth, int(size.cx)); - } - return maxDigitWidth; -} + lineEdit = new QLineEdit(this); + lineEdit->setAutoFillBackground(true); + lineEdit->hide(); + QDoubleValidator *lineEditValidator = new QDoubleValidator(); + lineEditValidator->setNotation(QDoubleValidator::StandardNotation); + lineEditValidator->setLocale(QLocale::c()); + lineEdit->setValidator(lineEditValidator); -static int getMaxCharacterWidthFromString(HDC hdc, const char *chars) -{ - return getMaxCharacterWidth(hdc, chars, strlen(chars)); -} + QObject::connect(lineEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished())); + + viewport()->setAutoFillBackground(false); + + setFocus(Qt::OtherFocusReason); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); -TrackView::TrackView() : - document(NULL) -{ scrollPosX = 0; scrollPosY = 0; - windowWidth = -1; - windowHeight = -1; - + editRow = 0; editTrack = 0; selectStartTrack = selectStopTrack = 0; selectStartRow = selectStopRow = 0; - - this->hwnd = NULL; - - bgBaseBrush = GetSysColorBrush(COLOR_WINDOW); - bgDarkBrush = CreateSolidBrush(darken(GetSysColor(COLOR_WINDOW), 0.9f)); - - selectBaseBrush = GetSysColorBrush(COLOR_HIGHLIGHT); - selectDarkBrush = CreateSolidBrush(darken(GetSysColor(COLOR_HIGHLIGHT), 0.9f)); - - rowPen = CreatePen(PS_SOLID, 1, darken(GetSysColor(COLOR_WINDOW), 0.7f)); - rowSelectPen = CreatePen(PS_SOLID, 1, darken(GetSysColor(COLOR_HIGHLIGHT), 0.7f)); - - lerpPen = CreatePen(PS_SOLID, 2, RGB(255, 0, 0)); - cosinePen = CreatePen(PS_SOLID, 2, RGB(0, 255, 0)); - rampPen = CreatePen(PS_SOLID, 2, RGB(0, 0, 255)); - editBrush = CreateSolidBrush(RGB(255, 255, 0)); // yellow - bookmarkBrush = CreateSolidBrush(RGB(128, 128, 255)); // red + updateFont(); + updatePalette(); - handCursor = LoadCursor(NULL, IDC_HAND); + handCursor = QCursor(Qt::OpenHandCursor); + setMouseTracking(true); - clipboardFormat = RegisterClipboardFormat("syncdata"); - assert(0 != clipboardFormat); + setupScrollBars(); + QObject::connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(onHScroll(int))); + QObject::connect(verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(onVScroll(int))); +} + +void TrackView::updatePalette() +{ + bgBaseBrush = palette().base(); + bgDarkBrush = palette().window(); + + selectBaseBrush = palette().highlight(); + selectDarkBrush = palette().highlight().color().darker(100.0 / 0.9); + + rowPen = QPen(QBrush(palette().base().color().darker(100.0 / 0.7)), 1); + rowSelectPen = QPen(QBrush(palette().highlight().color().darker(100.0 / 0.7)), 1); + + lerpPen = QPen(QBrush(QColor(255, 0, 0)), 2); + cosinePen = QPen(QBrush(QColor(0, 255, 0)), 2); + rampPen = QPen(QBrush(QColor(0, 0, 255)), 2); + + editBrush = Qt::yellow; + bookmarkBrush = QColor(128, 128, 255); +} + +void TrackView::updateFont() +{ + rowHeight = fontMetrics().lineSpacing(); + trackWidth = fontMetrics().width('0') * 16; + + topMarginHeight = rowHeight + 4; + leftMarginWidth = fontMetrics().width('0') * 8; } TrackView::~TrackView() { - DeleteObject(bgBaseBrush); - DeleteObject(bgDarkBrush); - DeleteObject(selectBaseBrush); - DeleteObject(selectDarkBrush); - DeleteObject(lerpPen); - DeleteObject(cosinePen); - DeleteObject(rampPen); - DeleteObject(editBrush); - DeleteObject(bookmarkBrush); - DeleteObject(rowPen); - DeleteObject(rowSelectPen); if (document) delete document; } -void TrackView::setFont(HFONT font) +int TrackView::getLogicalX(int track) const { - this->font = font; - - assert(NULL != hwnd); - HDC hdc = GetDC(hwnd); - SelectObject(hdc, font); - - TEXTMETRIC tm; - GetTextMetrics(hdc, &tm); - - rowHeight = tm.tmHeight + tm.tmExternalLeading; - fontWidth = tm.tmAveCharWidth; - trackWidth = getMaxCharacterWidthFromString(hdc, "0123456789.") * 16; - - topMarginHeight = rowHeight + 4; - leftMarginWidth = getMaxCharacterWidthFromString(hdc, "0123456789abcdefh") * 8; + return track * trackWidth; } -int TrackView::getScreenY(int row) const +int TrackView::getLogicalY(int row) const { - return topMarginHeight + (row * rowHeight) - scrollPosY; + return row * rowHeight; } -int TrackView::getScreenX(size_t track) const +int TrackView::getPhysicalX(int track) const { - return int(leftMarginWidth + (track * trackWidth)) - scrollPosX; + return leftMarginWidth + getLogicalX(track) - scrollPosX; +} + +int TrackView::getPhysicalY(int row) const +{ + return topMarginHeight + getLogicalY(row) - scrollPosY; } inline int divfloor(int a, int b) @@ -125,272 +124,291 @@ inline int divfloor(int a, int b) return a / b; } -int TrackView::getTrackFromX(int x) const +int TrackView::getTrackFromLogicalX(int x) const { - return divfloor(x + scrollPosX - leftMarginWidth, trackWidth); + return divfloor(x, trackWidth); } - -LRESULT TrackView::onCreate() +int TrackView::getTrackFromPhysicalX(int x) const { -// setFont((HFONT)GetStockObject(SYSTEM_FONT)); - setFont((HFONT)GetStockObject(SYSTEM_FIXED_FONT)); - - setupScrollBars(); - return FALSE; + return getTrackFromLogicalX(x - leftMarginWidth + scrollPosX); } -LRESULT TrackView::onPaint() +void TrackView::paintEvent(QPaintEvent *event) { - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - -// SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT)); - SelectObject(hdc, font); - paintTracks(hdc, ps.rcPaint); - - EndPaint(hwnd, &ps); - - return FALSE; + QPainter painter(this->viewport()); + paintTopMargin(painter, event->rect()); + paintLeftMargin(painter, event->rect()); + paintTracks(painter, event->rect()); } -void TrackView::paintTopMargin(HDC hdc, RECT rcTracks) +void TrackView::paintTopMargin(QPainter &painter, const QRect &rcTracks) { - RECT fillRect; - RECT topLeftMargin; + QRect topLeftMargin; const SyncDocument *doc = getDocument(); if (NULL == doc) return; - topLeftMargin.top = 0; - topLeftMargin.bottom = topMarginHeight; - topLeftMargin.left = 0; - topLeftMargin.right = leftMarginWidth; - fillRect = topLeftMargin; - DrawEdge(hdc, &fillRect, BDR_RAISEDINNER | BDR_RAISEDOUTER, BF_ADJUST | BF_BOTTOM); - FillRect(hdc, &fillRect, GetSysColorBrush(COLOR_3DFACE)); - - int startTrack = scrollPosX / trackWidth; - int endTrack = min(startTrack + windowTracks + 1, int(getTrackCount())); - - SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT)); - + topLeftMargin.setTop(-1); + topLeftMargin.setBottom(topMarginHeight - 1); + topLeftMargin.setLeft(-1); + topLeftMargin.setRight(leftMarginWidth + 1); + painter.fillRect(topLeftMargin, palette().button()); + qDrawWinButton(&painter, topLeftMargin, palette()); + + QRect topRightMargin; + topRightMargin.setTop(-1); + topRightMargin.setBottom(topMarginHeight - 1); + topRightMargin.setLeft(getPhysicalX(getTrackCount()) - 1); + topRightMargin.setRight(rcTracks.right() + 1); + painter.fillRect(topRightMargin, palette().button()); + qDrawWinButton(&painter, topRightMargin, palette()); + + int startTrack = qBound(0, getTrackFromPhysicalX(qMax(rcTracks.left(), leftMarginWidth)), int(getTrackCount())); + int endTrack = qBound(0, getTrackFromPhysicalX(rcTracks.right()) + 1, int(getTrackCount())); + for (int track = startTrack; track < endTrack; ++track) { - size_t index = doc->getTrackIndexFromPos(track); - const sync_track *t = doc->tracks[index]; + int index = doc->getTrackIndexFromPos(track); + const SyncTrack *t = doc->getTrack(index); - RECT topMargin; - topMargin.top = 0; - topMargin.bottom = topMarginHeight; - topMargin.left = getScreenX(track); - topMargin.right = topMargin.left + trackWidth; - - if (!RectVisible(hdc, &topMargin)) + QRect topMargin(getPhysicalX(track), 0, trackWidth, topMarginHeight); + if (!rcTracks.intersects(topMargin)) continue; - RECT fillRect = topMargin; + QRect fillRect = topMargin; - HBRUSH bgBrush = GetSysColorBrush(COLOR_3DFACE); + QBrush bgBrush = palette().button(); if (track == editTrack) bgBrush = editBrush; - DrawEdge(hdc, &fillRect, BDR_RAISEDINNER | BDR_RAISEDOUTER, BF_ADJUST | BF_LEFT | BF_RIGHT | BF_BOTTOM); - FillRect(hdc, &fillRect, bgBrush); + painter.fillRect(fillRect, bgBrush); + qDrawWinButton(&painter, fillRect, palette()); - if (!doc->clientSocket.clientTracks.count(t->name)) - SetTextColor(hdc, GetSysColor(COLOR_GRAYTEXT)); + if (!t->isActive()) + painter.setPen(QColor(128, 128, 128)); else - SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT)); - TextOut(hdc, fillRect.left, 0, t->name, int(strlen(t->name))); + painter.setPen(QColor(0, 0, 0)); + + painter.drawText(fillRect, t->name); } - - RECT topRightMargin; - topRightMargin.top = 0; - topRightMargin.bottom = topMarginHeight; - topRightMargin.left = getScreenX(getTrackCount()); - topRightMargin.right = rcTracks.right; - fillRect = topRightMargin; - DrawEdge(hdc, &fillRect, BDR_RAISEDINNER | BDR_RAISEDOUTER, BF_ADJUST | BF_BOTTOM); - FillRect(hdc, &fillRect, GetSysColorBrush(COLOR_3DFACE)); - + // make sure that the top margin isn't overdrawn by the track-data - ExcludeClipRect(hdc, 0, 0, rcTracks.right, topMarginHeight); + painter.setClipRegion(QRect(0, topMarginHeight, rcTracks.right() + 1, rcTracks.bottom() + 1)); } -void TrackView::paintTracks(HDC hdc, RECT rcTracks) +void TrackView::paintLeftMargin(QPainter &painter, const QRect &rcTracks) { const SyncDocument *doc = getDocument(); if (NULL == doc) return; - - char temp[256]; - + int firstRow = editRow - windowRows / 2 - 1; int lastRow = editRow + windowRows / 2 + 1; - - /* clamp first & last row */ - firstRow = min(max(firstRow, 0), int(getRows()) - 1); - lastRow = min(max(lastRow, 0), int(getRows()) - 1); - - SetBkMode(hdc, TRANSPARENT); - paintTopMargin(hdc, rcTracks); - - for (int row = firstRow; row <= lastRow; ++row) - { - RECT leftMargin; - leftMargin.left = 0; - leftMargin.right = leftMarginWidth; - leftMargin.top = getScreenY(row); - leftMargin.bottom = leftMargin.top + rowHeight; - - if (!RectVisible(hdc, &leftMargin)) continue; - HBRUSH fillBrush; + /* clamp first & last row */ + firstRow = qBound(0, firstRow, int(getRows()) - 1); + lastRow = qBound(0, lastRow, int(getRows()) - 1); + + for (int row = firstRow; row <= lastRow; ++row) { + QRect leftMargin(0, getPhysicalY(row), leftMarginWidth, rowHeight); + if (!rcTracks.intersects(leftMargin)) + continue; + + QBrush fillBrush; if (row == editRow) fillBrush = editBrush; else if (doc->isRowBookmark(row)) fillBrush = bookmarkBrush; else - fillBrush = GetSysColorBrush(COLOR_3DFACE); - FillRect(hdc, &leftMargin, fillBrush); + fillBrush = palette().button(); + painter.fillRect(leftMargin, fillBrush); - DrawEdge(hdc, &leftMargin, BDR_RAISEDINNER | BDR_RAISEDOUTER, BF_RIGHT | BF_BOTTOM | BF_TOP); - if ((row % 8) == 0) SetTextColor(hdc, RGB(0, 0, 0)); - else if ((row % 4) == 0) SetTextColor(hdc, RGB(64, 64, 64)); - else SetTextColor(hdc, RGB(128, 128, 128)); - -/* if ((row % 4) == 0) SetTextColor(hdc, GetSysColor(COLOR_BTNTEXT)); - else SetTextColor(hdc, GetSysColor(COLOR_GRAYTEXT)); */ - - snprintf(temp, 256, "%0*Xh", 5, row); - TextOut(hdc, - leftMargin.left, leftMargin.top, - temp, int(strlen(temp)) - ); + qDrawWinButton(&painter, leftMargin, palette()); + if ((row % 8) == 0) painter.setPen(QColor(0, 0, 0)); + else if ((row % 4) == 0) painter.setPen(QColor(64, 64, 64)); + else painter.setPen(QColor(128, 128, 128)); + + painter.drawText(leftMargin, QString("%1").arg(row, 5, 16, QChar('0')).toUpper() + "h"); } - - SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT)); - - int selectLeft = min(selectStartTrack, selectStopTrack); - int selectRight = max(selectStartTrack, selectStopTrack); - int selectTop = min(selectStartRow, selectStopRow); - int selectBottom = max(selectStartRow, selectStopRow); - - int startTrack = scrollPosX / trackWidth; - int endTrack = min(startTrack + windowTracks + 1, int(getTrackCount())); - - for (int track = startTrack; track < endTrack; ++track) { - const sync_track *t = doc->tracks[doc->getTrackIndexFromPos(track)]; - for (int row = firstRow; row <= lastRow; ++row) { - RECT patternDataRect; - patternDataRect.left = getScreenX(track); - patternDataRect.right = patternDataRect.left + trackWidth; - patternDataRect.top = getScreenY(row); - patternDataRect.bottom = patternDataRect.top + rowHeight; - - if (!RectVisible(hdc, &patternDataRect)) - continue; +} - int idx = sync_find_key(t, row); - int fidx = idx >= 0 ? idx : -idx - 2; - key_type interpolationType = (fidx >= 0) ? t->keys[fidx].type : KEY_STEP; - bool selected = (track >= selectLeft && track <= selectRight) && (row >= selectTop && row <= selectBottom); +void TrackView::paintTracks(QPainter &painter, const QRect &rcTracks) +{ + const SyncDocument *doc = getDocument(); + if (NULL == doc) return; - HBRUSH baseBrush = bgBaseBrush; - HBRUSH darkBrush = bgDarkBrush; - if (selected) - { - baseBrush = selectBaseBrush; - darkBrush = selectDarkBrush; - } - - HBRUSH bgBrush = baseBrush; - if (row % 8 == 0) bgBrush = darkBrush; - - RECT fillRect = patternDataRect; - FillRect( hdc, &fillRect, bgBrush); - if (row % 8 == 0) { - MoveToEx(hdc, patternDataRect.left, patternDataRect.top, (LPPOINT) NULL); - if (selected) SelectObject(hdc, rowSelectPen); - else SelectObject(hdc, rowPen); - LineTo(hdc, patternDataRect.right, patternDataRect.top); - } - - switch (interpolationType) { - case KEY_STEP: - break; - case KEY_LINEAR: - SelectObject(hdc, lerpPen); - break; - case KEY_SMOOTH: - SelectObject(hdc, cosinePen); - break; - case KEY_RAMP: - SelectObject(hdc, rampPen); - break; - } - if (interpolationType != KEY_STEP) { - MoveToEx(hdc, patternDataRect.right - 1, patternDataRect.top, (LPPOINT) NULL); - LineTo(hdc, patternDataRect.right - 1, patternDataRect.bottom); - } + int firstRow = editRow - windowRows / 2 - 1; + int lastRow = editRow + windowRows / 2 + 1; - bool drawEditString = false; - if (row == editRow && track == editTrack) { - FrameRect(hdc, &fillRect, (HBRUSH)GetStockObject(BLACK_BRUSH)); - if (editString.size() > 0) - drawEditString = true; - } - /* format the text */ - if (drawEditString) - snprintf(temp, 256, editString.c_str()); - else if (idx < 0) - snprintf(temp, 256, " ---"); - else { - float val = t->keys[idx].value; - snprintf(temp, 256, "% .2f", val); - } + /* clamp first & last row */ + firstRow = qBound(0, firstRow, int(getRows()) - 1); + lastRow = qBound(0, lastRow, int(getRows()) - 1); - COLORREF oldCol; - if (selected) oldCol = SetTextColor(hdc, GetSysColor(COLOR_HIGHLIGHTTEXT)); - TextOut(hdc, - patternDataRect.left, patternDataRect.top, - temp, int(strlen(temp)) - ); - if (selected) SetTextColor(hdc, oldCol); + int startTrack = qBound(0, getTrackFromPhysicalX(qMax(rcTracks.left(), leftMarginWidth)), int(getTrackCount())); + int endTrack = qBound(0, getTrackFromPhysicalX(rcTracks.right()) + 1, int(getTrackCount())); + + QRect topPadding(QPoint(rcTracks.left(), qMax(int(rcTracks.top()), topMarginHeight)), + QPoint(rcTracks.right(), getPhysicalY(0) - 1)); + painter.fillRect(topPadding, palette().dark()); + + QRect bottomPadding(QPoint(rcTracks.left(), getPhysicalY(int(getRows()))), + QPoint(rcTracks.right(), rcTracks.bottom())); + painter.fillRect(bottomPadding, palette().dark()); + + painter.setClipRect(leftMarginWidth, + topMarginHeight, + viewport()->width() - leftMarginWidth, + viewport()->height() - topMarginHeight); + + for (int track = startTrack; track < endTrack; ++track) + paintTrack(painter, rcTracks, track); + + QRect rightMargin(QPoint(getPhysicalX(getTrackCount()), getPhysicalY(0)), + QPoint(rcTracks.right(), getPhysicalY(int(getRows())) - 1)); + painter.fillRect(rightMargin, palette().dark()); +} + +static QPen getInterpolationBrush(SyncTrack::TrackKey::KeyType type) +{ + switch (type) { + case SyncTrack::TrackKey::STEP: + return QPen(); + + case SyncTrack::TrackKey::LINEAR: + return QPen(QBrush(QColor(255, 0, 0)), 2); + + case SyncTrack::TrackKey::SMOOTH: + return QPen(QBrush(QColor(0, 255, 0)), 2); + + case SyncTrack::TrackKey::RAMP: + return QPen(QBrush(QColor(0, 0, 255)), 2); + + default: + Q_ASSERT(false); + return QPen(); + } +} + +void TrackView::paintTrack(QPainter &painter, const QRect &rcTracks, int track) +{ + int firstRow = editRow - windowRows / 2 - 1; + int lastRow = editRow + windowRows / 2 + 1; + + /* clamp first & last row */ + firstRow = qBound(0, firstRow, int(getRows()) - 1); + lastRow = qBound(0, lastRow, int(getRows()) - 1); + + QRect selection = getSelection(); + + const SyncTrack *t = getDocument()->getTrack(getDocument()->getTrackIndexFromPos(track)); + QMap keyMap = t->getKeyMap(); + + for (int row = firstRow; row <= lastRow; ++row) { + QRect patternDataRect(getPhysicalX(track), getPhysicalY(row), trackWidth, rowHeight); + if (!rcTracks.intersects(patternDataRect)) + continue; + + QMap::const_iterator it = keyMap.lowerBound(row); + if (it != keyMap.constBegin() && it.key() != row) + --it; + + SyncTrack::TrackKey::KeyType interpolationType = + (it != keyMap.constEnd() && it.key() <= row) ? + it->type : SyncTrack::TrackKey::STEP; + bool selected = selection.contains(track, row); + + QBrush baseBrush = bgBaseBrush; + QBrush darkBrush = bgDarkBrush; + + if (selected) { + baseBrush = selectBaseBrush; + darkBrush = selectDarkBrush; } + + QBrush bgBrush = (row % 8 == 0) ? darkBrush : baseBrush; + + QRect fillRect = patternDataRect; + painter.fillRect(fillRect, bgBrush); + if (row % 8 == 0) { + painter.setPen(selected ? rowSelectPen : rowPen); + painter.drawLine(patternDataRect.topLeft(), + patternDataRect.topRight()); + } + + if (interpolationType != SyncTrack::TrackKey::STEP) { + painter.setPen(getInterpolationBrush(interpolationType)); + painter.drawLine(patternDataRect.topRight(), + patternDataRect.bottomRight()); + } + + if (row == editRow && track == editTrack) { + painter.setPen(QColor(0, 0, 0)); + painter.drawRect(fillRect.x(), fillRect.y(), fillRect.width() - 1, fillRect.height() - 1); + } + + painter.setPen(selected ? + palette().color(QPalette::HighlightedText) : + palette().color(QPalette::WindowText)); + painter.drawText(patternDataRect, t->isKeyFrame(row) ? + QString::number(t->getKeyFrame(row).value, 'f', 2) : + " ---"); } - - /* right margin */ - { - RECT rightMargin; - rightMargin.top = getScreenY(0); - rightMargin.bottom = getScreenY(int(getRows())); - rightMargin.left = getScreenX(getTrackCount()); - rightMargin.right = rcTracks.right; - FillRect( hdc, &rightMargin, GetSysColorBrush(COLOR_APPWORKSPACE)); +} + +void TrackView::mouseMoveEvent(QMouseEvent *event) +{ + int track = getTrackFromPhysicalX(event->pos().x()); + if (dragging) { + SyncDocument *doc = getDocument(); + const int trackCount = getTrackCount(); + + if (!doc || track < 0 || track >= trackCount) + return; + + if (track > anchorTrack) { + for (int i = anchorTrack; i < track; ++i) + doc->swapTrackOrder(i, i + 1); + anchorTrack = track; + setEditTrack(track); + viewport()->update(); + } else if (track < anchorTrack) { + for (int i = anchorTrack; i > track; --i) + doc->swapTrackOrder(i, i - 1); + anchorTrack = track; + setEditTrack(track); + viewport()->update(); + } + } else { + if (event->pos().y() < topMarginHeight && + track >= 0 && track < int(getTrackCount())) { + setCursor(handCursor); + } else + setCursor(QCursor(Qt::ArrowCursor)); } - - { - RECT bottomPadding; - bottomPadding.top = getScreenY(int(getRows())); - bottomPadding.bottom = rcTracks.bottom; - bottomPadding.left = rcTracks.left; - bottomPadding.right = rcTracks.right; - FillRect(hdc, &bottomPadding, GetSysColorBrush(COLOR_APPWORKSPACE)); +} + +void TrackView::mousePressEvent(QMouseEvent *event) +{ + int track = getTrackFromPhysicalX(event->pos().x()); + if (event->button() == Qt::LeftButton && + event->pos().y() < topMarginHeight && + track >= 0 && track < int(getTrackCount())) { + dragging = true; + anchorTrack = track; } - - { - RECT topPadding; - topPadding.top = max(int(rcTracks.top), topMarginHeight); - topPadding.bottom = getScreenY(0); - topPadding.left = rcTracks.left; - topPadding.right = rcTracks.right; - FillRect(hdc, &topPadding, GetSysColorBrush(COLOR_APPWORKSPACE)); +} + +void TrackView::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + dragging = false; + setCursor(QCursor(Qt::ArrowCursor)); + setEditTrack(editTrack); } } struct CopyEntry { int track; - track_key keyFrame; + SyncTrack::TrackKey keyFrame; }; void TrackView::editCopy() @@ -399,69 +417,52 @@ void TrackView::editCopy() if (NULL == doc) return; if (0 == getTrackCount()) { - MessageBeep(~0U); - return; - } - - int selectLeft = min(selectStartTrack, selectStopTrack); - int selectRight = max(selectStartTrack, selectStopTrack); - int selectTop = min(selectStartRow, selectStopRow); - int selectBottom = max(selectStartRow, selectStopRow); - - if (FAILED(OpenClipboard(getWin()))) - { - MessageBox(NULL, "Failed to open clipboard", NULL, MB_OK); + QApplication::beep(); return; } - std::vector copyEntries; - for (int track = selectLeft; track <= selectRight; ++track) { - const size_t trackIndex = doc->getTrackIndexFromPos(track); - const sync_track *t = doc->tracks[trackIndex]; + QRect selection = getSelection(); - for (int row = selectTop; row <= selectBottom; ++row) { - int idx = sync_find_key(t, row); - if (idx >= 0) { + QVector copyEntries; + for (int track = selection.left(); track <= selection.right(); ++track) { + const int trackIndex = doc->getTrackIndexFromPos(track); + const SyncTrack *t = doc->getTrack(trackIndex); + + for (int row = selection.top(); row <= selection.bottom(); ++row) { + if (t->isKeyFrame(row)) { CopyEntry ce; - ce.track = track - selectLeft; - ce.keyFrame = t->keys[idx]; - ce.keyFrame.row -= selectTop; + ce.track = track - selection.left(); + ce.keyFrame = t->getKeyFrame(row); + ce.keyFrame.row -= selection.top(); copyEntries.push_back(ce); } } } - int buffer_width = selectRight - selectLeft + 1; - int buffer_height = selectBottom - selectTop + 1; + int buffer_width = selection.width(); + int buffer_height = selection.height(); size_t buffer_size = copyEntries.size(); - - HGLOBAL hmem = GlobalAlloc(GMEM_MOVEABLE, sizeof(int) * 3 + sizeof(CopyEntry) * copyEntries.size()); - char *clipbuf = (char *)GlobalLock(hmem); - - // copy data - memcpy(clipbuf + 0, &buffer_width, sizeof(int)); - memcpy(clipbuf + sizeof(int), &buffer_height, sizeof(int)); - memcpy(clipbuf + 2 * sizeof(int), &buffer_size, sizeof(size_t)); - if (copyEntries.size() > 0 ) memcpy(clipbuf + 2 * sizeof(int) + sizeof(size_t), ©Entries[0], sizeof(CopyEntry) * copyEntries.size()); - - GlobalUnlock(hmem); - clipbuf = NULL; - - // update clipboard - EmptyClipboard(); - SetClipboardData(clipboardFormat, hmem); - CloseClipboard(); + + QByteArray data; + data.append((char *)&buffer_width, sizeof(int)); + data.append((char *)&buffer_height, sizeof(int)); + data.append((char *)&buffer_size, sizeof(size_t)); + data.append((char *)©Entries[0], sizeof(CopyEntry) * copyEntries.size()); + + QMimeData *mimeData = new QMimeData; + mimeData->setData("application/x-gnu-rocket", data); + QApplication::clipboard()->setMimeData(mimeData); } void TrackView::editCut() { if (0 == getTrackCount()) { - MessageBeep(~0U); + QApplication::beep(); return; } editCopy(); - editDelete(); + editClear(); } void TrackView::editPaste() @@ -470,134 +471,169 @@ void TrackView::editPaste() if (NULL == doc) return; if (0 == getTrackCount()) { - MessageBeep(~0U); + QApplication::beep(); return; } - - if (FAILED(OpenClipboard(getWin()))) - { - MessageBox(NULL, "Failed to open clipboard", NULL, MB_OK); - return; - } - - if (IsClipboardFormatAvailable(clipboardFormat)) - { - HGLOBAL hmem = GetClipboardData(clipboardFormat); - char *clipbuf = (char *)GlobalLock(hmem); + + const QMimeData *mimeData = QApplication::clipboard()->mimeData(); + if (mimeData->hasFormat("application/x-gnu-rocket")) { + const QByteArray mimeDataBuffer = mimeData->data("application/x-gnu-rocket"); + const char *clipbuf = mimeDataBuffer.data(); // copy data int buffer_width, buffer_height, buffer_size; memcpy(&buffer_width, clipbuf + 0, sizeof(int)); memcpy(&buffer_height, clipbuf + sizeof(int), sizeof(int)); - memcpy(&buffer_size, clipbuf + 2 * sizeof(int), sizeof(size_t)); - - SyncDocument::MultiCommand *multiCmd = new SyncDocument::MultiCommand(); + memcpy(&buffer_size, clipbuf + 2 * sizeof(int), sizeof(int)); + + doc->beginMacro("paste"); for (int i = 0; i < buffer_width; ++i) { - size_t trackPos = editTrack + i; + int trackPos = editTrack + i; if (trackPos >= getTrackCount()) continue; - size_t trackIndex = doc->getTrackIndexFromPos(trackPos); - const sync_track *t = doc->tracks[trackIndex]; + int trackIndex = doc->getTrackIndexFromPos(trackPos); + SyncTrack *t = doc->getTrack(trackIndex); for (int j = 0; j < buffer_height; ++j) { int row = editRow + j; - if (is_key_frame(t, row)) - multiCmd->addCommand(new SyncDocument::DeleteCommand(int(trackIndex), row)); + if (t->isKeyFrame(row)) + doc->deleteKeyFrame(t, row); } } - char *src = clipbuf + 2 * sizeof(int) + sizeof(size_t); + const char *src = clipbuf + 2 * sizeof(int) + sizeof(size_t); for (int i = 0; i < buffer_size; ++i) { struct CopyEntry ce; memcpy(&ce, src, sizeof(CopyEntry)); src += sizeof(CopyEntry); - - assert(ce.track >= 0); - assert(ce.track < buffer_width); - assert(ce.keyFrame.row >= 0); - assert(ce.keyFrame.row < buffer_height); - size_t trackPos = editTrack + ce.track; - if (trackPos < getTrackCount()) - { - size_t trackIndex = doc->getTrackIndexFromPos(trackPos); - track_key key = ce.keyFrame; + Q_ASSERT(ce.track >= 0); + Q_ASSERT(ce.track < buffer_width); + Q_ASSERT(ce.keyFrame.row >= 0); + Q_ASSERT(ce.keyFrame.row < buffer_height); + + int trackPos = editTrack + ce.track; + if (trackPos < getTrackCount()) { + int track = doc->getTrackIndexFromPos(trackPos); + SyncTrack::TrackKey key = ce.keyFrame; key.row += editRow; // since we deleted all keyframes in the edit-box already, we can just insert this one. - SyncDocument::Command *cmd = new SyncDocument::InsertCommand(int(trackIndex), key); - multiCmd->addCommand(cmd); + doc->setKeyFrame(doc->getTrack(track), key); } } - doc->exec(multiCmd); - - GlobalUnlock(hmem); + doc->endMacro(); + + invalidateRange(editTrack, editTrack + buffer_width - 1, editRow, editRow + buffer_height - 1); + dirtyCurrentValue(); + clipbuf = NULL; - } - else - MessageBeep(~0U); - - CloseClipboard(); + } else + QApplication::beep(); } +void TrackView::editUndo() +{ + SyncDocument *doc = getDocument(); + if (!doc) + return; + + if (!doc->canUndo()) + QApplication::beep(); + else + doc->undo(); + + // unfortunately, we don't know how much to invalidate... so we'll just invalidate it all. + invalidateAll(); +} + +void TrackView::editRedo() +{ + SyncDocument *doc = getDocument(); + if (!doc) + return; + + if (!doc->canRedo()) + QApplication::beep(); + else + doc->redo(); + + // unfortunately, we don't know how much to invalidate... so we'll just invalidate it all. + invalidateAll(); +} + +void TrackView::selectAll() +{ + selectStartTrack = int(this->getTrackCount()) - 1; + selectStopTrack = editTrack = 0; + selectStartRow = int(this->getRows()) - 1; + selectStopRow = editRow = 0; + invalidateAll(); +} + +void TrackView::selectTrack() +{ + selectStartTrack = selectStopTrack = editTrack; + selectStartRow = int(this->getRows()) - 1; + selectStopRow = editRow = 0; + invalidateAll(); +} + +void TrackView::selectRow() +{ + selectStartTrack = int(this->getTrackCount()) - 1; + selectStopTrack = editTrack = 0; + selectStartRow = selectStopRow = editRow; + invalidateAll(); +} + + void TrackView::setupScrollBars() { - SCROLLINFO si = { sizeof(si) }; - si.fMask = SIF_POS | SIF_PAGE | SIF_RANGE | SIF_DISABLENOSCROLL; - si.nPos = editRow; - si.nPage = windowRows; - si.nMin = 0; - si.nMax = int(getRows()) - 1 + windowRows - 1; - SetScrollInfo(hwnd, SB_VERT, &si, TRUE); - - si.fMask = SIF_POS | SIF_PAGE | SIF_RANGE | SIF_DISABLENOSCROLL; - si.nPos = editTrack; - si.nPage = windowTracks; - si.nMin = 0; - si.nMax = int(getTrackCount()) - 1 + windowTracks - 1; - SetScrollInfo(hwnd, SB_HORZ, &si, TRUE); + verticalScrollBar()->setValue(editRow); + verticalScrollBar()->setMinimum(0); + verticalScrollBar()->setMaximum(int(getRows()) - 1); + verticalScrollBar()->setPageStep(windowRows); + + int contentWidth = getTrackCount() * trackWidth; + int viewWidth = qMax(viewport()->width() - leftMarginWidth, 0); + horizontalScrollBar()->setValue(editTrack * trackWidth); + horizontalScrollBar()->setRange(0, contentWidth - viewWidth); + horizontalScrollBar()->setSingleStep(20); + horizontalScrollBar()->setPageStep(viewWidth); } void TrackView::scrollWindow(int scrollX, int scrollY) { - RECT clip; - GetClientRect(hwnd, &clip); - - if (scrollX == 0) clip.top = topMarginHeight; // don't scroll the top margin - if (scrollY == 0) clip.left = leftMarginWidth; // don't scroll the left margin - - ScrollWindowEx( - hwnd, - scrollX, - scrollY, - NULL, - &clip, - NULL, - NULL, - SW_INVALIDATE - ); + QRect clip = viewport()->geometry(); + + if (scrollX == 0) clip.setTop(topMarginHeight); // don't scroll the top margin + if (scrollY == 0) clip.setLeft(leftMarginWidth); // don't scroll the left margin + + viewport()->scroll(scrollX, scrollY, clip); } void TrackView::setScrollPos(int newScrollPosX, int newScrollPosY) { // clamp newscrollPosX - newScrollPosX = max(newScrollPosX, 0); + newScrollPosX = qMax(newScrollPosX, 0); - if (newScrollPosX != scrollPosX || newScrollPosY != scrollPosY) - { - int scrollX = scrollPosX - newScrollPosX; - int scrollY = scrollPosY - newScrollPosY; - + if (newScrollPosX != scrollPosX || newScrollPosY != scrollPosY) { + int deltaX = scrollPosX - newScrollPosX; + int deltaY = scrollPosY - newScrollPosY; + // update scrollPos scrollPosX = newScrollPosX; scrollPosY = newScrollPosY; - - scrollWindow(scrollX, scrollY); + + scrollWindow(deltaX, deltaY); } - setupScrollBars(); + + horizontalScrollBar()->setValue(newScrollPosX); + verticalScrollBar()->setValue(editRow); } -void TrackView::setEditRow(int newEditRow) +void TrackView::setEditRow(int newEditRow, bool selecting) { SyncDocument *doc = getDocument(); if (NULL == doc) return; @@ -606,34 +642,28 @@ void TrackView::setEditRow(int newEditRow) editRow = newEditRow; // clamp to document - editRow = min(max(editRow, 0), int(getRows()) - 1); + editRow = qBound(0, editRow, int(getRows()) - 1); - if (oldEditRow != editRow) - { - if (GetKeyState(VK_SHIFT) < 0) - { + if (oldEditRow != editRow) { + if (selecting) { selectStopRow = editRow; invalidateRange(selectStartTrack, selectStopTrack, oldEditRow, editRow); - } - else - { + } else { invalidateRange(selectStartTrack, selectStopTrack, selectStartRow, selectStopRow); selectStartRow = selectStopRow = editRow; selectStartTrack = selectStopTrack = editTrack; - } if (doc->clientSocket.clientPaused) { - doc->clientSocket.sendSetRowCommand(editRow); } - SendMessage(GetParent(getWin()), WM_ROWCHANGED, 0, editRow); - SendMessage(GetParent(getWin()), WM_CURRVALDIRTY, 0, 0); + dirtyPosition(); + dirtyCurrentValue(); } invalidateRow(oldEditRow); invalidateRow(editRow); - setScrollPos(scrollPosX, (editRow * rowHeight) - ((windowHeight - topMarginHeight) / 2) + rowHeight / 2); + setScrollPos(scrollPosX, (editRow * rowHeight) - ((viewport()->height() - topMarginHeight) / 2) + rowHeight / 2); } -void TrackView::setEditTrack(int newEditTrack, bool autoscroll) +void TrackView::setEditTrack(int newEditTrack, bool autoscroll, bool selecting) { if (0 == getTrackCount()) return; @@ -641,12 +671,11 @@ void TrackView::setEditTrack(int newEditTrack, bool autoscroll) editTrack = newEditTrack; // clamp to document - editTrack = max(editTrack, 0); - editTrack = min(editTrack, int(getTrackCount()) - 1); + editTrack = qBound(0, editTrack, int(getTrackCount()) - 1); if (oldEditTrack != editTrack) { - if (GetKeyState(VK_SHIFT) < 0) + if (selecting) { selectStopTrack = editTrack; invalidateRange(oldEditTrack, editTrack, selectStartRow, selectStopRow); @@ -657,142 +686,93 @@ void TrackView::setEditTrack(int newEditTrack, bool autoscroll) selectStartRow = selectStopRow = editRow; selectStartTrack = selectStopTrack = editTrack; } - SendMessage(GetParent(getWin()), WM_TRACKCHANGED, 0, editTrack); - SendMessage(GetParent(getWin()), WM_CURRVALDIRTY, 0, 0); + dirtyPosition(); + dirtyCurrentValue(); + invalidateTrack(oldEditTrack); + invalidateTrack(editTrack); } - - invalidateTrack(oldEditTrack); - invalidateTrack(editTrack); - if (autoscroll) { - int firstTrack = scrollPosX / trackWidth; - int lastTrack = firstTrack + windowTracks; + if (autoscroll && viewport()->width() > 0) { + int viewportWidth = viewport()->width() - leftMarginWidth; + int minX = getLogicalX(editTrack); + int maxX = getLogicalX(editTrack + 1); - int newFirstTrack = firstTrack; - if (editTrack >= lastTrack) - newFirstTrack = editTrack - lastTrack + firstTrack + 1; - if (editTrack < firstTrack) - newFirstTrack = editTrack; - setScrollPos(newFirstTrack * trackWidth, scrollPosY); + if (minX < scrollPosX) + setScrollPos(minX, scrollPosY); + else if (maxX > scrollPosX + viewportWidth) + setScrollPos(maxX - viewportWidth, scrollPosY); } else setupScrollBars(); } -static int getScrollPos(HWND hwnd, int bar) -{ - SCROLLINFO si = { sizeof(si), SIF_TRACKPOS }; - GetScrollInfo(hwnd, bar, &si); - return int(si.nTrackPos); -} - -void TrackView::setRows(size_t rows) +void TrackView::setRows(int rows) { SyncDocument *doc = getDocument(); - assert(doc); + Q_ASSERT(doc); doc->setRows(rows); - InvalidateRect(getWin(), NULL, FALSE); - setEditRow(min(editRow, int(rows) - 1)); + viewport()->update(); + setEditRow(qMin(editRow, int(rows) - 1)); } - -LRESULT TrackView::onVScroll(UINT sbCode, int /*newPos*/) +int TrackView::getRows() const { - switch (sbCode) - { - case SB_TOP: - setEditRow(0); - break; - - case SB_LINEUP: - setEditRow(editRow - 1); - break; - - case SB_LINEDOWN: - setEditRow(editRow + 1); - break; - - case SB_PAGEUP: - setEditRow(editRow - windowRows / 2); - break; - - case SB_PAGEDOWN: - setEditRow(editRow + windowRows / 2); - break; - - case SB_THUMBPOSITION: - case SB_THUMBTRACK: - setEditRow(getScrollPos(hwnd, SB_VERT)); - break; - } - - return FALSE; + const SyncDocument *doc = getDocument(); + if (!doc) + return 0; + return doc->getRows(); } -LRESULT TrackView::onHScroll(UINT sbCode, int /*newPos*/) +int TrackView::getTrackCount() const { - switch (sbCode) - { - case SB_LEFT: - setEditTrack(0); - break; - - case SB_RIGHT: - setEditTrack(int(getTrackCount()) - 1); - break; - - case SB_LINELEFT: - setEditTrack(editTrack - 1); - break; - - case SB_LINERIGHT: - setEditTrack(editTrack + 1); - break; - - case SB_PAGELEFT: - setEditTrack(editTrack - windowTracks); - break; - - case SB_PAGEDOWN: - setEditTrack(editTrack + windowTracks); - break; - - case SB_THUMBPOSITION: - case SB_THUMBTRACK: - setEditTrack(getScrollPos(hwnd, SB_HORZ)); - break; - } - - return FALSE; + const SyncDocument *doc = getDocument(); + if (!doc) + return 0; + return doc->getTrackCount(); +}; + +void TrackView::onVScroll(int value) +{ + setEditRow(value); +} + +void TrackView::onHScroll(int value) +{ + setScrollPos(value, scrollPosY); +} + +void TrackView::onEditingFinished() +{ + editEnterValue(); } void TrackView::editEnterValue() { SyncDocument *doc = getDocument(); - if (NULL == doc) return; - - if (int(editString.size()) > 0 && editTrack < int(getTrackCount())) - { - size_t trackIndex = doc->getTrackIndexFromPos(editTrack); - const sync_track *t = doc->tracks[trackIndex]; + if (!doc || !lineEdit->isVisible()) + return; - track_key newKey; - newKey.type = KEY_STEP; + if (lineEdit->text().length() > 0 && editTrack < int(getTrackCount())) { + int track = doc->getTrackIndexFromPos(editTrack); + SyncTrack *t = doc->getTrack(track); + + SyncTrack::TrackKey newKey; + newKey.type = SyncTrack::TrackKey::STEP; newKey.row = editRow; - int idx = sync_find_key(t, editRow); - if (idx >= 0) - newKey = t->keys[idx]; // copy old key - newKey.value = float(atof(editString.c_str())); // modify value - editString.clear(); + if (t->isKeyFrame(editRow)) + newKey = t->getKeyFrame(editRow); // copy old key + QString text = lineEdit->text(); + text.remove(lineEdit->validator()->locale().groupSeparator()); // workaround QTBUG-40456 + newKey.value = lineEdit->validator()->locale().toFloat(text); // modify value - SyncDocument::Command *cmd = doc->getSetKeyFrameCommand(int(trackIndex), newKey); - doc->exec(cmd); + doc->setKeyFrame(t, newKey); - SendMessage(GetParent(getWin()), WM_CURRVALDIRTY, 0, 0); - InvalidateRect(getWin(), NULL, FALSE); - } - else - MessageBeep(~0U); + dirtyCurrentValue(); + invalidateTrack(editTrack); + } else + QApplication::beep(); + + lineEdit->hide(); } void TrackView::editToggleInterpolationType() @@ -801,512 +781,271 @@ void TrackView::editToggleInterpolationType() if (NULL == doc) return; if (editTrack < int(getTrackCount())) { - size_t trackIndex = doc->getTrackIndexFromPos(editTrack); - const sync_track *t = doc->tracks[trackIndex]; + int track = doc->getTrackIndexFromPos(editTrack); + SyncTrack *t = doc->getTrack(track); + QMap keyMap = t->getKeyMap(); - int idx = key_idx_floor(t, editRow); - if (idx < 0) { - MessageBeep(~0U); + QMap::const_iterator it = keyMap.lowerBound(editRow); + if (it != keyMap.constBegin() && it.key() != editRow) + --it; + + if (it.key() > editRow || it == keyMap.constEnd()) { + QApplication::beep(); return; } // copy and modify - track_key newKey = t->keys[idx]; - newKey.type = (enum key_type) - ((newKey.type + 1) % KEY_TYPE_COUNT); + SyncTrack::TrackKey newKey = *it; + newKey.type = (SyncTrack::TrackKey::KeyType) + ((newKey.type + 1) % SyncTrack::TrackKey::KEY_TYPE_COUNT); // apply change to data-set - SyncDocument::Command *cmd = doc->getSetKeyFrameCommand(int(trackIndex), newKey); - doc->exec(cmd); + doc->setKeyFrame(t, newKey); // update user interface - SendMessage(GetParent(getWin()), WM_CURRVALDIRTY, 0, 0); - InvalidateRect(getWin(), NULL, FALSE); + dirtyCurrentValue(); + invalidateTrack(editTrack); } else - MessageBeep(~0U); + QApplication::beep(); } -void TrackView::editDelete() +void TrackView::editClear() { SyncDocument *doc = getDocument(); if (NULL == doc) return; - - int selectLeft = min(selectStartTrack, selectStopTrack); - int selectRight = max(selectStartTrack, selectStopTrack); - int selectTop = min(selectStartRow, selectStopRow); - int selectBottom = max(selectStartRow, selectStopRow); - - if (0 == getTrackCount()) return; - assert(selectRight < int(getTrackCount())); - - SyncDocument::MultiCommand *multiCmd = new SyncDocument::MultiCommand(); - for (int track = selectLeft; track <= selectRight; ++track) { - size_t trackIndex = doc->getTrackIndexFromPos(track); - const sync_track *t = doc->tracks[trackIndex]; - for (int row = selectTop; row <= selectBottom; ++row) { - if (is_key_frame(t, row)) { - SyncDocument::Command *cmd = new SyncDocument::DeleteCommand(int(trackIndex), row); - multiCmd->addCommand(cmd); - } + QRect selection = getSelection(); + + if (0 == getTrackCount()) return; + Q_ASSERT(selection.right() < int(getTrackCount())); + + doc->beginMacro("clear"); + for (int track = selection.left(); track <= selection.right(); ++track) { + int trackIndex = doc->getTrackIndexFromPos(track); + SyncTrack *t = doc->getTrack(trackIndex); + + for (int row = selection.top(); row <= selection.bottom(); ++row) { + if (t->isKeyFrame(row)) + doc->deleteKeyFrame(t, row); } } - - if (0 == multiCmd->getSize()) { - MessageBeep(~0U); - delete multiCmd; - } - else - { - doc->exec(multiCmd); - - SendMessage(GetParent(getWin()), WM_CURRVALDIRTY, 0, 0); - InvalidateRect(getWin(), NULL, FALSE); - } + + doc->endMacro(); + dirtyCurrentValue(); + invalidateRange(selection.left(), selection.right(), selection.top(), selection.bottom()); } void TrackView::editBiasValue(float amount) { SyncDocument *doc = getDocument(); if (NULL == doc) return; - - int selectLeft = min(selectStartTrack, selectStopTrack); - int selectRight = max(selectStartTrack, selectStopTrack); - int selectTop = min(selectStartRow, selectStopRow); - int selectBottom = max(selectStartRow, selectStopRow); - + if (0 == getTrackCount()) { - MessageBeep(~0U); + QApplication::beep(); return; } - - SyncDocument::MultiCommand *multiCmd = new SyncDocument::MultiCommand(); - for (int track = selectLeft; track <= selectRight; ++track) { - assert(track < int(getTrackCount())); - size_t trackIndex = doc->getTrackIndexFromPos(track); - const sync_track *t = doc->tracks[trackIndex]; - for (int row = selectTop; row <= selectBottom; ++row) { - int idx = sync_find_key(t, row); - if (idx >= 0) { - struct track_key k = t->keys[idx]; // copy old key + QRect selection = getSelection(); + + doc->beginMacro("bias"); + for (int track = selection.left(); track <= selection.right(); ++track) { + Q_ASSERT(track < int(getTrackCount())); + int trackIndex = doc->getTrackIndexFromPos(track); + SyncTrack *t = doc->getTrack(trackIndex); + + for (int row = selection.top(); row <= selection.bottom(); ++row) { + if (t->isKeyFrame(row)) { + SyncTrack::TrackKey k = t->getKeyFrame(row); // copy old key k.value += amount; // modify value // add sub-command - SyncDocument::Command *cmd = doc->getSetKeyFrameCommand(int(trackIndex), k); - multiCmd->addCommand(cmd); + doc->setKeyFrame(t, k); } } } - - if (0 == multiCmd->getSize()) { - MessageBeep(~0U); - delete multiCmd; - } - else - { - doc->exec(multiCmd); - - SendMessage(GetParent(getWin()), WM_CURRVALDIRTY, 0, 0); - invalidateRange(selectLeft, selectRight, selectTop, selectBottom); - } + doc->endMacro(); + + dirtyCurrentValue(); + invalidateRange(selection.left(), selection.right(), selection.top(), selection.bottom()); } -LRESULT TrackView::onKeyDown(UINT keyCode, UINT /*flags*/) +void TrackView::keyPressEvent(QKeyEvent *event) { SyncDocument *doc = getDocument(); - if (NULL == doc) return FALSE; + if (NULL == doc) return; - if (!editString.empty()) - { - switch(keyCode) - { - case VK_UP: - case VK_DOWN: - case VK_LEFT: - case VK_RIGHT: - case VK_PRIOR: - case VK_NEXT: - case VK_HOME: - case VK_END: + if (paused && lineEdit->isVisible()) { + switch (event->key()) { + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + case Qt::Key_Home: + case Qt::Key_End: + case Qt::Key_Space: editEnterValue(); } } - - if (editString.empty()) - { - switch (keyCode) - { - case VK_LEFT: - if (GetKeyState(VK_CONTROL) < 0) { + + bool shiftDown = (event->modifiers() & Qt::ShiftModifier) != 0; + bool ctrlDown = (event->modifiers() & Qt::ControlModifier) != 0; + bool selecting = shiftDown; + + if (lineEdit->isHidden()) { + switch (event->key()) { + case Qt::Key_Backtab: + ctrlDown = false; + selecting = false; + // FALLTHROUGH + case Qt::Key_Left: + if (ctrlDown) { if (0 < editTrack) doc->swapTrackOrder(editTrack, editTrack - 1); else - MessageBeep(~0U); + QApplication::beep(); } if (0 != getTrackCount()) - setEditTrack(editTrack - 1); + setEditTrack(editTrack - 1, true, selecting); else - MessageBeep(~0U); - break; - - case VK_RIGHT: - if (GetKeyState(VK_CONTROL) < 0) { + QApplication::beep(); + return; + + case Qt::Key_Tab: + ctrlDown = false; + selecting = false; + // FALLTHROUGH + case Qt::Key_Right: + if (ctrlDown) { if (int(getTrackCount()) > editTrack + 1) doc->swapTrackOrder(editTrack, editTrack + 1); else - MessageBeep(~0U); + QApplication::beep(); } if (0 != getTrackCount()) - setEditTrack(editTrack + 1); + setEditTrack(editTrack + 1, true, selecting); else - MessageBeep(~0U); - break; + QApplication::beep(); + return; } } - if (editString.empty() && doc->clientSocket.clientPaused) { - switch (keyCode) { - case VK_UP: - if (GetKeyState(VK_CONTROL) < 0) - { - float bias = 1.0f; - if (GetKeyState(VK_SHIFT) < 0) bias = 0.1f; - if (int(getTrackCount()) > editTrack) editBiasValue(bias); + if (lineEdit->isHidden() && paused) { + switch (event->key()) { + case Qt::Key_Up: + if (ctrlDown) { + if (int(getTrackCount()) > editTrack) + editBiasValue(shiftDown ? 0.1f : 1.0f); else - MessageBeep(~0U); - } - else setEditRow(editRow - 1); - break; - - case VK_DOWN: - if (GetKeyState(VK_CONTROL) < 0) - { - float bias = 1.0f; - if (GetKeyState(VK_SHIFT) < 0) bias = 0.1f; - if (int(getTrackCount()) > editTrack) editBiasValue(-bias); + QApplication::beep(); + } else + setEditRow(editRow - 1, selecting); + return; + + case Qt::Key_Down: + if (ctrlDown) { + if (int(getTrackCount()) > editTrack) + editBiasValue(shiftDown ? -0.1f : -1.0f); else - MessageBeep(~0U); - } - else setEditRow(editRow + 1); - break; - - case VK_PRIOR: - if (GetKeyState(VK_CONTROL) < 0) { - float bias = 10.0f; - if (GetKeyState(VK_SHIFT) < 0) - bias = 100.0f; - editBiasValue(bias); + QApplication::beep(); } else - setEditRow(editRow - 0x10); - break; - - case VK_NEXT: - if (GetKeyState(VK_CONTROL) < 0) { - float bias = 10.0f; - if (GetKeyState(VK_SHIFT) < 0) - bias = 100.0f; - editBiasValue(-bias); - } else - setEditRow(editRow + 0x10); - break; - - case VK_HOME: - if (GetKeyState(VK_CONTROL) < 0) setEditTrack(0); - else setEditRow(0); - break; - - case VK_END: - if (GetKeyState(VK_CONTROL) < 0) setEditTrack(int(getTrackCount()) - 1); - else setEditRow(int(getRows()) - 1); - break; + setEditRow(editRow + 1, selecting); + return; + + case Qt::Key_PageUp: + if (ctrlDown) + editBiasValue(shiftDown ? 100.0f : 10.0f); + else + setEditRow(editRow - 0x10, selecting); + return; + + case Qt::Key_PageDown: + if (ctrlDown) + editBiasValue(shiftDown ? -100.0f : -10.0f); + else + setEditRow(editRow + 0x10, selecting); + return; + + case Qt::Key_Home: + if (ctrlDown) + setEditTrack(0); + else + setEditRow(0, selecting); + return; + + case Qt::Key_End: + if (ctrlDown) + setEditTrack(int(getTrackCount()) - 1); + else + setEditRow(int(getRows()) - 1, selecting); + return; } } - - switch (keyCode) - { - case VK_RETURN: editEnterValue(); break; - case VK_DELETE: editDelete(); break; - - case VK_BACK: - if (!editString.empty()) - { - editString.resize(editString.size() - 1); - invalidatePos(editTrack, editRow); - } - else - MessageBeep(~0U); - break; - - case VK_CANCEL: - case VK_ESCAPE: - if (!editString.empty()) { + + switch (event->key()) { + case Qt::Key_Delete: editClear(); return; + + case Qt::Key_Cancel: + case Qt::Key_Escape: + if (paused && lineEdit->isVisible()) { // return to old value (i.e don't clear) - editString.clear(); - invalidatePos(editTrack, editRow); - MessageBeep(~0U); + lineEdit->hide(); + QApplication::beep(); } - break; - case VK_SPACE: - if (!editString.empty()) { - editString.clear(); - invalidatePos(editTrack, editRow); - MessageBeep(~0U); - } - doc->clientSocket.sendPauseCommand( !doc->clientSocket.clientPaused ); - break; - } - return FALSE; -} + return; -LRESULT TrackView::onChar(UINT keyCode, UINT /*flags*/) -{ - switch (char(keyCode)) - { - case '-': - if (editString.empty()) - { - editString.push_back(char(keyCode)); - invalidatePos(editTrack, editRow); + case Qt::Key_Space: + if (connected) { + paused = !paused; + emit pauseChanged(paused); } - break; - case '.': - // only one '.' allowed - if (std::string::npos != editString.find('.')) { - MessageBeep(~0U); - break; - } - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - if (editTrack < int(getTrackCount())) - { - editString.push_back(char(keyCode)); - invalidatePos(editTrack, editRow); - } - else - MessageBeep(~0U); - break; - - case 'i': + return; + + case Qt::Key_I: editToggleInterpolationType(); - break; + return; - case 'k': + case Qt::Key_K: getDocument()->toggleRowBookmark(getEditRow()); invalidateRow(getEditRow()); - break; + return; + } + + if (paused && lineEdit->isHidden() && event->text().length() && doc->getTrackCount()) { + // no line-edit, check if input matches a double + QString str = event->text(); + int pos = 0; + if (lineEdit->validator()->validate(str, pos) != QValidator::Invalid) { + lineEdit->move(getPhysicalX(getEditTrack()), getPhysicalY(getEditRow())); + lineEdit->resize(trackWidth, rowHeight); + lineEdit->setText(""); + lineEdit->show(); + lineEdit->event(event); + lineEdit->setFocus(); + } } - return FALSE; } -LRESULT TrackView::onSize(int width, int height) +void TrackView::resizeEvent(QResizeEvent *event) { - windowWidth = width; - windowHeight = height; - - windowRows = (height - topMarginHeight) / rowHeight; - windowTracks = (width - leftMarginWidth) / trackWidth; - + windowRows = (event->size().height() - topMarginHeight) / rowHeight; setEditRow(editRow); setupScrollBars(); - return FALSE; } -LRESULT TrackView::onSetCursor(HWND /*win*/, UINT hitTest, UINT message) +void TrackView::changeEvent(QEvent *event) { - POINT cpos; - GetCursorPos(&cpos); - ScreenToClient(hwnd, &cpos); - int track = getTrackFromX(cpos.x); - if (cpos.y < topMarginHeight && - track >= 0 && track < int(getTrackCount())) { - SetCursor(handCursor); - return TRUE; - } - return DefWindowProc(this->hwnd, WM_SETCURSOR, (WPARAM)hwnd, - MAKELPARAM(hitTest, message)); -} - -LRESULT TrackView::onLButtonDown(UINT /*flags*/, POINTS pos) -{ - int track = getTrackFromX(pos.x); - if (pos.y < topMarginHeight && - track >= 0 && track < int(getTrackCount())) { - setEditTrack(track, false); - SetCapture(hwnd); - anchorTrack = track; - } - return FALSE; -} - -LRESULT TrackView::onLButtonUp(UINT /*flags*/, POINTS /*pos*/) -{ - ReleaseCapture(); - setEditTrack(editTrack); - return FALSE; -} - -LRESULT TrackView::onMouseMove(UINT /*flags*/, POINTS pos) -{ - if (GetCapture() == hwnd) { - SyncDocument *doc = getDocument(); - const int posTrack = getTrackFromX(pos.x), - trackCount = getTrackCount(); - - if (!doc || posTrack < 0 || posTrack >= trackCount) - return FALSE; - - if (posTrack > anchorTrack) { - for (int i = anchorTrack; i < posTrack; ++i) - doc->swapTrackOrder(i, i + 1); - anchorTrack = posTrack; - setEditTrack(posTrack); - InvalidateRect(hwnd, NULL, FALSE); - } else if (posTrack < anchorTrack) { - for (int i = anchorTrack; i > posTrack; --i) - doc->swapTrackOrder(i, i - 1); - anchorTrack = posTrack; - setEditTrack(posTrack); - InvalidateRect(hwnd, NULL, FALSE); - } - } - return FALSE; -} - -LRESULT TrackView::windowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - assert(hwnd == this->hwnd); - - switch(msg) - { - case WM_CREATE: return onCreate(); - case WM_SIZE: return onSize(LOWORD(lParam), HIWORD(lParam)); - case WM_VSCROLL: return onVScroll(LOWORD(wParam), getScrollPos(hwnd, SB_VERT)); - case WM_HSCROLL: return onHScroll(LOWORD(wParam), getScrollPos(hwnd, SB_HORZ)); - case WM_PAINT: return onPaint(); - case WM_KEYDOWN: return onKeyDown((UINT)wParam, (UINT)lParam); - case WM_CHAR: return onChar((UINT)wParam, (UINT)lParam); - case WM_LBUTTONDOWN: - return onLButtonDown((UINT)wParam, MAKEPOINTS(lParam)); - case WM_LBUTTONUP: - return onLButtonUp((UINT)wParam, MAKEPOINTS(lParam)); - case WM_MOUSEMOVE: - return onMouseMove((UINT)wParam, MAKEPOINTS(lParam)); - case WM_SETCURSOR: - return onSetCursor((HWND)wParam, LOWORD(lParam), - HIWORD(lParam)); - - case WM_COPY: editCopy(); break; - case WM_CUT: editCut(); break; - case WM_PASTE: - editPaste(); - SendMessage(GetParent(getWin()), WM_CURRVALDIRTY, 0, 0); - InvalidateRect(hwnd, NULL, FALSE); + switch (event->type()) { + case QEvent::FontChange: + updateFont(); break; - - case WM_UNDO: - if (NULL == getDocument()) return FALSE; - if (!getDocument()->undo()) - MessageBeep(~0U); - // unfortunately, we don't know how much to invalidate... so we'll just invalidate it all. - InvalidateRect(hwnd, NULL, FALSE); - break; - - case WM_REDO: - if (NULL == getDocument()) return FALSE; - if (!getDocument()->redo()) - MessageBeep(~0U); - // unfortunately, we don't know how much to invalidate... so we'll just invalidate it all. - InvalidateRect(hwnd, NULL, FALSE); - break; - - default: - return DefWindowProc(hwnd, msg, wParam, lParam); - } - return FALSE; -} -LRESULT CALLBACK trackViewWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) -{ - // Get the TrackView instance (if any) -#pragma warning(suppress:4312) /* remove a pointless warning */ - TrackView *trackView = (TrackView*)GetWindowLongPtr(hwnd, 0); - - switch(msg) - { - case WM_NCCREATE: - // Get TrackView from createstruct - trackView = (TrackView*)((CREATESTRUCT*)lParam)->lpCreateParams; - trackView->hwnd = hwnd; - - // Set the TrackView instance -#pragma warning(suppress:4244) /* remove a pointless warning */ - SetWindowLongPtr(hwnd, 0, (LONG_PTR)trackView); - - // call the proper window procedure - return trackView->windowProc(hwnd, msg, wParam, lParam); + case QEvent::PaletteChange: + updatePalette(); break; - - case WM_NCDESTROY: - assert(NULL != trackView); - { - // call the window proc and store away the return code - LRESULT res = trackView->windowProc(hwnd, msg, wParam, lParam); - - // get rid of the TrackView instance - trackView = NULL; - SetWindowLongPtr(hwnd, 0, (LONG_PTR)NULL); - - // return the stored return code - return res; - } - break; - - default: - assert(NULL != trackView); - return trackView->windowProc(hwnd, msg, wParam, lParam); + + default: ; } } - -ATOM registerTrackViewWindowClass(HINSTANCE hInstance) -{ - WNDCLASSEX wc; - - wc.cbSize = sizeof(WNDCLASSEX); - wc.style = 0; - wc.lpfnWndProc = trackViewWindowProc; - wc.cbClsExtra = 0; - wc.cbWndExtra = sizeof(TrackView*); - wc.hInstance = hInstance; - wc.hIcon = 0; - wc.hCursor = LoadCursor(NULL, IDC_IBEAM); - wc.hbrBackground = (HBRUSH)0; - wc.lpszMenuName = NULL; - wc.lpszClassName = trackViewWindowClassName; - wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); - - return RegisterClassEx(&wc); -} - -HWND TrackView::create(HINSTANCE hInstance, HWND hwndParent) -{ - HWND hwnd = CreateWindowEx( - WS_EX_CLIENTEDGE, - trackViewWindowClassName, "", - WS_VSCROLL | WS_HSCROLL | WS_CHILD | WS_VISIBLE, - CW_USEDEFAULT, CW_USEDEFAULT, // x, y - CW_USEDEFAULT, CW_USEDEFAULT, // width, height - hwndParent, NULL, hInstance, (void*)this - ); - return hwnd; -} - diff --git a/editor/trackview.h b/editor/trackview.h index 79eca11..35c1d53 100644 --- a/editor/trackview.h +++ b/editor/trackview.h @@ -1,26 +1,20 @@ -/* Copyright (C) 2007-2008 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ +#ifndef TRACKVIEW_H +#define TRACKVIEW_H -#pragma once +#include +#include +#include +#include -#include "syncdocument.h" -#include +class QLineEdit; +class SyncDocument; -// custom messages -#define WM_REDO (WM_USER + 0x40 + 3) -#define WM_ROWCHANGED (WM_USER + 0x40 + 4) -#define WM_TRACKCHANGED (WM_USER + 0x40 + 5) -#define WM_CURRVALDIRTY (WM_USER + 0x40 + 6) - -class TrackView +class TrackView : public QAbstractScrollArea { + Q_OBJECT public: - TrackView(); + TrackView(QWidget *parent); ~TrackView(); - - HWND create(HINSTANCE hInstance, HWND hwndParent); - HWND getWin() const { return hwnd; } void setDocument(SyncDocument *document) { @@ -30,184 +24,157 @@ public: const SyncDocument *getDocument() const { return document; } SyncDocument *getDocument() { return document; } - - void setRows(size_t rows); - size_t getRows() const - { - const SyncDocument *doc = getDocument(); - if (!doc) - return 0; - return doc->getRows(); - } - - void setFont(HFONT font); - + + void setRows(int rows); + int getRows() const; + void editEnterValue(); - void editDelete(); - void editCopy(); - void editCut(); - void editPaste(); void editBiasValue(float amount); void editToggleInterpolationType(); - void setEditRow(int newEditRow); + void setEditRow(int newEditRow, bool selecting = false); int getEditRow() const { return editRow; } - void setEditTrack(int newEditTrack, bool autoscroll = true); + void setEditTrack(int newEditTrack, bool autoscroll = true, bool selecting = false); int getEditTrack() const { return editTrack; } - - void selectAll() - { - selectStartTrack = int(this->getTrackCount()) - 1; - selectStopTrack = editTrack = 0; - selectStartRow = int(this->getRows()) - 1; - selectStopRow = editRow = 0; - - InvalidateRect(hwnd, NULL, FALSE); - } - - void selectTrack(int track) - { - selectStartTrack = selectStopTrack = editTrack = track; - selectStartRow = int(this->getRows()) - 1; - selectStopRow = editRow = 0; - - InvalidateRect(hwnd, NULL, FALSE); - } - - void selectRow(int row) - { - selectStartTrack = int(this->getTrackCount()) - 1; - selectStopTrack = editTrack = 0; - selectStartRow = selectStopRow = editRow = row; - - InvalidateRect(hwnd, NULL, FALSE); - } - + void selectNone() { selectStartTrack = selectStopTrack = editTrack; selectStartRow = selectStopRow = editRow; - InvalidateRect(hwnd, NULL, FALSE); + update(); } - -private: - // some nasty hackery to forward the window messages - friend LRESULT CALLBACK trackViewWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); - LRESULT windowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); - - // events - LRESULT onCreate(); - LRESULT onPaint(); - LRESULT onVScroll(UINT sbCode, int newPos); - LRESULT onHScroll(UINT sbCode, int newPos); - LRESULT onSize(int width, int height); - LRESULT onKeyDown(UINT keyCode, UINT flags); - LRESULT onChar(UINT keyCode, UINT flags); - LRESULT onSetCursor(HWND win, UINT hitTest, UINT message); - LRESULT onLButtonDown(UINT flags, POINTS pos); - LRESULT onLButtonUp(UINT flags, POINTS pos); - LRESULT onMouseMove(UINT flags, POINTS pos); - void paintTracks(HDC hdc, RECT rcTracks); - void paintTopMargin(HDC hdc, RECT rcTracks); - + void dirtyCurrentValue() + { + emit currValDirty(); + } + + void dirtyPosition() + { + emit posChanged(editTrack, editRow); + } + + bool paused, connected; + +signals: + void posChanged(int col, int row); + void pauseChanged(bool paused); + void currValDirty(); + +private slots: + void onHScroll(int value); + void onVScroll(int value); + void onEditingFinished(); + +public slots: + void editUndo(); + void editRedo(); + void editCopy(); + void editCut(); + void editPaste(); + void editClear(); + + void selectAll(); + void selectTrack(); + void selectRow(); + +private: + + /* paint helpers */ + void paintTopMargin(QPainter &painter, const QRect &rcTracks); + void paintLeftMargin(QPainter &painter, const QRect &rcTracks); + void paintTracks(QPainter &painter, const QRect &rcTracks); + void paintTrack(QPainter &painter, const QRect &rcTracks, int track); + + void paintEvent(QPaintEvent *); + void keyPressEvent(QKeyEvent *); + void resizeEvent(QResizeEvent *); + void mouseMoveEvent(QMouseEvent *); + void mousePressEvent(QMouseEvent *); + void mouseReleaseEvent(QMouseEvent *); + void changeEvent(QEvent *); + void setupScrollBars(); void setScrollPos(int newScrollPosX, int newScrollPosY); void scrollWindow(int newScrollPosX, int newScrollPosY); - + void invalidateRange(int startTrack, int stopTrack, int startRow, int stopRow) { - RECT rect; - rect.left = getScreenX(std::min(startTrack, stopTrack)); - rect.right = getScreenX(std::max(startTrack, stopTrack) + 1); - rect.top = getScreenY(std::min(startRow, stopRow)); - rect.bottom = getScreenY(std::max(startRow, stopRow) + 1); - InvalidateRect(hwnd, &rect, FALSE); + QRect rect(QPoint(getPhysicalX(qMin(startTrack, stopTrack)), + getPhysicalY(qMin(startRow, stopRow))), + QPoint(getPhysicalX(qMax(startTrack, stopTrack) + 1) - 1, + getPhysicalY(qMax(startRow, stopRow) + 1) - 1)); + viewport()->update(rect); } - + void invalidatePos(int track, int row) { - RECT rect; - rect.left = getScreenX(track); - rect.right = getScreenX(track + 1); - rect.top = getScreenY(row); - rect.bottom = getScreenY(row + 1); - InvalidateRect(hwnd, &rect, FALSE); + invalidateRange(track, track, row, row); } - + void invalidateRow(int row) { - RECT clientRect; - GetClientRect(hwnd, &clientRect); - - RECT rect; - rect.left = clientRect.left; - rect.right = clientRect.right; - rect.top = getScreenY(row); - rect.bottom = getScreenY(row + 1); - - InvalidateRect(hwnd, &rect, FALSE); + invalidateRange(0, getTrackCount(), row, row); } - + void invalidateTrack(int track) { - RECT clientRect; - GetClientRect(hwnd, &clientRect); - - RECT rect; - rect.left = getScreenX(track); - rect.right = getScreenX(track + 1); - rect.top = clientRect.top; - rect.bottom = clientRect.bottom; - - InvalidateRect(hwnd, &rect, FALSE); + invalidateRange(track, track, 0, getRows()); } - - int getScreenY(int row) const; - int getScreenX(size_t track) const; - int getTrackFromX(int x) const; - size_t getTrackCount() const + void invalidateAll() { - const SyncDocument *doc = getDocument(); - if (NULL == doc) return 0; - return int(doc->getTrackOrderCount()); - }; - + invalidateRange(0, getTrackCount(), 0, getRows()); + } + + QRect getSelection() const + { + return QRect(QPoint(qMin(selectStartTrack, selectStopTrack), + qMin(selectStartRow, selectStopRow)), + QPoint(qMax(selectStartTrack, selectStopTrack), + qMax(selectStartRow, selectStopRow))); + } + + int getLogicalX(int track) const; + int getLogicalY(int row) const; + int getPhysicalX(int track) const; + int getPhysicalY(int row) const; + + int getTrackFromLogicalX(int x) const; + int getTrackFromPhysicalX(int x) const; + + int getTrackCount() const; + int selectStartTrack, selectStopTrack; int selectStartRow, selectStopRow; - - HFONT font; + int rowHeight; - int fontWidth; int trackWidth; int topMarginHeight; int leftMarginWidth; + void updateFont(); + + QBrush bgBaseBrush, bgDarkBrush; + QBrush selectBaseBrush, selectDarkBrush; + QPen rowPen, rowSelectPen; + QBrush editBrush, bookmarkBrush; + QPen lerpPen, cosinePen, rampPen; + QCursor handCursor; + void updatePalette(); - - HBRUSH bgBaseBrush, bgDarkBrush; - HBRUSH selectBaseBrush, selectDarkBrush; - HPEN rowPen, rowSelectPen; - HBRUSH editBrush, bookmarkBrush; - HPEN lerpPen, cosinePen, rampPen; - HCURSOR handCursor; - /* cursor position */ int editRow, editTrack; int scrollPosX, scrollPosY; - int windowWidth, windowHeight; - int windowRows, windowTracks; + int windowRows; SyncDocument *document; - - std::string editString; - - HWND hwnd; - - UINT clipboardFormat; + + QLineEdit *lineEdit; + + bool dragging; int anchorTrack; }; -ATOM registerTrackViewWindowClass(HINSTANCE hInstance); +#endif // !defined(TRACKVIEW_H) diff --git a/example_bass/example_bass.cpp b/example_bass/example_bass.cpp index 8218bec..5a9cc30 100644 --- a/example_bass/example_bass.cpp +++ b/example_bass/example_bass.cpp @@ -1,21 +1,24 @@ -/* Copyright (C) 2007-2008 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - * sdl+opengl examle by rasmus/loonies http://visualizethis.tumblr.com 2011 - */ - #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #endif #include +#ifdef WIN32 #undef main /* avoid SDL's nasty SDLmain hack */ +#endif #include #include #include #include #include -#include "../sync/sync.h" +#if defined(__APPLE__) && defined(__MACH__) +#include +#define gluPerspective(f, a, zn, zf) glMultMatrixf(GLKMatrix4MakePerspective((f) * M_PI / 180, a, zn, zf).m) +#define gluLookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz) glMultMatrixf(GLKMatrix4MakeLookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz).m) +#endif + +#include "../lib/sync.h" static const float bpm = 150.0f; /* beats per minute */ static const int rpb = 8; /* rows per beat */ @@ -80,7 +83,7 @@ static void die(const char *fmt, ...) static const unsigned int width = 800; static const unsigned int height = 600; -SDL_Surface *setup_sdl() +void setup_sdl() { if (SDL_Init(SDL_INIT_VIDEO)) die("%s", SDL_GetError()); @@ -93,7 +96,8 @@ SDL_Surface *setup_sdl() SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1); - return SDL_SetVideoMode(width, height, 32, SDL_OPENGL); + if (!SDL_SetVideoMode(width, height, 32, SDL_OPENGL)) + die("%s", SDL_GetError()); } void draw_cube() @@ -147,13 +151,12 @@ void draw_cube() int main(int argc, char *argv[]) { - SDL_Surface *screen; HSTREAM stream; const struct sync_track *clear_r, *clear_g, *clear_b; const struct sync_track *cam_rot, *cam_dist; - screen = setup_sdl(); + setup_sdl(); /* init BASS */ if (!BASS_Init(-1, 44100, 0, 0, 0)) @@ -193,13 +196,13 @@ int main(int argc, char *argv[]) /* draw */ - glClearColor(sync_get_val(clear_r, row), - sync_get_val(clear_g, row), - sync_get_val(clear_b, row), 1.0f); + glClearColor(float(sync_get_val(clear_r, row)), + float(sync_get_val(clear_g, row)), + float(sync_get_val(clear_b, row)), 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - float rot = sync_get_val(cam_rot, row); - float dist = sync_get_val(cam_dist, row); + float rot = float(sync_get_val(cam_rot, row)); + float dist = float(sync_get_val(cam_dist, row)); glMatrixMode(GL_PROJECTION); glLoadIdentity(); diff --git a/example_bass/example_bass.vcproj b/example_bass/example_bass.vcproj deleted file mode 100644 index 1126e65..0000000 --- a/example_bass/example_bass.vcproj +++ /dev/null @@ -1,672 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example_bass/example_bass.vs2008.vcproj b/example_bass/example_bass.vs2008.vcproj new file mode 100644 index 0000000..1126e65 --- /dev/null +++ b/example_bass/example_bass.vs2008.vcproj @@ -0,0 +1,672 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example_bass/example_bass.vs2013.vcxproj b/example_bass/example_bass.vs2013.vcxproj new file mode 100644 index 0000000..9b4612c --- /dev/null +++ b/example_bass/example_bass.vs2013.vcxproj @@ -0,0 +1,361 @@ + + + + + Debug Client + Win32 + + + Debug Client + x64 + + + Debug + Win32 + + + Debug + x64 + + + Release Client + Win32 + + + Release Client + x64 + + + Release + Win32 + + + Release + x64 + + + + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3} + example_bass + Win32Proj + example_bass + + + + MultiByte + true + v120 + + + MultiByte + v120 + + + MultiByte + true + v120 + + + MultiByte + v120 + + + MultiByte + true + v120 + + + MultiByte + v120 + + + MultiByte + true + v120 + + + MultiByte + v120 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>12.0.30501.0 + db1c9e5e + + + $(SolutionDir)$(Configuration)\ + $(Configuration)\ + true + false + false + + + $(SolutionDir)$(Configuration)\ + $(Configuration)\ + false + + + $(SolutionDir)$(Configuration)\ + $(Configuration)\ + true + false + false + + + $(SolutionDir)$(Configuration)\ + $(Configuration)\ + false + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + true + false + false + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + false + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + false + false + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + false + + + + Disabled + include;%(AdditionalIncludeDirectories) + WIN32;_DEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;SYNC_PLAYER;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebug + + EditAndContinue + Level3 + + + opengl32.lib;glu32.lib;bass.lib;%(AdditionalDependencies) + lib;%(AdditionalLibraryDirectories) + true + Console + + MachineX86 + false + + + + + include;%(AdditionalIncludeDirectories) + WIN32;NDEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;SYNC_PLAYER;%(PreprocessorDefinitions) + MultiThreadedDLL + + ProgramDatabase + Level3 + + + opengl32.lib;glu32.lib;bass.lib;%(AdditionalDependencies) + lib;%(AdditionalLibraryDirectories) + true + Console + true + true + + MachineX86 + + + + + Disabled + include;%(AdditionalIncludeDirectories) + WIN32;_DEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebug + + EditAndContinue + Level3 + + + opengl32.lib;glu32.lib;bass.lib;%(AdditionalDependencies) + lib;%(AdditionalLibraryDirectories) + true + Console + + MachineX86 + false + + + + + include;%(AdditionalIncludeDirectories) + WIN32;NDEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + MultiThreadedDLL + + ProgramDatabase + Level3 + + + opengl32.lib;glu32.lib;bass.lib;%(AdditionalDependencies) + lib;%(AdditionalLibraryDirectories) + true + Console + true + true + + MachineX86 + + + + + X64 + + + Disabled + include;%(AdditionalIncludeDirectories) + WIN32;_DEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;SYNC_PLAYER;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebug + + ProgramDatabase + Level3 + + + opengl32.lib;glu32.lib;bass.lib;%(AdditionalDependencies) + lib64;%(AdditionalLibraryDirectories) + true + Console + + MachineX64 + + + + + X64 + + + include;%(AdditionalIncludeDirectories) + WIN32;NDEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;SYNC_PLAYER;%(PreprocessorDefinitions) + MultiThreadedDLL + + ProgramDatabase + Level3 + + + opengl32.lib;glu32.lib;bass.lib;%(AdditionalDependencies) + lib64;%(AdditionalLibraryDirectories) + true + Console + true + true + + MachineX64 + + + + + X64 + + + Disabled + include;%(AdditionalIncludeDirectories) + WIN32;_DEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebug + + ProgramDatabase + Level3 + + + opengl32.lib;glu32.lib;bass.lib;%(AdditionalDependencies) + lib64;%(AdditionalLibraryDirectories) + true + Console + + MachineX64 + + + + + X64 + + + include;%(AdditionalIncludeDirectories) + WIN32;NDEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + MultiThreadedDLL + + ProgramDatabase + Level3 + + + opengl32.lib;glu32.lib;bass.lib;%(AdditionalDependencies) + lib64;%(AdditionalLibraryDirectories) + true + Console + true + true + + MachineX64 + + + + + + + + {5866042c-7fcb-4db1-baad-44df6567511f} + false + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/example_bass/example_bass.vs2013.vcxproj.filters b/example_bass/example_bass.vs2013.vcxproj.filters new file mode 100644 index 0000000..d97473d --- /dev/null +++ b/example_bass/example_bass.vs2013.vcxproj.filters @@ -0,0 +1,25 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav + + + + + Source Files + + + + + + \ No newline at end of file diff --git a/example_bass/packages.config b/example_bass/packages.config new file mode 100644 index 0000000..03fa99d --- /dev/null +++ b/example_bass/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples.sln b/examples.sln deleted file mode 100644 index 40b5bef..0000000 --- a/examples.sln +++ /dev/null @@ -1,59 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 10.00 -# Visual Studio 2008 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "example_bass", "example_bass\example_bass.vcproj", "{96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}" - ProjectSection(ProjectDependencies) = postProject - {5866042C-7FCB-4DB1-BAAD-44DF6567511F} = {5866042C-7FCB-4DB1-BAAD-44DF6567511F} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sync_player", "sync_player.vcproj", "{5866042C-7FCB-4DB1-BAAD-44DF6567511F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug Client|Win32 = Debug Client|Win32 - Debug Client|x64 = Debug Client|x64 - Debug|Win32 = Debug|Win32 - Debug|x64 = Debug|x64 - Release Client|Win32 = Release Client|Win32 - Release Client|x64 = Release Client|x64 - Release|Win32 = Release|Win32 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|Win32.ActiveCfg = Debug Client|Win32 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|Win32.Build.0 = Debug Client|Win32 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|x64.ActiveCfg = Debug Client|x64 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|x64.Build.0 = Debug Client|x64 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|Win32.ActiveCfg = Debug|Win32 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|Win32.Build.0 = Debug|Win32 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|x64.ActiveCfg = Debug|x64 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|x64.Build.0 = Debug|x64 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|Win32.ActiveCfg = Release Client|Win32 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|Win32.Build.0 = Release Client|Win32 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|x64.ActiveCfg = Release Client|x64 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|x64.Build.0 = Release Client|x64 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|Win32.ActiveCfg = Release|Win32 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|Win32.Build.0 = Release|Win32 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|x64.ActiveCfg = Release|x64 - {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|x64.Build.0 = Release|x64 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|Win32.ActiveCfg = Debug Client|Win32 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|Win32.Build.0 = Debug Client|Win32 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|x64.ActiveCfg = Debug Client|x64 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|x64.Build.0 = Debug Client|x64 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|Win32.ActiveCfg = Debug|Win32 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|Win32.Build.0 = Debug|Win32 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|x64.ActiveCfg = Debug|x64 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|x64.Build.0 = Debug|x64 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|Win32.ActiveCfg = Release Client|Win32 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|Win32.Build.0 = Release Client|Win32 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|x64.ActiveCfg = Release Client|x64 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|x64.Build.0 = Release Client|x64 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|Win32.ActiveCfg = Release|Win32 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|Win32.Build.0 = Release|Win32 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|x64.ActiveCfg = Release|x64 - {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/examples.vs2008.sln b/examples.vs2008.sln new file mode 100644 index 0000000..33c72ed --- /dev/null +++ b/examples.vs2008.sln @@ -0,0 +1,59 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "example_bass", "example_bass\example_bass.vs2008.vcproj", "{96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}" + ProjectSection(ProjectDependencies) = postProject + {5866042C-7FCB-4DB1-BAAD-44DF6567511F} = {5866042C-7FCB-4DB1-BAAD-44DF6567511F} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "librocket", "lib/librocket.vs2008.vcproj", "{5866042C-7FCB-4DB1-BAAD-44DF6567511F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug Client|Win32 = Debug Client|Win32 + Debug Client|x64 = Debug Client|x64 + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release Client|Win32 = Release Client|Win32 + Release Client|x64 = Release Client|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|Win32.ActiveCfg = Debug Client|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|Win32.Build.0 = Debug Client|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|x64.ActiveCfg = Debug Client|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|x64.Build.0 = Debug Client|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|Win32.ActiveCfg = Debug|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|Win32.Build.0 = Debug|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|x64.ActiveCfg = Debug|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|x64.Build.0 = Debug|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|Win32.ActiveCfg = Release Client|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|Win32.Build.0 = Release Client|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|x64.ActiveCfg = Release Client|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|x64.Build.0 = Release Client|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|Win32.ActiveCfg = Release|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|Win32.Build.0 = Release|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|x64.ActiveCfg = Release|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|x64.Build.0 = Release|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|Win32.ActiveCfg = Debug Client|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|Win32.Build.0 = Debug Client|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|x64.ActiveCfg = Debug Client|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|x64.Build.0 = Debug Client|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|Win32.ActiveCfg = Debug|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|Win32.Build.0 = Debug|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|x64.ActiveCfg = Debug|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|x64.Build.0 = Debug|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|Win32.ActiveCfg = Release Client|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|Win32.Build.0 = Release Client|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|x64.ActiveCfg = Release Client|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|x64.Build.0 = Release Client|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|Win32.ActiveCfg = Release|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|Win32.Build.0 = Release|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|x64.ActiveCfg = Release|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/examples.vs2013.sln b/examples.vs2013.sln new file mode 100644 index 0000000..080e03a --- /dev/null +++ b/examples.vs2013.sln @@ -0,0 +1,58 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.40629.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "example_bass", "example_bass\example_bass.vs2013.vcxproj", "{96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "librocket", "lib\librocket.vs2013.vcxproj", "{5866042C-7FCB-4DB1-BAAD-44DF6567511F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug Client|Win32 = Debug Client|Win32 + Debug Client|x64 = Debug Client|x64 + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release Client|Win32 = Release Client|Win32 + Release Client|x64 = Release Client|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|Win32.ActiveCfg = Debug Client|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|Win32.Build.0 = Debug Client|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|x64.ActiveCfg = Debug Client|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug Client|x64.Build.0 = Debug Client|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|Win32.ActiveCfg = Debug|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|Win32.Build.0 = Debug|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|x64.ActiveCfg = Debug|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Debug|x64.Build.0 = Debug|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|Win32.ActiveCfg = Release Client|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|Win32.Build.0 = Release Client|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|x64.ActiveCfg = Release Client|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release Client|x64.Build.0 = Release Client|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|Win32.ActiveCfg = Release|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|Win32.Build.0 = Release|Win32 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|x64.ActiveCfg = Release|x64 + {96D91AAD-2F45-4CC6-A923-96B80E1C3CE3}.Release|x64.Build.0 = Release|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|Win32.ActiveCfg = Debug Client|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|Win32.Build.0 = Debug Client|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|x64.ActiveCfg = Debug Client|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug Client|x64.Build.0 = Debug Client|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|Win32.ActiveCfg = Debug|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|Win32.Build.0 = Debug|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|x64.ActiveCfg = Debug|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Debug|x64.Build.0 = Debug|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|Win32.ActiveCfg = Release Client|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|Win32.Build.0 = Release Client|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|x64.ActiveCfg = Release Client|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release Client|x64.Build.0 = Release Client|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|Win32.ActiveCfg = Release|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|Win32.Build.0 = Release|Win32 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|x64.ActiveCfg = Release|x64 + {5866042C-7FCB-4DB1-BAAD-44DF6567511F}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/lib/base.h b/lib/base.h new file mode 100644 index 0000000..71d5727 --- /dev/null +++ b/lib/base.h @@ -0,0 +1,35 @@ +#ifndef SYNC_BASE_H +#define SYNC_BASE_H + +#ifdef _MSC_VER + #define _CRT_SECURE_NO_WARNINGS + #define _CRT_NONSTDC_NO_DEPRECATE +#endif + +#include + +/* configure inline keyword */ +#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L)) && !defined(__cplusplus) + #if defined(_MSC_VER) || defined(__GNUC__) || defined(__SASC) + #define inline __inline + #else + /* compiler does not support inline */ + #define inline + #endif +#endif + +/* configure lacking CRT features */ +#ifdef _MSC_VER + #if _MSC_VER < 1900 + #define snprintf _snprintf + #endif + /* int is 32-bit for both x86 and x64 */ + typedef unsigned int uint32_t; + #define UINT32_MAX UINT_MAX +#elif defined(__GNUC__) + #include +#elif defined(M68000) + typedef unsigned int uint32_t; +#endif + +#endif /* SYNC_BASE_H */ diff --git a/lib/device.c b/lib/device.c new file mode 100644 index 0000000..9d74d08 --- /dev/null +++ b/lib/device.c @@ -0,0 +1,475 @@ +#include "device.h" +#include "track.h" +#include +#include +#include +#include + +static int find_track(struct sync_device *d, const char *name) +{ + int i; + for (i = 0; i < (int)d->num_tracks; ++i) + if (!strcmp(name, d->tracks[i]->name)) + return i; + return -1; /* not found */ +} + +static const char *sync_track_path(const char *base, const char *name) +{ + static char temp[FILENAME_MAX]; + strncpy(temp, base, sizeof(temp) - 1); + temp[sizeof(temp) - 1] = '\0'; + strncat(temp, "_", sizeof(temp) - 1); + strncat(temp, name, sizeof(temp) - 1); + strncat(temp, ".track", sizeof(temp) - 1); + return temp; +} + +#ifndef SYNC_PLAYER + +#define CLIENT_GREET "hello, synctracker!" +#define SERVER_GREET "hello, demo!" + +enum { + SET_KEY = 0, + DELETE_KEY = 1, + GET_TRACK = 2, + SET_ROW = 3, + PAUSE = 4, + SAVE_TRACKS = 5 +}; + +static inline int socket_poll(SOCKET socket) +{ + struct timeval to = { 0, 0 }; + fd_set fds; + + FD_ZERO(&fds); + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4127) +#endif + FD_SET(socket, &fds); +#ifdef _MSC_VER +#pragma warning(pop) +#endif + + return select((int)socket + 1, &fds, NULL, NULL, &to) > 0; +} + +static inline int xsend(SOCKET s, const void *buf, size_t len, int flags) +{ +#ifdef WIN32 + assert(len <= INT_MAX); + return send(s, (const char *)buf, (int)len, flags) != (int)len; +#else + return send(s, (const char *)buf, len, flags) != (int)len; +#endif +} + +static inline int xrecv(SOCKET s, void *buf, size_t len, int flags) +{ +#ifdef WIN32 + assert(len <= INT_MAX); + return recv(s, (char *)buf, (int)len, flags) != (int)len; +#else + return recv(s, (char *)buf, len, flags) != (int)len; +#endif +} + +#ifdef USE_AMITCP +static struct Library *socket_base = NULL; +#endif + +static SOCKET server_connect(const char *host, unsigned short nport) +{ +#ifdef USE_GETADDRINFO + struct addrinfo *addr; + char port[6]; +#else + struct hostent *he; + char **ap; +#endif + +#ifdef WIN32 + static int need_init = 1; + if (need_init) { + WSADATA wsa; + if (WSAStartup(MAKEWORD(2, 0), &wsa)) + return INVALID_SOCKET; + need_init = 0; + } +#elif defined(USE_AMITCP) + if (!socket_base) { + socket_base = OpenLibrary("bsdsocket.library", 4); + if (!socket_base) + return INVALID_SOCKET; + } +#endif + +#ifdef USE_GETADDRINFO + + snprintf(port, sizeof(port), "%u", nport); + if (getaddrinfo(host, port, 0, &addr) != 0) + return INVALID_SOCKET; + + for (; addr; addr = addr->ai_next) { + SOCKET sock; + int family = addr->ai_family; + struct sockaddr *sa = addr->ai_addr; + int sa_len = (int) addr->ai_addrlen; /* elim. warning on (at least) Win/x64, size_t vs. int/socklen_t */ + +#else + + he = gethostbyname(host); + if (!he) + return INVALID_SOCKET; + + for (ap = he->h_addr_list; *ap; ++ap) { + SOCKET sock; + int family = he->h_addrtype; + struct sockaddr_in sin; + struct sockaddr *sa = (struct sockaddr *)&sin; + int sa_len = sizeof(*sa); + + sin.sin_family = he->h_addrtype; + sin.sin_port = htons(nport); + memcpy(&sin.sin_addr, *ap, he->h_length); + memset(&sin.sin_zero, 0, sizeof(sin.sin_zero)); + +#endif + + sock = socket(family, SOCK_STREAM, 0); + if (sock == INVALID_SOCKET) + continue; + + if (connect(sock, sa, sa_len) >= 0) { + char greet[128]; + + if (xsend(sock, CLIENT_GREET, strlen(CLIENT_GREET), 0) || + xrecv(sock, greet, strlen(SERVER_GREET), 0)) { + closesocket(sock); + continue; + } + + if (!strncmp(SERVER_GREET, greet, strlen(SERVER_GREET))) + return sock; + } + + closesocket(sock); + } + + return INVALID_SOCKET; +} + +#else + +void sync_set_io_cb(struct sync_device *d, struct sync_io_cb *cb) +{ + d->io_cb.open = cb->open; + d->io_cb.read = cb->read; + d->io_cb.close = cb->close; +} + +#endif + +#ifdef NEED_STRDUP +static inline char *rocket_strdup(const char *str) +{ + char *ret = malloc(strlen(str) + 1); + if (ret) + strcpy(ret, str); + return ret; +} +#define strdup rocket_strdup +#endif + +struct sync_device *sync_create_device(const char *base) +{ + struct sync_device *d = malloc(sizeof(*d)); + if (!d) + return NULL; + + d->base = strdup(base); + if (!d->base) { + free(d); + return NULL; + } + + d->tracks = NULL; + d->num_tracks = 0; + +#ifndef SYNC_PLAYER + d->row = -1; + d->sock = INVALID_SOCKET; +#else + d->io_cb.open = (void *(*)(const char *, const char *))fopen; + d->io_cb.read = (size_t (*)(void *, size_t, size_t, void *))fread; + d->io_cb.close = (int (*)(void *))fclose; +#endif + + return d; +} + +void sync_destroy_device(struct sync_device *d) +{ + int i; + for (i = 0; i < (int)d->num_tracks; ++i) { + free(d->tracks[i]->name); + free(d->tracks[i]->keys); + free(d->tracks[i]); + } + free(d->tracks); + free(d->base); + free(d); + +#if defined(USE_AMITCP) && !defined(SYNC_PLAYER) + if (socket_base) { + CloseLibrary(socket_base); + socket_base = NULL; + } +#endif +} + +#ifdef SYNC_PLAYER + +static int get_track_data(struct sync_device *d, struct sync_track *t) +{ + int i; + void *fp = d->io_cb.open(sync_track_path(d->base, t->name), "rb"); + if (!fp) + return -1; + + d->io_cb.read(&t->num_keys, sizeof(int), 1, fp); + t->keys = malloc(sizeof(struct track_key) * t->num_keys); + if (!t->keys) + return -1; + + for (i = 0; i < (int)t->num_keys; ++i) { + struct track_key *key = t->keys + i; + char type; + d->io_cb.read(&key->row, sizeof(int), 1, fp); + d->io_cb.read(&key->value, sizeof(float), 1, fp); + d->io_cb.read(&type, sizeof(char), 1, fp); + key->type = (enum key_type)type; + } + + d->io_cb.close(fp); + return 0; +} + +#else + +static int save_track(const struct sync_track *t, const char *path) +{ + int i; + FILE *fp = fopen(path, "wb"); + if (!fp) + return -1; + + fwrite(&t->num_keys, sizeof(int), 1, fp); + for (i = 0; i < (int)t->num_keys; ++i) { + char type = (char)t->keys[i].type; + fwrite(&t->keys[i].row, sizeof(int), 1, fp); + fwrite(&t->keys[i].value, sizeof(float), 1, fp); + fwrite(&type, sizeof(char), 1, fp); + } + + fclose(fp); + return 0; +} + +void sync_save_tracks(const struct sync_device *d) +{ + int i; + for (i = 0; i < (int)d->num_tracks; ++i) { + const struct sync_track *t = d->tracks[i]; + save_track(t, sync_track_path(d->base, t->name)); + } +} + +static int get_track_data(struct sync_device *d, struct sync_track *t) +{ + unsigned char cmd = GET_TRACK; + uint32_t name_len; + + assert(strlen(t->name) <= UINT32_MAX); + name_len = htonl((uint32_t)strlen(t->name)); + + /* send request data */ + if (xsend(d->sock, (char *)&cmd, 1, 0) || + xsend(d->sock, (char *)&name_len, sizeof(name_len), 0) || + xsend(d->sock, t->name, (int)strlen(t->name), 0)) + { + closesocket(d->sock); + d->sock = INVALID_SOCKET; + return -1; + } + + return 0; +} + +static int handle_set_key_cmd(SOCKET sock, struct sync_device *data) +{ + uint32_t track, row; + union { + float f; + uint32_t i; + } v; + struct track_key key; + unsigned char type; + + if (xrecv(sock, (char *)&track, sizeof(track), 0) || + xrecv(sock, (char *)&row, sizeof(row), 0) || + xrecv(sock, (char *)&v.i, sizeof(v.i), 0) || + xrecv(sock, (char *)&type, 1, 0)) + return -1; + + track = ntohl(track); + v.i = ntohl(v.i); + + key.row = ntohl(row); + key.value = v.f; + + assert(type < KEY_TYPE_COUNT); + assert(track < data->num_tracks); + key.type = (enum key_type)type; + return sync_set_key(data->tracks[track], &key); +} + +static int handle_del_key_cmd(SOCKET sock, struct sync_device *data) +{ + uint32_t track, row; + + if (xrecv(sock, (char *)&track, sizeof(track), 0) || + xrecv(sock, (char *)&row, sizeof(row), 0)) + return -1; + + track = ntohl(track); + row = ntohl(row); + + assert(track < data->num_tracks); + return sync_del_key(data->tracks[track], row); +} + +int sync_connect(struct sync_device *d, const char *host, unsigned short port) +{ + int i; + if (d->sock != INVALID_SOCKET) + closesocket(d->sock); + + d->sock = server_connect(host, port); + if (d->sock == INVALID_SOCKET) + return -1; + + for (i = 0; i < (int)d->num_tracks; ++i) { + free(d->tracks[i]->keys); + d->tracks[i]->keys = NULL; + d->tracks[i]->num_keys = 0; + } + + for (i = 0; i < (int)d->num_tracks; ++i) { + if (get_track_data(d, d->tracks[i])) { + closesocket(d->sock); + d->sock = INVALID_SOCKET; + return -1; + } + } + return 0; +} + +int sync_update(struct sync_device *d, int row, struct sync_cb *cb, + void *cb_param) +{ + if (d->sock == INVALID_SOCKET) + return -1; + + /* look for new commands */ + while (socket_poll(d->sock)) { + unsigned char cmd = 0, flag; + uint32_t row; + if (xrecv(d->sock, (char *)&cmd, 1, 0)) + goto sockerr; + + switch (cmd) { + case SET_KEY: + if (handle_set_key_cmd(d->sock, d)) + goto sockerr; + break; + case DELETE_KEY: + if (handle_del_key_cmd(d->sock, d)) + goto sockerr; + break; + case SET_ROW: + if (xrecv(d->sock, (char *)&row, sizeof(row), 0)) + goto sockerr; + if (cb && cb->set_row) + cb->set_row(cb_param, ntohl(row)); + break; + case PAUSE: + if (xrecv(d->sock, (char *)&flag, 1, 0)) + goto sockerr; + if (cb && cb->pause) + cb->pause(cb_param, flag); + break; + case SAVE_TRACKS: + sync_save_tracks(d); + break; + default: + fprintf(stderr, "unknown cmd: %02x\n", cmd); + goto sockerr; + } + } + + if (cb && cb->is_playing && cb->is_playing(cb_param)) { + if (d->row != row && d->sock != INVALID_SOCKET) { + unsigned char cmd = SET_ROW; + uint32_t nrow = htonl(row); + if (xsend(d->sock, (char*)&cmd, 1, 0) || + xsend(d->sock, (char*)&nrow, sizeof(nrow), 0)) + goto sockerr; + d->row = row; + } + } + return 0; + +sockerr: + closesocket(d->sock); + d->sock = INVALID_SOCKET; + return -1; +} + +#endif + +static int create_track(struct sync_device *d, const char *name) +{ + struct sync_track *t; + assert(find_track(d, name) < 0); + + t = malloc(sizeof(*t)); + t->name = strdup(name); + t->keys = NULL; + t->num_keys = 0; + + d->num_tracks++; + d->tracks = realloc(d->tracks, sizeof(d->tracks[0]) * d->num_tracks); + d->tracks[d->num_tracks - 1] = t; + + return (int)d->num_tracks - 1; +} + +const struct sync_track *sync_get_track(struct sync_device *d, + const char *name) +{ + struct sync_track *t; + int idx = find_track(d, name); + if (idx >= 0) + return d->tracks[idx]; + + idx = create_track(d, name); + t = d->tracks[idx]; + + get_track_data(d, t); + return t; +} diff --git a/lib/device.h b/lib/device.h new file mode 100644 index 0000000..4628724 --- /dev/null +++ b/lib/device.h @@ -0,0 +1,55 @@ +#ifndef SYNC_DEVICE_H +#define SYNC_DEVICE_H + +#include "base.h" +#include "sync.h" + +#ifndef SYNC_PLAYER + +/* configure socket-stack */ +#ifdef _WIN32 + #define WIN32_LEAN_AND_MEAN + #define USE_GETADDRINFO + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include + #include + #include + #include +#elif defined(USE_AMITCP) + #include + #include + #include + #include + #define SOCKET int + #define INVALID_SOCKET -1 + #define select(n,r,w,e,t) WaitSelect(n,r,w,e,t,0) + #define closesocket(x) CloseSocket(x) +#else + #include + #include + #include + #include + #include + #define SOCKET int + #define INVALID_SOCKET -1 + #define closesocket(x) close(x) +#endif + +#endif /* !defined(SYNC_PLAYER) */ + +struct sync_device { + char *base; + struct sync_track **tracks; + size_t num_tracks; + +#ifndef SYNC_PLAYER + int row; + SOCKET sock; +#else + struct sync_io_cb io_cb; +#endif +}; + +#endif /* SYNC_DEVICE_H */ diff --git a/lib/librocket.vs2008.vcproj b/lib/librocket.vs2008.vcproj new file mode 100644 index 0000000..3f65814 --- /dev/null +++ b/lib/librocket.vs2008.vcproj @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/librocket.vs2013.vcxproj b/lib/librocket.vs2013.vcxproj new file mode 100644 index 0000000..ca177b6 --- /dev/null +++ b/lib/librocket.vs2013.vcxproj @@ -0,0 +1,292 @@ + + + + + Debug Client + Win32 + + + Debug Client + x64 + + + Debug + Win32 + + + Debug + x64 + + + Release Client + Win32 + + + Release Client + x64 + + + Release + Win32 + + + Release + x64 + + + + {5866042C-7FCB-4DB1-BAAD-44DF6567511F} + librocket + Win32Proj + librocket + + + + StaticLibrary + v120 + MultiByte + true + + + StaticLibrary + v120 + MultiByte + + + StaticLibrary + v120 + MultiByte + true + + + StaticLibrary + v120 + MultiByte + + + StaticLibrary + v120 + MultiByte + true + + + StaticLibrary + v120 + MultiByte + + + StaticLibrary + v120 + MultiByte + true + + + StaticLibrary + v120 + MultiByte + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>12.0.30501.0 + + + $(SolutionDir)lib\ + $(Configuration)\ + $(ProjectName)-playerd + + + $(SolutionDir)lib\ + $(Configuration)\ + $(ProjectName)-player + + + $(SolutionDir)lib\ + $(Configuration)\ + $(ProjectName)d + + + $(SolutionDir)lib\ + $(Configuration)\ + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + $(ProjectName)-playerd + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + $(ProjectName)-player + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + $(ProjectName)d + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + + + + Disabled + WIN32;_DEBUG;_LIB;SYNC_PLAYER;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebug + + Level3 + EditAndContinue + + + $(OutDir)$(ProjectName)-playerd.lib + + + + + WIN32;NDEBUG;_LIB;SYNC_PLAYER;%(PreprocessorDefinitions) + MultiThreadedDLL + + Level3 + ProgramDatabase + + + $(OutDir)$(ProjectName)-player.lib + + + + + Disabled + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebug + + Level4 + EditAndContinue + + + ws2_32.lib;%(AdditionalDependencies) + $(OutDir)$(ProjectName)d.lib + + + + + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + MultiThreadedDLL + + Level3 + ProgramDatabase + + + ws2_32.lib;%(AdditionalDependencies) + + + + + X64 + + + Disabled + WIN32;_DEBUG;_LIB;SYNC_PLAYER;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebug + + Level3 + ProgramDatabase + + + $(OutDir)$(ProjectName)-playerd.lib + + + + + X64 + + + WIN32;NDEBUG;_LIB;SYNC_PLAYER;%(PreprocessorDefinitions) + MultiThreadedDLL + + Level3 + ProgramDatabase + + + $(OutDir)$(ProjectName)-player.lib + + + + + X64 + + + Disabled + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebug + + Level4 + ProgramDatabase + + + ws2_32.lib;%(AdditionalDependencies) + $(OutDir)$(ProjectName)d.lib + + + + + X64 + + + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + MultiThreadedDLL + + Level3 + ProgramDatabase + + + ws2_32.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/librocket.vs2013.vcxproj.filters b/lib/librocket.vs2013.vcxproj.filters new file mode 100644 index 0000000..1cbe57d --- /dev/null +++ b/lib/librocket.vs2013.vcxproj.filters @@ -0,0 +1,39 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav + + + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/lib/sync.h b/lib/sync.h new file mode 100644 index 0000000..f3077ff --- /dev/null +++ b/lib/sync.h @@ -0,0 +1,46 @@ +/* Copyright (C) 2010 Contributors + * For conditions of distribution and use, see copyright notice in COPYING + */ + +#ifndef SYNC_H +#define SYNC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct sync_device; +struct sync_track; + +struct sync_device *sync_create_device(const char *); +void sync_destroy_device(struct sync_device *); + +#ifndef SYNC_PLAYER +struct sync_cb { + void (*pause)(void *, int); + void (*set_row)(void *, int); + int (*is_playing)(void *); +}; +#define SYNC_DEFAULT_PORT 1338 +int sync_connect(struct sync_device *, const char *, unsigned short); +int sync_update(struct sync_device *, int, struct sync_cb *, void *); +void sync_save_tracks(const struct sync_device *); +#else /* defined(SYNC_PLAYER) */ +struct sync_io_cb { + void *(*open)(const char *filename, const char *mode); + size_t (*read)(void *ptr, size_t size, size_t nitems, void *stream); + int (*close)(void *stream); +}; +void sync_set_io_cb(struct sync_device *d, struct sync_io_cb *cb); +#endif /* defined(SYNC_PLAYER) */ + +const struct sync_track *sync_get_track(struct sync_device *, const char *); +double sync_get_val(const struct sync_track *, double); + +#ifdef __cplusplus +} +#endif + +#endif /* !defined(SYNC_H) */ diff --git a/lib/track.c b/lib/track.c new file mode 100644 index 0000000..3f6e535 --- /dev/null +++ b/lib/track.c @@ -0,0 +1,124 @@ +#include +#include +#include +#ifndef M_PI +#define M_PI 3.141926 +#endif + +#include "sync.h" +#include "track.h" +#include "base.h" + +static double key_linear(const struct track_key k[2], double row) +{ + double t = (row - k[0].row) / (k[1].row - k[0].row); + return k[0].value + (k[1].value - k[0].value) * t; +} + +static double key_smooth(const struct track_key k[2], double row) +{ + double t = (row - k[0].row) / (k[1].row - k[0].row); + t = t * t * (3 - 2 * t); + return k[0].value + (k[1].value - k[0].value) * t; +} + +static double key_ramp(const struct track_key k[2], double row) +{ + double t = (row - k[0].row) / (k[1].row - k[0].row); + t = pow(t, 2.0); + return k[0].value + (k[1].value - k[0].value) * t; +} + +double sync_get_val(const struct sync_track *t, double row) +{ + int idx, irow; + + /* If we have no keys at all, return a constant 0 */ + if (!t->num_keys) + return 0.0f; + + irow = (int)floor(row); + idx = key_idx_floor(t, irow); + + /* at the edges, return the first/last value */ + if (idx < 0) + return t->keys[0].value; + if (idx > (int)t->num_keys - 2) + return t->keys[t->num_keys - 1].value; + + /* interpolate according to key-type */ + switch (t->keys[idx].type) { + case KEY_STEP: + return t->keys[idx].value; + case KEY_LINEAR: + return key_linear(t->keys + idx, row); + case KEY_SMOOTH: + return key_smooth(t->keys + idx, row); + case KEY_RAMP: + return key_ramp(t->keys + idx, row); + default: + assert(0); + return 0.0f; + } +} + +int sync_find_key(const struct sync_track *t, int row) +{ + int lo = 0, hi = t->num_keys; + + /* binary search, t->keys is sorted by row */ + while (lo < hi) { + int mi = (lo + hi) / 2; + assert(mi != hi); + + if (t->keys[mi].row < row) + lo = mi + 1; + else if (t->keys[mi].row > row) + hi = mi; + else + return mi; /* exact hit */ + } + assert(lo == hi); + + /* return first key after row, negated and biased (to allow -0) */ + return -lo - 1; +} + +#ifndef SYNC_PLAYER +int sync_set_key(struct sync_track *t, const struct track_key *k) +{ + int idx = sync_find_key(t, k->row); + if (idx < 0) { + /* no exact hit, we need to allocate a new key */ + void *tmp; + idx = -idx - 1; + tmp = realloc(t->keys, sizeof(struct track_key) * + (t->num_keys + 1)); + if (!tmp) + return -1; + t->num_keys++; + t->keys = tmp; + memmove(t->keys + idx + 1, t->keys + idx, + sizeof(struct track_key) * (t->num_keys - idx - 1)); + } + t->keys[idx] = *k; + return 0; +} + +int sync_del_key(struct sync_track *t, int pos) +{ + void *tmp; + int idx = sync_find_key(t, pos); + assert(idx >= 0); + memmove(t->keys + idx, t->keys + idx + 1, + sizeof(struct track_key) * (t->num_keys - idx - 1)); + assert(t->keys); + tmp = realloc(t->keys, sizeof(struct track_key) * + (t->num_keys - 1)); + if (t->num_keys != 1 && !tmp) + return -1; + t->num_keys--; + t->keys = tmp; + return 0; +} +#endif diff --git a/lib/track.h b/lib/track.h new file mode 100644 index 0000000..fd59ded --- /dev/null +++ b/lib/track.h @@ -0,0 +1,47 @@ +#ifndef SYNC_TRACK_H +#define SYNC_TRACK_H + +#include +#include +#include "base.h" + +enum key_type { + KEY_STEP, /* stay constant */ + KEY_LINEAR, /* lerp to the next value */ + KEY_SMOOTH, /* smooth curve to the next value */ + KEY_RAMP, + KEY_TYPE_COUNT +}; + +struct track_key { + int row; + float value; + enum key_type type; +}; + +struct sync_track { + char *name; + struct track_key *keys; + int num_keys; +}; + +int sync_find_key(const struct sync_track *, int); +static inline int key_idx_floor(const struct sync_track *t, int row) +{ + int idx = sync_find_key(t, row); + if (idx < 0) + idx = -idx - 2; + return idx; +} + +#ifndef SYNC_PLAYER +int sync_set_key(struct sync_track *, const struct track_key *); +int sync_del_key(struct sync_track *, int); +static inline int is_key_frame(const struct sync_track *t, int row) +{ + return sync_find_key(t, row) >= 0; +} + +#endif /* !defined(SYNC_PLAYER) */ + +#endif /* SYNC_TRACK_H */ diff --git a/rocket.sln b/rocket.sln deleted file mode 100644 index 6681f8f..0000000 --- a/rocket.sln +++ /dev/null @@ -1,20 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 10.00 -# Visual Studio 2008 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sync_editor", "editor\editor.vcproj", "{76B44BC8-8BB4-4B6E-B2FA-7738C9E7F80B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Win32 = Debug|Win32 - Release|Win32 = Release|Win32 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {76B44BC8-8BB4-4B6E-B2FA-7738C9E7F80B}.Debug|Win32.ActiveCfg = Debug|Win32 - {76B44BC8-8BB4-4B6E-B2FA-7738C9E7F80B}.Debug|Win32.Build.0 = Debug|Win32 - {76B44BC8-8BB4-4B6E-B2FA-7738C9E7F80B}.Release|Win32.ActiveCfg = Release|Win32 - {76B44BC8-8BB4-4B6E-B2FA-7738C9E7F80B}.Release|Win32.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/sync/base.h b/sync/base.h deleted file mode 100644 index da1a978..0000000 --- a/sync/base.h +++ /dev/null @@ -1,124 +0,0 @@ -/* Copyright (C) 2007-2010 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ - -#ifndef SYNC_BASE_H -#define SYNC_BASE_H - -/* configure inline keyword */ -#if !defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) - #if defined(_MSC_VER) || defined(__GNUC__) || defined(__SASC) -#ifndef inline - #define inline __inline -#endif -#else - /* compiler does not support inline, make function static instead */ - #define inline static - #endif -#endif - -/* configure lacking CRT features */ -#ifdef _MSC_VER - #define strdup _strdup - #define snprintf _snprintf - /* int is 32-bit for both x86 and x64 */ - typedef unsigned int uint32_t; - #define UINT32_MAX UINT_MAX -#elif defined(__GNUC__) - #include -#elif defined(M68000) - typedef unsigned int uint32_t; -#endif - -/* configure socket-stack */ -#ifdef _WIN32 - #define WIN32_LEAN_AND_MEAN - #define NOMINMAX - #include - #include - #include -#elif defined(USE_AMITCP) - #include - #include - #include - #include - #define SOCKET int - #define INVALID_SOCKET -1 - #define select(n,r,w,e,t) WaitSelect(n,r,w,e,t,0) - #define closesocket(x) CloseSocket(x) -#else - #include - #include - #include - #include - #include - #define SOCKET int - #define INVALID_SOCKET -1 - #define closesocket(x) close(x) -#endif - -#define CLIENT_GREET "hello, synctracker!" -#define SERVER_GREET "hello, demo!" - -enum { - SET_KEY = 0, - DELETE_KEY = 1, - GET_TRACK = 2, - SET_ROW = 3, - PAUSE = 4, - SAVE_TRACKS = 5 -}; - -static inline int socket_poll(SOCKET socket) -{ - struct timeval to = { 0, 0 }; - fd_set fds; - - FD_ZERO(&fds); - -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable: 4127) -#endif - FD_SET(socket, &fds); -#ifdef _MSC_VER -#pragma warning(pop) -#endif - - return select((int)socket + 1, &fds, NULL, NULL, &to) > 0; -} - -#include - -static inline int xsend(SOCKET s, const void *buf, size_t len, int flags) -{ -#ifdef WIN32 - assert(len <= INT_MAX); - return send(s, (const char *)buf, (int)len, flags) != (int)len; -#else - return send(s, (const char *)buf, len, flags) != len; -#endif -} - -static inline int xrecv(SOCKET s, void *buf, size_t len, int flags) -{ -#ifdef WIN32 - assert(len <= INT_MAX); - return recv(s, (char *)buf, (int)len, flags) != (int)len; -#else - return recv(s, (char *)buf, len, flags) != len; -#endif -} - -#ifdef NEED_STRDUP -static inline char *rocket_strdup(const char *str) -{ - char *ret = malloc(strlen(str) + 1); - if (ret) - strcpy(ret, str); - return ret; -} -#define strdup rocket_strdup -#endif - -#endif /* SYNC_BASE_H */ diff --git a/sync/data.c b/sync/data.c deleted file mode 100644 index 59faedd..0000000 --- a/sync/data.c +++ /dev/null @@ -1,33 +0,0 @@ -/* Copyright (C) 2007-2008 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ - -#include "data.h" - -void sync_data_deinit(struct sync_data *d) -{ - int i; - for (i = 0; i < (int)d->num_tracks; ++i) { - free(d->tracks[i]->name); - free(d->tracks[i]->keys); - free(d->tracks[i]); - } - free(d->tracks); -} - -int sync_create_track(struct sync_data *d, const char *name) -{ - struct sync_track *t; - assert(sync_find_track(d, name) < 0); - - t = malloc(sizeof(*t)); - t->name = strdup(name); - t->keys = NULL; - t->num_keys = 0; - - d->num_tracks++; - d->tracks = realloc(d->tracks, sizeof(d->tracks[0]) * d->num_tracks); - d->tracks[d->num_tracks - 1] = t; - - return (int)d->num_tracks - 1; -} diff --git a/sync/data.h b/sync/data.h deleted file mode 100644 index fca7a08..0000000 --- a/sync/data.h +++ /dev/null @@ -1,28 +0,0 @@ -/* Copyright (C) 2007-2010 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ - -#ifndef SYNC_DATA_H -#define SYNC_DATA_H - -#include "track.h" - -struct sync_data { - struct sync_track **tracks; - size_t num_tracks; -}; - -static inline int sync_find_track(const struct sync_data *data, - const char *name) -{ - int i; - for (i = 0; i < (int)data->num_tracks; ++i) - if (!strcmp(name, data->tracks[i]->name)) - return i; - return -1; /* not found */ -} - -void sync_data_deinit(struct sync_data *); -int sync_create_track(struct sync_data *, const char *); - -#endif /* SYNC_DATA_H */ diff --git a/sync/device.c b/sync/device.c deleted file mode 100644 index e9b75c5..0000000 --- a/sync/device.c +++ /dev/null @@ -1,359 +0,0 @@ -/* Copyright (C) 2007-2008 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ - -#include "device.h" -#include "sync.h" -#include -#include - -static const char *sync_track_path(const char *base, const char *name) -{ - static char temp[FILENAME_MAX]; - strncpy(temp, base, sizeof(temp) - 1); - temp[sizeof(temp) - 1] = '\0'; - strncat(temp, "_", sizeof(temp) - 1); - strncat(temp, name, sizeof(temp) - 1); - strncat(temp, ".track", sizeof(temp) - 1); - return temp; -} - -#ifndef SYNC_PLAYER - -#ifdef USE_AMITCP -static struct Library *socket_base = NULL; -#endif - -static SOCKET server_connect(const char *host, unsigned short nport) -{ - struct hostent *he; - struct sockaddr_in sa; - char greet[128], **ap; - SOCKET sock = INVALID_SOCKET; - -#ifdef WIN32 - static int need_init = 1; - if (need_init) { - WSADATA wsa; - if (WSAStartup(MAKEWORD(2, 0), &wsa)) - return INVALID_SOCKET; - need_init = 0; - } -#elif defined(USE_AMITCP) - if (!socket_base) { - socket_base = OpenLibrary("bsdsocket.library", 4); - if (!socket_base) - return INVALID_SOCKET; - } -#endif - - he = gethostbyname(host); - if (!he) - return INVALID_SOCKET; - - for (ap = he->h_addr_list; *ap; ++ap) { - sa.sin_family = he->h_addrtype; - sa.sin_port = htons(nport); - memcpy(&sa.sin_addr, *ap, he->h_length); - - sock = socket(he->h_addrtype, SOCK_STREAM, 0); - if (sock == INVALID_SOCKET) - continue; - - if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) >= 0) - break; - - closesocket(sock); - sock = INVALID_SOCKET; - } - - if (sock == INVALID_SOCKET) - return INVALID_SOCKET; - - if (xsend(sock, CLIENT_GREET, strlen(CLIENT_GREET), 0) || - xrecv(sock, greet, strlen(SERVER_GREET), 0)) - return INVALID_SOCKET; - - if (!strncmp(SERVER_GREET, greet, strlen(SERVER_GREET))) - return sock; - - closesocket(sock); - return INVALID_SOCKET; -} - -#else - -void sync_set_io_cb(struct sync_device *d, struct sync_io_cb *cb) -{ - d->io_cb.open = cb->open; - d->io_cb.read = cb->read; - d->io_cb.close = cb->close; -} - -#endif - -struct sync_device *sync_create_device(const char *base) -{ - struct sync_device *d = malloc(sizeof(*d)); - if (!d) - return NULL; - - d->base = strdup(base); - if (!d->base) { - free(d); - return NULL; - } - - d->data.tracks = NULL; - d->data.num_tracks = 0; - -#ifndef SYNC_PLAYER - d->row = -1; - d->sock = INVALID_SOCKET; -#else - d->io_cb.open = fopen; - d->io_cb.read = fread; - d->io_cb.close = fclose; -#endif - - return d; -} - -void sync_destroy_device(struct sync_device *d) -{ - free(d->base); - sync_data_deinit(&d->data); - free(d); - -#if defined(USE_AMITCP) && !defined(SYNC_PLAYER) - if (socket_base) { - CloseLibrary(socket_base); - socket_base = NULL; - } -#endif -} - -#ifdef SYNC_PLAYER - -static int get_track_data(struct sync_device *d, struct sync_track *t) -{ - int i; - void *fp = d->io_cb.open(sync_track_path(d->base, t->name), "rb"); - if (!fp) - return -1; - - d->io_cb.read(&t->num_keys, sizeof(size_t), 1, fp); - t->keys = malloc(sizeof(struct track_key) * t->num_keys); - if (!t->keys) - return -1; - - for (i = 0; i < (int)t->num_keys; ++i) { - struct track_key *key = t->keys + i; - char type; - d->io_cb.read(&key->row, sizeof(size_t), 1, fp); - d->io_cb.read(&key->value, sizeof(float), 1, fp); - d->io_cb.read(&type, sizeof(char), 1, fp); - key->type = (enum key_type)type; - } - - d->io_cb.close(fp); - return 0; -} - -#else - -static int save_track(const struct sync_track *t, const char *path) -{ - int i; - FILE *fp = fopen(path, "wb"); - if (!fp) - return -1; - - fwrite(&t->num_keys, sizeof(size_t), 1, fp); - for (i = 0; i < (int)t->num_keys; ++i) { - char type = (char)t->keys[i].type; - fwrite(&t->keys[i].row, sizeof(int), 1, fp); - fwrite(&t->keys[i].value, sizeof(float), 1, fp); - fwrite(&type, sizeof(char), 1, fp); - } - - fclose(fp); - return 0; -} - -void sync_save_tracks(const struct sync_device *d) -{ - int i; - for (i = 0; i < (int)d->data.num_tracks; ++i) { - const struct sync_track *t = d->data.tracks[i]; - save_track(t, sync_track_path(d->base, t->name)); - } -} - -static int get_track_data(struct sync_device *d, struct sync_track *t) -{ - unsigned char cmd = GET_TRACK; - uint32_t name_len; - - assert(strlen(t->name) <= UINT32_MAX); - name_len = htonl((uint32_t)strlen(t->name)); - - /* send request data */ - if (xsend(d->sock, (char *)&cmd, 1, 0) || - xsend(d->sock, (char *)&name_len, sizeof(name_len), 0) || - xsend(d->sock, t->name, (int)strlen(t->name), 0)) - { - closesocket(d->sock); - d->sock = INVALID_SOCKET; - return -1; - } - - return 0; -} - -static int handle_set_key_cmd(SOCKET sock, struct sync_data *data) -{ - uint32_t track, row; - union { - float f; - uint32_t i; - } v; - struct track_key key; - unsigned char type; - - if (xrecv(sock, (char *)&track, sizeof(track), 0) || - xrecv(sock, (char *)&row, sizeof(row), 0) || - xrecv(sock, (char *)&v.i, sizeof(v.i), 0) || - xrecv(sock, (char *)&type, 1, 0)) - return -1; - - track = ntohl(track); - v.i = ntohl(v.i); - - key.row = ntohl(row); - key.value = v.f; - - assert(type < KEY_TYPE_COUNT); - assert(track < data->num_tracks); - key.type = (enum key_type)type; - return sync_set_key(data->tracks[track], &key); -} - -static int handle_del_key_cmd(SOCKET sock, struct sync_data *data) -{ - uint32_t track, row; - - if (xrecv(sock, (char *)&track, sizeof(track), 0) || - xrecv(sock, (char *)&row, sizeof(row), 0)) - return -1; - - track = ntohl(track); - row = ntohl(row); - - assert(track < data->num_tracks); - return sync_del_key(data->tracks[track], row); -} - -int sync_connect(struct sync_device *d, const char *host, unsigned short port) -{ - int i; - if (d->sock != INVALID_SOCKET) - closesocket(d->sock); - - d->sock = server_connect(host, port); - if (d->sock == INVALID_SOCKET) - return -1; - - for (i = 0; i < (int)d->data.num_tracks; ++i) { - free(d->data.tracks[i]->keys); - d->data.tracks[i]->keys = NULL; - d->data.tracks[i]->num_keys = 0; - } - - for (i = 0; i < (int)d->data.num_tracks; ++i) { - if (get_track_data(d, d->data.tracks[i])) { - closesocket(d->sock); - d->sock = INVALID_SOCKET; - return -1; - } - } - return 0; -} - -int sync_update(struct sync_device *d, int row, struct sync_cb *cb, - void *cb_param) -{ - if (d->sock == INVALID_SOCKET) - return -1; - - /* look for new commands */ - while (socket_poll(d->sock)) { - unsigned char cmd = 0, flag; - uint32_t row; - if (xrecv(d->sock, (char *)&cmd, 1, 0)) - goto sockerr; - - switch (cmd) { - case SET_KEY: - if (handle_set_key_cmd(d->sock, &d->data)) - goto sockerr; - break; - case DELETE_KEY: - if (handle_del_key_cmd(d->sock, &d->data)) - goto sockerr; - break; - case SET_ROW: - if (xrecv(d->sock, (char *)&row, sizeof(row), 0)) - goto sockerr; - if (cb && cb->set_row) - cb->set_row(cb_param, ntohl(row)); - break; - case PAUSE: - if (xrecv(d->sock, (char *)&flag, 1, 0)) - goto sockerr; - if (cb && cb->pause) - cb->pause(cb_param, flag); - break; - case SAVE_TRACKS: - sync_save_tracks(d); - break; - default: - fprintf(stderr, "unknown cmd: %02x\n", cmd); - goto sockerr; - } - } - - if (cb && cb->is_playing && cb->is_playing(cb_param)) { - if (d->row != row && d->sock != INVALID_SOCKET) { - unsigned char cmd = SET_ROW; - uint32_t nrow = htonl(row); - if (xsend(d->sock, (char*)&cmd, 1, 0) || - xsend(d->sock, (char*)&nrow, sizeof(nrow), 0)) - goto sockerr; - d->row = row; - } - } - return 0; - -sockerr: - closesocket(d->sock); - d->sock = INVALID_SOCKET; - return -1; -} - -#endif - -const struct sync_track *sync_get_track(struct sync_device *d, - const char *name) -{ - struct sync_track *t; - int idx = sync_find_track(&d->data, name); - if (idx >= 0) - return d->data.tracks[idx]; - - idx = sync_create_track(&d->data, name); - t = d->data.tracks[idx]; - - get_track_data(d, t); - return t; -} diff --git a/sync/device.h b/sync/device.h deleted file mode 100644 index 4dca4bf..0000000 --- a/sync/device.h +++ /dev/null @@ -1,23 +0,0 @@ -/* Copyright (C) 2007-2008 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ - -#ifndef SYNC_DEVICE_H -#define SYNC_DEVICE_H - -#include "data.h" -#include "sync.h" - -struct sync_device { - char *base; - struct sync_data data; - -#ifndef SYNC_PLAYER - int row; - SOCKET sock; -#else - struct sync_io_cb io_cb; -#endif -}; - -#endif /* SYNC_DEVICE_H */ diff --git a/sync/sync.h b/sync/sync.h deleted file mode 100644 index 70987ef..0000000 --- a/sync/sync.h +++ /dev/null @@ -1,44 +0,0 @@ -/* Copyright (C) 2010 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ - -#ifndef SYNC_H -#define SYNC_H - -#ifdef __cplusplus -extern "C" { -#endif - -struct sync_device; -struct sync_track; - -struct sync_device *sync_create_device(const char *); -void sync_destroy_device(struct sync_device *); - -#ifndef SYNC_PLAYER -struct sync_cb { - void (*pause)(void *, int); - void (*set_row)(void *, int); - int (*is_playing)(void *); -}; -#define SYNC_DEFAULT_PORT 1338 -int sync_connect(struct sync_device *, const char *, unsigned short); -int sync_update(struct sync_device *, int, struct sync_cb *, void *); -void sync_save_tracks(const struct sync_device *); -#else /* defined(SYNC_PLAYER) */ -struct sync_io_cb { - void *(*open)(const char *filename, const char *mode); - size_t (*read)(void *ptr, size_t size, size_t nitems, void *stream); - int (*close)(void *stream); -}; -void sync_set_io_cb(struct sync_device *d, struct sync_io_cb *cb); -#endif /* defined(SYNC_PLAYER) */ - -const struct sync_track *sync_get_track(struct sync_device *, const char *); -double sync_get_val(const struct sync_track *, double); - -#ifdef __cplusplus -} -#endif - -#endif /* !defined(SYNC_H) */ diff --git a/sync/track.c b/sync/track.c deleted file mode 100644 index 05f4793..0000000 --- a/sync/track.c +++ /dev/null @@ -1,128 +0,0 @@ -/* Copyright (C) 2010 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ - -#include -#include -#include -#ifndef M_PI -#define M_PI 3.141926 -#endif - -#include "sync.h" -#include "track.h" -#include "base.h" - -static double key_linear(const struct track_key k[2], double row) -{ - double t = (row - k[0].row) / (k[1].row - k[0].row); - return k[0].value + (k[1].value - k[0].value) * t; -} - -static double key_smooth(const struct track_key k[2], double row) -{ - double t = (row - k[0].row) / (k[1].row - k[0].row); - t = t * t * (3 - 2 * t); - return k[0].value + (k[1].value - k[0].value) * t; -} - -static double key_ramp(const struct track_key k[2], double row) -{ - double t = (row - k[0].row) / (k[1].row - k[0].row); - t = pow(t, 2.0); - return k[0].value + (k[1].value - k[0].value) * t; -} - -double sync_get_val(const struct sync_track *t, double row) -{ - int idx, irow; - - /* If we have no keys at all, return a constant 0 */ - if (!t->num_keys) - return 0.0f; - - irow = (int)floor(row); - idx = key_idx_floor(t, irow); - - /* at the edges, return the first/last value */ - if (idx < 0) - return t->keys[0].value; - if (idx > (int)t->num_keys - 2) - return t->keys[t->num_keys - 1].value; - - /* interpolate according to key-type */ - switch (t->keys[idx].type) { - case KEY_STEP: - return t->keys[idx].value; - case KEY_LINEAR: - return key_linear(t->keys + idx, row); - case KEY_SMOOTH: - return key_smooth(t->keys + idx, row); - case KEY_RAMP: - return key_ramp(t->keys + idx, row); - default: - assert(0); - return 0.0f; - } -} - -int sync_find_key(const struct sync_track *t, int row) -{ - int lo = 0, hi = t->num_keys; - - /* binary search, t->keys is sorted by row */ - while (lo < hi) { - int mi = (lo + hi) / 2; - assert(mi != hi); - - if (t->keys[mi].row < row) - lo = mi + 1; - else if (t->keys[mi].row > row) - hi = mi; - else - return mi; /* exact hit */ - } - assert(lo == hi); - - /* return first key after row, negated and biased (to allow -0) */ - return -lo - 1; -} - -#ifndef SYNC_PLAYER -int sync_set_key(struct sync_track *t, const struct track_key *k) -{ - int idx = sync_find_key(t, k->row); - if (idx < 0) { - /* no exact hit, we need to allocate a new key */ - void *tmp; - idx = -idx - 1; - tmp = realloc(t->keys, sizeof(struct track_key) * - (t->num_keys + 1)); - if (!tmp) - return -1; - t->num_keys++; - t->keys = tmp; - memmove(t->keys + idx + 1, t->keys + idx, - sizeof(struct track_key) * (t->num_keys - idx - 1)); - } - t->keys[idx] = *k; - return 0; -} - -int sync_del_key(struct sync_track *t, int pos) -{ - void *tmp; - int idx = sync_find_key(t, pos); - assert(idx >= 0); - memmove(t->keys + idx, t->keys + idx + 1, - sizeof(struct track_key) * (t->num_keys - idx - 1)); - assert(t->keys); - tmp = realloc(t->keys, sizeof(struct track_key) * - (t->num_keys - 1)); - if (t->num_keys != 1 && !tmp) - return -1; - t->num_keys--; - t->keys = tmp; - return 0; -} -#endif diff --git a/sync/track.h b/sync/track.h deleted file mode 100644 index 9c9d24a..0000000 --- a/sync/track.h +++ /dev/null @@ -1,51 +0,0 @@ -/* Copyright (C) 2007-2010 Erik Faye-Lund and Egbert Teeselink - * For conditions of distribution and use, see copyright notice in COPYING - */ - -#ifndef SYNC_TRACK_H -#define SYNC_TRACK_H - -#include -#include -#include "base.h" - -enum key_type { - KEY_STEP, /* stay constant */ - KEY_LINEAR, /* lerp to the next value */ - KEY_SMOOTH, /* smooth curve to the next value */ - KEY_RAMP, - KEY_TYPE_COUNT -}; - -struct track_key { - int row; - float value; - enum key_type type; -}; - -struct sync_track { - char *name; - struct track_key *keys; - int num_keys; -}; - -int sync_find_key(const struct sync_track *, int); -static inline int key_idx_floor(const struct sync_track *t, int row) -{ - int idx = sync_find_key(t, row); - if (idx < 0) - idx = -idx - 2; - return idx; -} - -#ifndef SYNC_PLAYER -int sync_set_key(struct sync_track *, const struct track_key *); -int sync_del_key(struct sync_track *, int); -static inline int is_key_frame(const struct sync_track *t, int row) -{ - return sync_find_key(t, row) >= 0; -} - -#endif /* !defined(SYNC_PLAYER) */ - -#endif /* SYNC_TRACK_H */ diff --git a/sync_player.vcproj b/sync_player.vcproj deleted file mode 100644 index fbe9afe..0000000 --- a/sync_player.vcproj +++ /dev/null @@ -1,570 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -