diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9ea681 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# IntelliJ Idea Project Files +*.iml +.idea \ No newline at end of file diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..4c19bd0 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,16 @@ +files['.luacheckrc'].global = false +std = 'max+busted' + +globals = { + 'love', + 'getVersion', + 'getTitle' +} + +exclude_files = { + './lua_install/*' +} + +ignore = { + '/self' +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 828cf56..d5be13e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,54 @@ -# Version 0xxx - 2015/xx/xx +# Version 1.0.0.572 - 2016-12-23 + +### Additions +- Added [Graphoon](https://github.com/rm-code/Graphoon) library +- Added a loading screen (Shows a nice graph animation while the repositories are loaded) +- Added a completely new selection screen + - Repositories can be named after being dropped on the application + - Mouse indicates clickable elements + - Added small instruction messages if LoGiVi is started for the first time +- Added a custom icon (Thanks to https://twitter.com/nsK_pz) +- Added an option to hide author labels (Closes [#65](https://github.com/rm-code/logivi/issues/65)) + +### Fixes +- Fixed default config not being used correctly +- Fixed pattern matching for windows specific paths +- Fixed camera being positioned off center when the visualisation starts +- Fixed nodes spawning on a straight line occasionally +- Fixed [#69](https://github.com/rm-code/logivi/issues/69) - Adjust position of folder labels if node is empty or just contains one file +- Fixed [#70](https://github.com/rm-code/logivi/issues/70) - Faulty creation of file colors + +### Removals +- Removed loading of custom avatars +- Removed code for deletion of temporary files +- Removed some of the debug output +- Removed warning and creation of example log when LoGiVi is loaded for the first time + +### Other Changes +- Completely rewrote the Timeline + - Clicks should be much more precise now + - Timeline is fading out when the mouse isn't hovering over it (Date label changes position accordingly) +- Use spritebatch when drawing avatars +- Improve warning message displayed when running LoGiVi for the first time +- Config Files are only loaded once when the program starts +- Updated config file for use with LÖVE 0.10.1 +- Repositories are stored in a separate file now + - This way we can also refresh logs which have been added via the directorydropped callback + - Repositories are only updated if they have changed since the last time they were opened +- Improved settings template +- Avatar icons have custom colors (Closes [#66](https://github.com/rm-code/logivi/issues/66)) +- Updated the sprites used for avatar icons (Closes [#67](https://github.com/rm-code/logivi/issues/67)) + +--- + +# Version 0432 - 2015-12-14 ### Additions - Added scaling for folder and name labels based on the camera's zoom factor - Added MessageBox which displays a warning in case git isn't found on the user's system (Closes [#50](https://github.com/rm-code/logivi/issues/50)) - Added mouse panning and scaling (Closes [#45](https://github.com/rm-code/logivi/issues/45)) - - The mouse can be used to drag around the camera while the left button is pressed - - The mouse wheel can be used to zoom in and out + - The mouse can be used to drag around the camera while the left button is pressed + - The mouse wheel can be used to zoom in and out ### Fixes - Fixed [#51](https://github.com/rm-code/logivi/issues/51) - Fixed crash caused by faulty variable @@ -15,9 +58,11 @@ ### Other Changes - LoGiVi now starts in windowed mode on first start -- Canged design of the file panel to be less intrusive +- Changed design of the file panel to be less intrusive + +--- -# Version 0404 - 2015/11/24 +# Version 0404 - 2015-11-24 ### Additions - Added option to add a repository by dropping its folder onto LoGiVi (Closes [#46](https://github.com/rm-code/logivi/issues/46)) @@ -29,7 +74,9 @@ - Fixed [#44](https://github.com/rm-code/logivi/issues/44) - File paths are validated after the config has been validated - Fixed direction of camera rotation -# Version 0375 - 2015/11/11 +--- + +# Version 0375 - 2015-11-11 **Important**: With this version LoGiVi now ***requires*** LÖVE Version [0.10.0](https://love2d.org/wiki/0.10.0) to run and will no longer work with LÖVE 0.9.2! LÖVE 0.10.0 has not yet been officially released, but can be compiled from the source. For more information check out the [official LÖVE repository](https://bitbucket.org/rude/love/overview). @@ -41,7 +88,9 @@ ### Other Changes - Updated LoGiVi to run on LÖVE 0.10.0 -# Version 0351 - 2015/08/01 +--- + +# Version 0351 - 2015-08-01 ### Additions - Added authors' names to their avatars @@ -58,26 +107,27 @@ ### Other Changes - Display a default string when no custom information about a project can be loaded +--- -# Version 0312 - 2015/04/20 +# Version 0312 - 2015-04-20 ### Additions - Added keybinding for easy exiting - Added selection screen - - LoGiVi can keep track of multiple git logs - - User can select which log to visualize on the selections screen - - User can use "exit"-key to return to the selection screen - - Log-selection list is scrollable with the mouse wheel - - Added watch button which takes the user to the visualization of the selected log + - LoGiVi can keep track of multiple git logs + - User can select which log to visualize on the selections screen + - User can use "exit"-key to return to the selection screen + - Log-selection list is scrollable with the mouse wheel + - Added watch button which takes the user to the visualization of the selected log - Added example log which will be written to the save directory if no logs are found - Added option to specify a custom color for a file extension in the config file - Git logs can now be created from within LoGiVi (Closes [#3](https://github.com/rm-code/logivi/issues/3)) - - The user can specify the path to a local repository in the config file - - LoGiVi will automatically create a log and load this repository on start - - Information about the repository will be automatically written to the project folder (first commit, latest commit, total number of commits) - - This currently doesn't work on Windows (See [#28](https://github.com/rm-code/logivi/issues/28)) - - Information is displayed on the info panel - - Added a refresh button to the SelectionScreen's info panel, which can be used to update the selected log + - The user can specify the path to a local repository in the config file + - LoGiVi will automatically create a log and load this repository on start + - Information about the repository will be automatically written to the project folder (first commit, latest commit, total number of commits) + - This currently doesn't work on Windows (See [#28](https://github.com/rm-code/logivi/issues/28)) + - Information is displayed on the info panel + - Added a refresh button to the SelectionScreen's info panel, which can be used to update the selected log - Added function to sort files based on their extension while placing them around their folder node (Closes [#22](https://github.com/rm-code/logivi/issues/22)) - Added button to SelectionScreen which opens the save directory - Added tooltips @@ -92,7 +142,7 @@ - Fixed [#30](https://github.com/rm-code/logivi/issues/30) - Ignore files when no changes were applied - Fixed [#29](https://github.com/rm-code/logivi/issues/29) - Reset the FileManager when MainScreen is closed - Fixed [#27](https://github.com/rm-code/logivi/issues/27) - Replace escape characters in the path to a repository -- Fixed [#23](https://github.com/rm-code/logivi/issues/23) - Increase speed at which example is written to the harddrive +- Fixed [#23](https://github.com/rm-code/logivi/issues/23) - Increase speed at which example is written to the HDD - Fixed [#20](https://github.com/rm-code/logivi/issues/20) - Center the screen when it is resized in the config - Fixed [#19](https://github.com/rm-code/logivi/issues/19) - Allow multiple key bindings - Fixed [#5](https://github.com/rm-code/logivi/issues/5) - Improve author movement @@ -104,7 +154,9 @@ - Reduced time before authors start fading - Config file now uses a custom format based on ini-files -# Version 0204 - 2015/04/10 +--- + +# Version 0204 - 2015-04-10 ### Additions - Added option to set the visibility of folder labels in the config file @@ -112,13 +164,13 @@ - Added keybinding for pausing the automatic commit loading - Added keybinding for manually loading the next commit - Added keybinding for manually loading the previous commit -- Added keybinding for rewersing the graph creation (will run back until it reaches the first commit) +- Added keybinding for reversing the graph creation (will run back until it reaches the first commit) - Added keybinding for toggling fullscreen mode - Added a timeline - - Indicates the current position of the log compared to the total commit history and shows the date of the currently indexed commit - - Allows the user to quickly jump around in time (forward and backwards) while still rendering the full graph (Closes [#10](https://github.com/rm-code/logivi/issues/10)) - - Can be hidden via keybinding or in the config file -- Added option to the config file which makes the visualisation start at the end of the git log (so it starts with the newest commit and moves towards the oldest) + - Indicates the current position of the log compared to the total commit history and shows the date of the currently indexed commit + - Allows the user to quickly jump around in time (forward and backwards) while still rendering the full graph (Closes [#10](https://github.com/rm-code/logivi/issues/10)) + - Can be hidden via keybinding or in the config file +- Added option to the config file which makes the visualization start at the end of the git log (so it starts with the newest commit and moves towards the oldest) - Added option to disable autoplay in the config file ### Fixes @@ -135,7 +187,7 @@ --- -# Version 0142 - 2015/04/01 +# Version 0142 - 2015-04-01 ### Additions - Added more options to the logivi config file @@ -157,19 +209,19 @@ - Fixed [#2](https://github.com/rm-code/logivi/issues/2) - Edges are removed correctly when a node is killed ### Other Changes -- Updated message box when no git log is found and added a button to directly open the logivi wiki +- Updated message box when no git log is found and added a button to directly open the LoGiVi wiki - Improved graph layout by tweaking the mass calculation and charge of each node (edges should now be shorter which reduces the total size of the graph) - Increased width of the graph's edges - Replaced old movement code for authors with physical based approach (Closes [#5](https://github.com/rm-code/logivi/issues/5)) --- -# Version 0104 - 2015/03/30 +# Version 0104 - 2015-03-30 ### Additions - Added debug information about the user's system and supported features of the LÖVE framework which will be printed to the console - Added configuration file reader which will contain all options for LoGiVi - - This means we can get rid of the _aliases_ and _avatars_ files since they now are bundled in the config file + - This means we can get rid of the _aliases_ and _avatars_ files since they now are bundled in the config file - Added option to set a background color in the configuration file - Added option for setting a resolution in the configuration file - Added possibility use local images as avatars @@ -189,10 +241,10 @@ ### Other Changes - Rewrote most of the graph system - - The graph is structured and handled completely different than before with files, folder nodes and edges being independent from each other - - Gets rid of a lot of issues like edges overlaying other nodes - - The arrangement of files around folder nodes is no longer updated every frame - - Major improvements in memory usage, performance and garbage production + - The graph is structured and handled completely different than before with files, folder nodes and edges being independent from each other + - Gets rid of a lot of issues like edges overlaying other nodes + - The arrangement of files around folder nodes is no longer updated every frame + - Major improvements in memory usage, performance and garbage production - Updated log reader to separate commits based on the author tag instead of looking for the "special" logivi_commit tag (which was pretty useless anyway) - Updated log reader to digest unix timestamps and transform them into human readable dates - Updated arrangement of file nodes to make them fill up the empty space where the folder nodes used to be @@ -207,20 +259,20 @@ --- -# Version 0052 - 2015/01/18 +# Version 0052 - 2015-01-18 ### Additions -- Added (rudimentary) Force-Directed Graph which - visualises the files and folders of a git repository at a given point in time - - Files are represented as evenly distributed leaves around their parent folder node - - Depending on the amount of files in one folder new folders will be created automatically) - - Modified files are coloured red and fade back to their original color over time - - Folders are represented as single green dots (this will be changed in one of the next releases) and are connected by lines +- Added (rudimentary) Force-Directed Graph which - visualizes the files and folders of a git repository at a given point in time + - Files are represented as evenly distributed leaves around their parent folder node + - Depending on the amount of files in one folder new folders will be created automatically) + - Modified files are colored red and fade back to their original color over time + - Folders are represented as single green dots (this will be changed in one of the next releases) and are connected by lines - Added list of all authors contributing to the project - Added list of all file extensions found in the project -- Added colouring of file nodes based on their file extensions +- Added coloring of file nodes based on their file extensions - Added camera which keeps tracking the generated graph automatically - Added floating authors - - Authors will show links to the files they currently edit - - Authors can be assigned an alias - - Authors can be assigned an avatar (grabbed online) + - Authors will show links to the files they currently edit + - Authors can be assigned an alias + - Authors can be assigned an avatar (grabbed online) - Added warning message if no log file can be found diff --git a/LICENSE.md b/LICENSE.md index b2c5812..38a3faa 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 - 2015 Robert Machmer +Copyright (c) 2014 - 2016 Robert Machmer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index bf3a8b7..db89922 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,7 @@ -# LoGiVi +# LoGiVi ![image](https://raw.githubusercontent.com/rm-code/logivi/develop/res/img/icon/64px.png) -LoGiVi is a git-repository visualisation tool inspired by [Gource](http://gource.io/) and __currently in development__. It was written from scratch using [Lua](http://www.lua.org/) and the [LÖVE](https://love2d.org/) framework. - -Note: Since version [0375](https://github.com/rm-code/logivi/releases/tag/0375) LoGiVi uses version [0.10.0](https://love2d.org/wiki/0.10.0) of the LÖVE framework. +[![Version](https://img.shields.io/badge/Version-0432-blue.svg)](https://github.com/rm-code/logivi/releases/latest) [![LOVE](https://img.shields.io/badge/L%C3%96VE-0.10.1-EA316E.svg)](http://love2d.org/) [![License](http://img.shields.io/badge/Licence-MIT-brightgreen.svg)](LICENSE.md) -![Example Visualization](https://github.com/rm-code/logivi/wiki/media/logivi_0312.gif) +LoGiVi is a [Git](https://git-scm.com/)-respository visualisation tool inspired by [Gource](http://gource.io/) and __currently in development__. It was written from scratch using [Lua](http://www.lua.org/) and the [LÖVE](https://love2d.org/) framework. -# Instructions -When you run LoGiVi for the first time it will set up all necessary folders, an example git log and a config file in the save directory on your harddrive. - -The location of this save directory depends on the OS you are using: - -- ***OSX*** ```/Users/user/Library/Application Support/LOVE/rmcode_LoGiVi``` -- ***WINDOWS*** ```C:\Users\user\AppData\Roaming\LOVE``` or ```%appdata%\LOVE\``` -- ***LINUX*** ```~/.local/share/love/``` - -A dialog will pop up which allows you to view the save directory on your computer. - -## Generating git logs automatically -LoGiVi can generate git logs automatically when you specify a path to a git repository on your harddrive. Open the _settings.cfg_ file in the LoGiVi save directory and look for the _[repositories]_ section. Add the absolute path to the folder containing the git repository like this: - -``` -[repositories] -logivi = /Users/Robert/Coding/Lua/LÖVE/LoGiVi/ -``` -The name on the left side of the equals sign will be used as the project name to identify this repository so make sure you use unique names here. - -LoGiVi can also handle Windows paths: - -``` -[repositories] -logivi = C:\Users\rmcode\Documents\Coding Projects\LoGiVi\ -``` -After you have added the paths of your project to the config file, the log and info files will be created the next time you run LoGiVi (this may take a few seconds depending on how large the repositories are). - -## Generating git logs manually -If you don't want the logs to be generated automatically, or if you don't have git in your PATH, you can also generate the git logs manually. - -Open your terminal and type in the following command (replace the path with your own path leading to a git repository): - -```bash -git -C "Path/To/Your/Repository" log --reverse --numstat --pretty=format:"info: %an|%ae|%ct" --name-status --no-merges > log.txt -``` -This will create the file _log.txt_ in the folder you are currently in. Take this newly created file and drop it into a folder in the _logs_ subfolder in the LoGiVi save directory: - -``` -/Users/Robert/Library/Application Support/LOVE/rmcode_LoGiVi/logs/yourProject/log.txt -``` -LoGiVi will use the folder's name to identify the log so make it informative. - -# License - -The MIT License (MIT) - -Copyright (c) 2014 - 2015 Robert Machmer - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +![Example Visualization](https://cloud.githubusercontent.com/assets/11627131/13007242/29da1fd0-d18f-11e5-9615-96cf0e4c2b3d.gif) diff --git a/conf.lua b/conf.lua index 6dbdef4..2c86d4c 100644 --- a/conf.lua +++ b/conf.lua @@ -1,22 +1,26 @@ local PROJECT_TITLE = "LoGiVi"; -local PROJECT_VERSION = "0432"; +local PROJECT_VERSION = require( 'version' ); local PROJECT_IDENTITY = "rmcode_LoGiVi"; -local LOVE_VERSION = "0.10.0"; +local LOVE_VERSION = "0.10.1"; --- --- Initialise löve's config file. --- @param t +-- Initialise LÖVE's config file. +-- @param t (table) The table containing LÖVE's default values. -- -function love.conf(t) +function love.conf( t ) t.identity = PROJECT_IDENTITY; t.version = LOVE_VERSION; t.console = true; + t.accelerometerjoystick = true; + t.externalstorage = false; + t.gammacorrect = false; + t.window.title = PROJECT_TITLE; - t.window.icon = nil; + t.window.icon = 'res/img/icon/1024px.png'; t.window.width = 800; t.window.height = 600; t.window.borderless = false; @@ -26,10 +30,9 @@ function love.conf(t) t.window.fullscreen = false; t.window.fullscreentype = "exclusive"; t.window.vsync = true; - t.window.fsaa = 0; + t.window.msaa = 0; t.window.display = 1; t.window.highdpi = false; - t.window.srgb = false; t.window.x = nil; t.window.y = nil; @@ -45,11 +48,15 @@ function love.conf(t) t.modules.sound = true; t.modules.system = true; t.modules.timer = true; + t.modules.touch = true; + t.modules.video = true; t.modules.window = true; + t.modules.thread = true; end --- -- Returns the project's version. +-- @return (string) The project's version. -- function getVersion() if PROJECT_VERSION then @@ -59,6 +66,7 @@ end --- -- Returns the project's title. +-- @return (string) The project's title. -- function getTitle() if PROJECT_TITLE then diff --git a/lib/graphoon/CHANGELOG.md b/lib/graphoon/CHANGELOG.md new file mode 100644 index 0000000..d25930d --- /dev/null +++ b/lib/graphoon/CHANGELOG.md @@ -0,0 +1,5 @@ +## Version 1.0.1 ( 2016-01-12 ) +- Fix [#1](https://github.com/rm-code/Graphoon/issues/1) - Adjusted force calculation and hopefully made it more stable + +## Version 1.0.0 ( 2016-01-12 ) + - Initial Release diff --git a/lib/graphoon/Graphoon.lua b/lib/graphoon/Graphoon.lua new file mode 100644 index 0000000..7ad8dbe --- /dev/null +++ b/lib/graphoon/Graphoon.lua @@ -0,0 +1 @@ +return require( (...) .. '.init' ); diff --git a/lib/graphoon/Graphoon/Edge.lua b/lib/graphoon/Graphoon/Edge.lua new file mode 100644 index 0000000..08ed954 --- /dev/null +++ b/lib/graphoon/Graphoon/Edge.lua @@ -0,0 +1,15 @@ +local current = (...):gsub('%.[^%.]+$', ''); + +local Edge = {}; + +function Edge.new( id, origin, target ) + local self = {}; + + self.id = id; + self.origin = origin; + self.target = target; + + return self; +end + +return Edge; diff --git a/lib/graphoon/Graphoon/Graph.lua b/lib/graphoon/Graphoon/Graph.lua new file mode 100644 index 0000000..c9788e7 --- /dev/null +++ b/lib/graphoon/Graphoon/Graph.lua @@ -0,0 +1,258 @@ +local current = (...):gsub('%.[^%.]+$', ''); + +-- ------------------------------------------------ +-- Required Modules +-- ------------------------------------------------ + +local Node = require(current .. '.Node'); +local Edge = require(current .. '.Edge'); + +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ + +local Graph = {}; + +function Graph.new() + local self = {}; + + local nodes = {}; -- Contains all nodes in the graph. + local edges = {}; -- Contains all edges in the graph. + local edgeIDs = 0; -- Used to create a unique ID for new edges. + + local minX, maxX, minY, maxY; -- The boundaries of the graph. + + -- ------------------------------------------------ + -- Local Functions + -- ------------------------------------------------ + + --- + -- (Re-)Sets the graph's boundaries to nil. + -- + local function resetBoundaries() + minX, maxX, minY, maxY = nil, nil, nil, nil; + end + + --- + -- Updates the boundaries of the graph. + -- This represents the rectangular area in which all nodes are contained. + -- @param nx - The new x position to check. + -- @param ny - The new y position to check. + -- + local function updateBoundaries( nx, ny ) + return math.min( minX or nx, nx ), math.max( maxX or nx, nx ), math.min( minY or ny, ny ), math.max( maxY or ny, ny ); + end + + --- + -- Adds a new edge between two nodes. + -- @param origin - The node from which the edge originates. + -- @param target - The node to which the edge is pointing to. + -- + local function addEdge( origin, target ) + for _, edge in pairs( edges ) do + if edge.origin == origin and edge.target == target then + error "Trying to connect nodes which are already connected."; + end + end + + assert( origin ~= target, "Tried to connect a node with itself." ); + edges[edgeIDs] = Edge.new( edgeIDs, origin, target ); + edgeIDs = edgeIDs + 1; + end + + -- ------------------------------------------------ + -- Public Functions + -- ------------------------------------------------ + + --- + -- Adds a node to the graph. + -- @param id - The ID will be used to reference the Node inside of the graph. + -- @param x - The x coordinate the Node should be spawned at (optional). + -- @param y - The y coordinate the Node should be spawned at (optional). + -- @param anchor - Wether the node should be locked in place or not (optional). + -- @param ... - Additional parameters (useful when a custom Node class is used). + -- + function self:addNode( id, x, y, anchor, ... ) + assert( not nodes[id], "Node IDs must be unique." ); + nodes[id] = Node.new( id, x, y, anchor, ... ); + return nodes[id]; + end + + --- + -- Removes a node from the graph. + -- This will also remove all edges pointing to, or originating from this + -- node. + -- @param node - The node to remove from the graph. + -- + function self:removeNode( node ) + nodes[node:getID()] = nil; + + self:removeEdges( node ); + end + + --- + -- Adds a new edge between two nodes. + -- @param origin - The node from which the edge originates. + -- @param target - The node to which the edge is pointing to. + -- + function self:connectNodes( origin, target ) + addEdge( origin, target ); + end + + --- + -- Adds a new edge between two nodes referenced by their IDs. + -- @param origin - The node id from which the edge originates. + -- @param target - The node id to which the edge is pointing to. + -- + function self:connectIDs( originID, targetID ) + assert( nodes[originID], string.format( "Tried to add an Edge to the nonexistent Node \"%s\".", originID )); + assert( nodes[targetID], string.format( "Tried to add an Edge to the nonexistent Node \"%s\".", targetID )); + addEdge( nodes[originID], nodes[targetID] ); + end + + --- + -- Removes all edges leading to, or originating from a node. + -- @param node - The node to remove all edges from. + -- + function self:removeEdges( node ) + for id, edge in pairs( edges ) do + if edge.origin == node or edge.target == node then + edges[id] = nil; + end + end + end + + --- + -- Updates the graph. + -- @param dt - The delta time between frames. + -- @param nodeCallback - A callback called on every node (optional). + -- @param edgeCallback - A callback called on every edge (optional). + -- + function self:update( dt, nodeCallback, edgeCallback ) + for _, edge in pairs( edges ) do + edge.origin:attractTo( edge.target ); + edge.target:attractTo( edge.origin ); + + if edgeCallback then + edgeCallback( edge ); + end + end + + resetBoundaries(); + + for _, nodeA in pairs( nodes ) do + if not nodeA:isAnchor() then + for _, nodeB in pairs( nodes ) do + if nodeA ~= nodeB then + nodeA:repelFrom( nodeB ); + end + end + nodeA:move( dt ); + end + + if nodeCallback then + nodeCallback( nodeA ); + end + + minX, maxX, minY, maxY = updateBoundaries( nodeA:getPosition() ); + end + end + + --- + -- Draws the graph. + -- Takes two callback functions as a parameter. These will be called + -- on each edge and node in the graph and will be used to wite a custom + -- drawing function. + -- @param nodeCallback - A callback called on every node. + -- @param edgeCallback - A callback called on every edge. + -- + function self:draw( nodeCallback, edgeCallback ) + for _, edge in pairs( edges ) do + if not edgeCallback then break end + edgeCallback( edge ); + end + + for _, node in pairs( nodes ) do + if not nodeCallback then break end + nodeCallback( node ); + end + end + + --- + -- Checks if a certain Node ID already exists. + -- @param id - The id to check for. + -- + function self:hasNode( id ) + return nodes[id] ~= nil; + end + + --- + -- Returns the node the id is pointing to. + -- @param id - The id to check for. + -- + function self:getNode( id ) + return nodes[id]; + end + + --- + -- Gets a node at a certain point in the graph. + -- @param x - The x coordinate to check. + -- @param y - The y coordinate to check. + -- @param range - The range in which to check around the given coordinates. + -- + function self:getNodeAt(x, y, range) + for _, node in pairs( nodes ) do + local nx, ny = node:getPosition(); + if x < nx + range and x > nx - range and y < ny + range and y > ny - range then + return node; + end + end + end + + --- + -- Returns the graph's minimum and maxmimum x and y values. + -- + function self:getBoundaries() + return minX, maxX, minY, maxY; + end + + --- + -- Returns the x and y coordinates of the graph's center. + -- + function self:getCenter() + return ( ( maxX - minX ) * 0.5 ) + minX, ( ( maxY - minY ) * 0.5 ) + minY; + end + + --- + -- Turn a node into an anchor. + -- Anchored nodes have fixed positions and can't be moved by the physical + -- forces. + -- @param id - The node's id. + -- @param x - The x coordinate to anchor the node to. + -- @param y - The y coordinate to anchor the node to. + -- + function self:setAnchor( id, x, y ) + nodes[id]:setPosition( x, y ); + nodes[id]:setAnchor( true ); + end + + return self; +end + +--- +-- Replaces the default Edge class with a custom one. +-- @param class - The custom Edge class to use. +-- +function Graph.setEdgeClass( class ) + Edge = class; +end + +--- +-- Replaces the default Node class with a custom one. +-- @param class - The custom Node class to use. +-- +function Graph.setNodeClass( class ) + Node = class; +end + +return Graph; diff --git a/lib/graphoon/Graphoon/Node.lua b/lib/graphoon/Graphoon/Node.lua new file mode 100644 index 0000000..6180ec7 --- /dev/null +++ b/lib/graphoon/Graphoon/Node.lua @@ -0,0 +1,149 @@ +local current = (...):gsub('%.[^%.]+$', ''); + +local Node = {}; + +local FORCE_SPRING = 0.005; +local FORCE_CHARGE = 200; + +local FORCE_MAX = 4; +local NODE_SPEED = 128; +local DAMPING_FACTOR = 0.95; + +local DEFAULT_MASS = 3; + +--- +-- @param id - A unique id which will be used to reference this node. +-- @param x - The x coordinate the Node should be spawned at (optional). +-- @param y - The y coordinate the Node should be spawned at (optional). +-- @param anchor - Wether the node should be locked in place or not (optional). +-- +function Node.new( id, x, y, anchor ) + local self = {}; + + local px, py = x or 0, y or 0; + local ax, ay = 0, 0; + local vx, vy = 0, 0; + local mass = DEFAULT_MASS; + + --- + -- Clamps a value to a certain range. + -- @param min + -- @param val + -- @param max + -- + local function clamp( min, val, max ) + return math.max( min, math.min( val, max ) ); + end + + --- + -- Calculates the new xy-acceleration for this node. + -- The values are clamped to keep the graph from "exploding". + -- @param fx - The force to apply in x-direction. + -- @param fy - The force to apply in y-direction. + -- + local function applyForce( fx, fy ) + ax = clamp( -FORCE_MAX, ax + fx, FORCE_MAX ); + ay = clamp( -FORCE_MAX, ay + fy, FORCE_MAX ); + end + + --- + -- Calculates the manhattan distance from the node's coordinates to the + -- target coordinates. + -- @param tx - The target coordinate in x-direction. + -- @param ty - The target coordinate in y-direction. + -- + local function getManhattanDistance( tx, ty ) + return px - tx, py - ty; + end + + --- + -- Calculates the actual distance vector between the node's current + -- coordinates and the target coordinates based on the manhattan distance. + -- @param dx - The horizontal distance. + -- @param dy - The vertical distance. + -- + local function getRealDistance( dx, dy ) + return math.sqrt( dx * dx + dy * dy ) + 0.1; + end + + --- + -- Attract this node to another node. + -- @param node - The node to use for force calculation. + -- + function self:attractTo( node ) + local dx, dy = getManhattanDistance( node:getPosition() ); + local distance = getRealDistance( dx, dy ); + dx = dx / distance; + dy = dy / distance; + + local strength = -1 * FORCE_SPRING * distance * 0.5; + applyForce( dx * strength, dy * strength ); + end + + --- + -- Repel this node from another node. + -- @param node - The node to use for force calculation. + -- + function self:repelFrom( node ) + local dx, dy = getManhattanDistance( node:getPosition() ); + local distance = getRealDistance( dx, dy ); + dx = dx / distance; + dy = dy / distance; + + local strength = FORCE_CHARGE * (( mass * node:getMass() ) / ( distance * distance )); + applyForce(dx * strength, dy * strength); + end + + --- + -- Update the node's position based on the calculated velocity and + -- acceleration. + -- @param dt - The delta time between frames. + -- + function self:move( dt ) + vx = (vx + ax * dt * NODE_SPEED) * DAMPING_FACTOR; + vy = (vy + ay * dt * NODE_SPEED) * DAMPING_FACTOR; + px = px + vx; + py = py + vy; + ax, ay = 0, 0; + end + + function self:getID() + return id; + end + + function self:getX() + return px; + end + + function self:getY() + return py; + end + + function self:getPosition() + return px, py; + end + + function self:setPosition( nx, ny ) + px, py = nx, ny; + end + + function self:setAnchor( nanchor ) + anchor = nanchor; + end + + function self:isAnchor() + return anchor; + end + + function self:setMass( nmass ) + mass = nmass; + end + + function self:getMass() + return mass; + end + + return self; +end + +return Node; diff --git a/lib/graphoon/Graphoon/init.lua b/lib/graphoon/Graphoon/init.lua new file mode 100644 index 0000000..5b24261 --- /dev/null +++ b/lib/graphoon/Graphoon/init.lua @@ -0,0 +1,31 @@ +return { + _VERSION = 'Graphoon v1.0.1', + _DESCRIPTION = 'A force directed graph algorithm written in Lua.', + _URL = 'https://github.com/rm-code/Graphoon', + _LICENSE = [[ + Copyright (c) 2015 - 2016 Robert Machmer + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]], + + Edge = require( (...):gsub('%.init$', '') .. '.Edge' ), + Graph = require( (...):gsub('%.init$', '') .. '.Graph' ), + Node = require( (...):gsub('%.init$', '') .. '.Node' ) +}; diff --git a/lib/graphoon/LICENSE.md b/lib/graphoon/LICENSE.md new file mode 100644 index 0000000..2f9fe3c --- /dev/null +++ b/lib/graphoon/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2015 - 2016 Robert Machmer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/graphoon/README.md b/lib/graphoon/README.md new file mode 100644 index 0000000..d956de3 --- /dev/null +++ b/lib/graphoon/README.md @@ -0,0 +1,89 @@ +# Graphoon + +A force directed graph algorithm written in Lua. + +![example](https://cloud.githubusercontent.com/assets/11627131/12252902/a5190d90-b8db-11e5-9199-a9fdb61416ac.png) + +## Introduction + +_Graphoon_ emerged from the graph calculation code used in both [LoGiVi](https://github.com/rm-code/logivi) and [LoFiVi](https://github.com/rm-code/lofivi). + +A force directed graph layout is achieved by simulating physical forces, which push and pull each node in the graph until a nice layout is found. + +## Basic Usage + +The basic idea is that you create a new graph object, to which you can then add nodes and edges. + +```lua +local GraphLibrary = require('Graphoon').Graph + +graph = GraphLibrary.new() +graph:addNode( "Ash Williams" ) +graph:addNode( "Necronomicon" ) +graph:connectIDs( "Ash Williams", "Necronomicon" ) +``` + +By itself Graphoon only provides functionality for creating the graph and calculating the layout based on physical attraction and repulsion forces. + +It provides a ```draw``` and ```update``` function, which can be used to easily write your own rendering code. + +The ```draw``` function should be called with two callback functions. The first callback will be used for all nodes and the second one for all the edges. + +```lua +graph:draw( function( node ) + local x, y = node:getPosition() + drawCircle( 'fill', x, y, 10 ) + end, + function( edge ) + local ox, oy = edge.origin:getPosition() + local tx, ty = edge.target:getPosition() + drawLine( ox, oy, tx, ty ) + end) +``` + +At its simplest the force calculations can be updated via ```graph:update( dt )```, but the ```update``` function also can receive optional callbacks for both nodes and edges. + +## Advanced usage + +### Using anchors + +Anchors can be used to attach a node to a certain position on the screen. This can be useful if you want to center a certain node for example. + +This can either be done directly via the constructor of the node: + +```lua +-- Anchor the node to the center of the screen. +graph:addNode( "Ash Williams", screenX * 0.5, screenY * 0.5, true ) +``` + +Or by using the ```setAnchor``` function: + +```lua +-- Invert anchor status +node:setAnchor( not node:isAnchor(), mouseX, mouseY ) +``` + +### Using custom classes for Nodes and Edges + +If you prefer to not touch the default classes, you can simply inherit from them and tell Graphoon to use your custom classes instead. + +```lua +local GraphLibraryNode = require('lib.libfdgraph.fd').Node + +local CustomNodeClass = {} + +-- You can pass additional arguments to your custom class. Just make sure the +-- default parameters ar in the right order. +function CustomNodeClass.new( id, x, y, anchor, ... ) + local self = GraphLibraryNode.new( id, x, y, anchor ) + + -- ... Custom code +end + +return CustomNodeClass +``` + +```lua +local GraphLibrary = require('Graphoon').Graph +GraphLibrary.setNodeClass( require('CustomNodeClass') ) +``` diff --git a/lib/screenmanager/CHANGELOG.md b/lib/screenmanager/CHANGELOG.md new file mode 100644 index 0000000..bde7f0d --- /dev/null +++ b/lib/screenmanager/CHANGELOG.md @@ -0,0 +1,30 @@ +## Version 1.8.0 ( 2016-01-30 ) +- Add gamepad and joystick callbacks +- Add love.threaderror +- Add varargs to ScreenManager.init + +## Version 1.7.0 ( 2016-01-12 ) +- Add textedited callback +- Fix parameters for keypressed, keyreleased, mousepressed and mousereleased + +## Version 1.6.0 ( 2015-10-29 ) +- Add filedropped and directorydropped callbacks + +## Version 1.5.0 ( 2015-10-25 ) +- Add more callbacks + +## Version 1.4.1 ( 2015-04-14 ) +- Allow passing of varargs to new Screens + +## Version 1.3.1 ( 2015-03-24 ) +- Update library information + +## Version 1.3.0 ( 2015-02-20 ) +- Add mousemoved callback + +## Version 1.2.1 ( 2015-02-03 ) +- Add library information +- Remove redundant code + +## Version 1.2.0 ( 2015-01-18 ) + - Initial Release diff --git a/lib/screenmanager/LICENSE.md b/lib/screenmanager/LICENSE.md new file mode 100644 index 0000000..de2628f --- /dev/null +++ b/lib/screenmanager/LICENSE.md @@ -0,0 +1,17 @@ +Copyright (c) 2014 - 2016 Robert Machmer + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. diff --git a/lib/screenmanager/Readme.md b/lib/screenmanager/Readme.md index c0e760e..08af4ca 100644 --- a/lib/screenmanager/Readme.md +++ b/lib/screenmanager/Readme.md @@ -1,22 +1,25 @@ #ScreenManager +[![Version](https://img.shields.io/badge/Version-1.8.0-blue.svg)](https://github.com/rm-code/screenmanager/releases/latest) +[![LOVE](https://img.shields.io/badge/L%C3%96VE-0.10.0-EA316E.svg)](http://love2d.org/) +[![License](http://img.shields.io/badge/Licence-zlib-brightgreen.svg)](LICENSE.md) + The ScreenManager library is a state manager at heart which allows some nifty things, like stacking multiple screens on top of each other. It also offers hooks for most of LÖVE's callback functions. Based on the type of callback the calls are rerouted to either only the active screen (love.keypressed, love.quit, ...) or to all screens (love.resize, love.visible, ...). ## Example -This is a simple example of how the ScreenManager should be used (note: You will have to change the paths in the example to fit your setup). +This is a simple example of how the ScreenManager should be used (note: You will have to change the paths in the example to fit your setup). -``` -#!lua +```lua -- main.lua -local ScreenManager = require('lib/ScreenManager'); +local ScreenManager = require('lib.ScreenManager'); function love.load() local screens = { - main = require('src/screens/MainScreen'); + main = require('src.screens.MainScreen'); }; ScreenManager.init(screens, 'main'); @@ -32,11 +35,10 @@ end ``` Note how MainScreen inherits from Screen.lua. This isn't mandatory, but recommended since Screen.lua already has templates for most of the callback functions. -``` -#!lua +```lua -- MainScreen.lua -local Screen = require('lib/Screen'); +local Screen = require('lib.Screen'); local MainScreen = {}; @@ -59,15 +61,3 @@ end return MainScreen; ``` - -## License - -Copyright (c) 2014 - 2015 Robert Machmer - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. - 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. - 3. This notice may not be removed or altered from any source distribution. diff --git a/lib/screenmanager/Screen.lua b/lib/screenmanager/Screen.lua index f23e0b8..5737a74 100644 --- a/lib/screenmanager/Screen.lua +++ b/lib/screenmanager/Screen.lua @@ -1,6 +1,6 @@ --===============================================================================-- -- -- --- Copyright (c) 2014 - 2015 Robert Machmer -- +-- Copyright (c) 2014 - 2016 Robert Machmer -- -- -- -- This software is provided 'as-is', without any express or implied -- -- warranty. In no event will the authors be held liable for any damages -- @@ -39,53 +39,79 @@ function Screen.new() function self:close() end - function self:update(dt) end + function self:isActive() + return active; + end - function self:draw() end + function self:setActive( dactiv ) + active = dactiv; + end - function self:focus(dfocus) end + -- ------------------------------------------------ + -- Callback-stubs + -- ------------------------------------------------ - function self:directorydropped(path) end + function self:directorydropped() end - function self:filedropped(file) end + function self:draw() end - function self:resize(w, h) end + function self:filedropped() end - function self:visible(dvisible) end + function self:focus() end - function self:keypressed(key) end + function self:keypressed() end - function self:keyreleased(key) end + function self:keyreleased() end function self:lowmemory() end - function self:textinput(input) end + function self:mousefocus() end - function self:mousereleased(x, y, button) end + function self:mousemoved() end - function self:mousepressed(x, y, button) end + function self:mousepressed() end - function self:mousefocus(focus) end + function self:mousereleased() end - function self:mousemoved(x, y, dx, dy) end + function self:quit() end - function self:quit(dquit) end + function self:resize() end - function self:touchmoved(id, x, y, pressure) end + function self:textedited() end - function self:touchpressed(id, x, y, pressure) end + function self:textinput() end - function self:touchreleased(id, x, y, pressure) end + function self:threaderror() end - function self:wheelmoved(x, y) end + function self:touchmoved() end - function self:isActive() - return active; - end + function self:touchpressed() end - function self:setActive(dactiv) - active = dactiv; - end + function self:touchreleased() end + + function self:update() end + + function self:visible() end + + function self:wheelmoved() end + + function self:gamepadaxis() end + + function self:gamepadpressed() end + + function self:gamepadreleased() end + + function self:joystickadded() end + + function self:joystickaxis() end + + function self:joystickhat() end + + function self:joystickpressed() end + + function self:joystickreleased() end + + function self:joystickremoved() end return self; end diff --git a/lib/screenmanager/ScreenManager.lua b/lib/screenmanager/ScreenManager.lua index 47eda77..baf342e 100644 --- a/lib/screenmanager/ScreenManager.lua +++ b/lib/screenmanager/ScreenManager.lua @@ -1,6 +1,6 @@ --===============================================================================-- -- -- --- Copyright (c) 2014 - 2015 Robert Machmer -- +-- Copyright (c) 2014 - 2016 Robert Machmer -- -- -- -- This software is provided 'as-is', without any express or implied -- -- warranty. In no event will the authors be held liable for any damages -- @@ -21,7 +21,7 @@ --===============================================================================-- local ScreenManager = { - _VERSION = '1.6.0', + _VERSION = '1.8.0', _DESCRIPTION = 'Screen/State Management for the LÖVE framework', _URL = 'https://github.com/rm-code/screenmanager/', }; @@ -52,39 +52,46 @@ end -- ------------------------------------------------ --- --- Initialise the ScreenManager. This pushes the first --- screen to the stack. --- @param nscreens - The list of possible screens. --- @param screen - The first screen to push to the stack. +-- Initialise the ScreenManager. +-- Sets up the ScreenManager and pushes the first screen. +-- @param nscreens (table) A table containing pointers to the different screen +-- classes. The keys will are used to call a specific +-- screen. +-- @param screen (string) The key of the first screen to push to the stack. +-- Use the key under which the screen in question is +-- stored in the nscreens table. +-- @param ... (vararg) One or multiple arguments passed to the new screen. -- -function ScreenManager.init(nscreens, screen) +function ScreenManager.init( nscreens, screen, ... ) stack = {}; screens = nscreens; - ScreenManager.push(screen); + ScreenManager.push( screen, ... ); end --- --- Clears the ScreenManager, creates a new screen and switches --- to it. Use this if you don't want to stack onto other screens. +-- Switches to a screen. +-- Removes all screens from the stack, creates a new screen and switches to it. +-- Use this if you don't want to stack onto other screens. +-- @param screen (string) The key of the screen to switch to. +-- @param ... (vararg) One or multiple arguments passed to the new screen. -- --- @param nscreen --- -function ScreenManager.switch(screen, ...) +function ScreenManager.switch( screen, ... ) clear(); - ScreenManager.push(screen, ...); + ScreenManager.push( screen, ... ); end --- --- Creates a new screen and pushes it on the screen stack, where --- it will overlay all the other screens. --- Screens below the this new screen will be set inactive. --- --- @param screen - The name of the screen to push on the stack. +-- Pushes a new screen to the stack. +-- Creates a new screen and pushes it on the screen stack, where it will overlay +-- the other screens below it. Screens below the this new screen will be set +-- inactive. +-- @param screen (string) The key of the screen to push to the stack. +-- @param ... (vararg) One or multiple arguments passed to the new screen. -- -function ScreenManager.push(screen, ...) +function ScreenManager.push( screen, ... ) -- Deactivate the previous screen if there is one. if ScreenManager.peek() then - ScreenManager.peek():setActive(false); + ScreenManager.peek():setActive( false ); end -- Push the new screen onto the stack. @@ -92,7 +99,7 @@ function ScreenManager.push(screen, ...) stack[#stack + 1] = screens[screen].new(); else local str = "{"; - for i, v in pairs(screens) do + for i, _ in pairs( screens ) do str = str .. i .. ', '; end str = str .. "}"; @@ -100,18 +107,20 @@ function ScreenManager.push(screen, ...) end -- Create the new screen and initialise it. - stack[#stack]:init(...); + stack[#stack]:init( ... ); end --- -- Returns the screen on top of the screen stack without removing it. +-- @return (table) The screen on top of the stack. -- function ScreenManager.peek() return stack[#stack]; end --- --- Removes the topmost screen of the stack. +-- Removes and returns the topmost screen of the stack. +-- @return (table) The screen on top of the stack. -- function ScreenManager.pop() if #stack > 1 then @@ -125,15 +134,31 @@ function ScreenManager.pop() tmp:close(); -- Activate next screen on the stack. - ScreenManager.peek():setActive(true); + ScreenManager.peek():setActive( true ); else error("Can't close the last screen. Use switch() to clear the screen manager and add a new screen."); end end +-- ------------------------------------------------ +-- LOVE Callbacks +-- ------------------------------------------------ + +--- +-- Reroutes the directorydropped callback to the currently active screen. +-- @param path (string) The full platform-dependent path to the directory. +-- It can be used as an argument to love.filesystem.mount, +-- in order to gain read access to the directory with +-- love.filesystem. +-- +function ScreenManager.directorydropped( path ) + ScreenManager.peek():directorydropped( path ); +end + --- --- Draw all screens on the stack. Screens that are higher on the stack --- will overlay screens that are on the bottom. +-- Reroutes the draw callback to all screens on the stack. +-- Screens that are higher on the stack will overlay screens that are below +-- them. -- function ScreenManager.draw() for i = 1, #stack do @@ -142,179 +167,311 @@ function ScreenManager.draw() end --- --- Update all screens on the stack. +-- Reroutes the filedropped callback to the currently active screen. +-- @param file (File) The unopened File object representing the file that was +-- dropped. -- -function ScreenManager.update(dt) - for i = 1, #stack do - stack[i]:update(dt); - end +function ScreenManager.filedropped( file ) + ScreenManager.peek():filedropped( file ); end --- --- Resize all screens on the stack. --- @param w --- @param h +-- Reroutes the focus callback to all screens on the stack. +-- @param focus (boolean) True if the window gains focus, false if it loses focus. -- -function ScreenManager.resize(w, h) +function ScreenManager.focus( focus ) for i = 1, #stack do - stack[i]:resize(w, h); + stack[i]:focus( focus ); end end --- --- Callback function triggered when a directory is dragged and dropped onto the window. --- @param file - The full platform-dependent path to the directory. +-- Reroutes the keypressed callback to the currently active screen. +-- @param key (KeyConstant) Character of the pressed key. +-- @param scancode (Scancode) The scancode representing the pressed key. +-- @param isrepeat (boolean) Whether this keypress event is a repeat. The +-- delay between key repeats depends on the +-- user's system settings. +-- +function ScreenManager.keypressed( key, scancode, isrepeat ) + ScreenManager.peek():keypressed( key, scancode, isrepeat ); +end + +--- +-- Reroutes the keyreleased callback to the currently active screen. +-- @param key (KeyConstant) Character of the released key. +-- @param scancode (Scancode) The scancode representing the released key. +-- +function ScreenManager.keyreleased( key, scancode ) + ScreenManager.peek():keyreleased( key, scancode ); +end + +--- +-- Reroutes the lowmemory callback to the currently active screen. +-- mobile devices. +-- +function ScreenManager.lowmemory() + ScreenManager.peek():lowmemory(); +end + +--- +-- Reroutes the mousefocus callback to the currently active screen. +-- @param focus (boolean) Wether the window has mouse focus or not. +-- +function ScreenManager.mousefocus( focus ) + ScreenManager.peek():mousefocus( focus ); +end + +--- +-- Reroutes the mousemoved callback to the currently active screen. +-- @param x (number) Mouse x position. +-- @param y (number) Mouse y position. +-- @param dx (number) The amount moved along the x-axis since the last time +-- love.mousemoved was called. +-- @param dy (number) The amount moved along the y-axis since the last time +-- love.mousemoved was called. -- -function ScreenManager.directorydropped(path) - ScreenManager.peek():directorydropped(path); +function ScreenManager.mousemoved( x, y, dx, dy ) + ScreenManager.peek():mousemoved( x, y, dx, dy ); end --- --- Callback function triggered when a file is dragged and dropped onto the window. --- @param file - The unopened File object representing the file that was dropped. +-- Reroutes the mousepressed callback to the currently active screen. +-- @param x (number) Mouse x position, in pixels. +-- @param y (number) Mouse y position, in pixels. +-- @param button (number) The button index that was pressed. 1 is the primary +-- mouse button, 2 is the secondary mouse button and 3 +-- is the middle button. Further buttons are mouse +-- dependent. +-- @param istouch (boolean) True if the mouse button press originated from a +-- touchscreen touch-press. -- -function ScreenManager.filedropped(file) - ScreenManager.peek():filedropped(file); +function ScreenManager.mousepressed( x, y, button, istouch ) + ScreenManager.peek():mousepressed( x, y, button, istouch ); end --- --- Update all screens on the stack whenever the game window gains or --- loses focus. --- @param nfocus +-- Reroutes the mousereleased callback to the currently active screen. +-- @param x (number) Mouse x position, in pixels. +-- @param y (number) Mouse y position, in pixels. +-- @param button (number) The button index that was released. 1 is the primary +-- mouse button, 2 is the secondary mouse button and 3 +-- is the middle button. Further buttons are mouse +-- dependent. +-- @param istouch (boolean) True if the mouse button release originated from a +-- touchscreen touch-release. -- -function ScreenManager.focus(nfocus) +function ScreenManager.mousereleased( x, y, button, istouch ) + ScreenManager.peek():mousereleased( x, y, button, istouch ); +end + +--- +-- Reroutes the quit callback to the currently active screen. +-- @return quit (boolean) Abort quitting. If true, do not close the game. +-- +function ScreenManager.quit() + ScreenManager.peek():quit(); +end + +--- +-- Reroutes the resize callback to all screens on the stack. +-- @param w (number) The new width, in pixels. +-- @param h (number) The new height, in pixels. +-- +function ScreenManager.resize( w, h ) for i = 1, #stack do - stack[i]:focus(nfocus); + stack[i]:resize( w, h ); end end --- --- Update all screens on the stack whenever the game window is minimized. --- @param nvisible +-- Reroutes the textedited callback to the currently active screen. +-- @param text (string) The UTF-8 encoded unicode candidate text. +-- @param start (number) The start cursor of the selected candidate text. +-- @param length (number) The length of the selected candidate text. May be 0. +-- +function ScreenManager.textedited( text, start, length ) + ScreenManager.peek():textedited( text, start, length ); +end + +--- +-- Reroutes the textinput callback to the currently active screen. +-- @param input (string) The UTF-8 encoded unicode text. +-- +function ScreenManager.textinput( input ) + ScreenManager.peek():textinput( input ); +end + +--- +-- Reroutes the threaderror callback to all screens. +-- @param thread (Thread) The thread which produced the error. +-- @param errorstr (string) The error message. -- -function ScreenManager.visible(nvisible) +function ScreenManager.threaderror( thread, errorstr ) for i = 1, #stack do - stack[i]:visible(nvisible); + stack[i]:threaderror( thread, errorstr ); end end + --- --- Reroutes the keypressed callback to the currently active screen. --- @param key +-- Reroutes the touchmoved callback to the currently active screen. +-- @param id (light userdata) The identifier for the touch press. +-- @param x (number) The x-axis position of the touch press inside the +-- window, in pixels. +-- @param y (number) The y-axis position of the touch press inside the +-- window, in pixels. +-- @param dx (number) The x-axis movement of the touch inside the +-- window, in pixels. +-- @param dy (number) The y-axis movement of the touch inside the +-- window, in pixels. +-- @param pressure (number) The amount of pressure being applied. Most +-- touch screens aren't pressure sensitive, +-- in which case the pressure will be 1. -- -function ScreenManager.keypressed(key) - ScreenManager.peek():keypressed(key); +function ScreenManager.touchmoved( id, x, y, dx, dy, pressure ) + ScreenManager.peek():touchmoved( id, x, y, dx, dy, pressure ); end --- --- Reroutes the keyreleased callback to the currently active screen. --- @param key +-- Reroutes the touchpressed callback to the currently active screen. +-- @param id (light userdata) The identifier for the touch press. +-- @param x (number) The x-axis position of the touch press inside the +-- window, in pixels. +-- @param y (number) The y-axis position of the touch press inside the +-- window, in pixels. +-- @param dx (number) The x-axis movement of the touch inside the +-- window, in pixels. +-- @param dy (number) The y-axis movement of the touch inside the +-- window, in pixels. +-- @param pressure (number) The amount of pressure being applied. Most +-- touch screens aren't pressure sensitive, +-- in which case the pressure will be 1. -- -function ScreenManager.keyreleased(key) - ScreenManager.peek():keyreleased(key); +function ScreenManager.touchpressed( id, x, y, dx, dy, pressure ) + ScreenManager.peek():touchpressed( id, x, y, dx, dy, pressure ); end --- --- Callback function triggered when the system is running out of memory on mobile devices. +-- Reroutes the touchreleased callback to the currently active screen. +-- @param id (light userdata) The identifier for the touch press. +-- @param x (number) The x-axis position of the touch press inside the +-- window, in pixels. +-- @param y (number) The y-axis position of the touch press inside the +-- window, in pixels. +-- @param dx (number) The x-axis movement of the touch inside the +-- window, in pixels. +-- @param dy (number) The y-axis movement of the touch inside the +-- window, in pixels. +-- @param pressure (number) The amount of pressure being applied. Most +-- touch screens aren't pressure sensitive, +-- in which case the pressure will be 1. -- -function ScreenManager.lowmemory() - ScreenManager.peek():lowmemory(); +function ScreenManager.touchreleased( id, x, y, dx, dy, pressure ) + ScreenManager.peek():touchreleased( id, x, y, dx, dy, pressure ); end --- --- Reroute the textinput callback to the currently active screen. --- @param input +-- Reroutes the update callback to all screens. +-- @param dt (number) Time since the last update in seconds. -- -function ScreenManager.textinput(input) - ScreenManager.peek():textinput(input); +function ScreenManager.update( dt ) + for i = 1, #stack do + stack[i]:update( dt ); + end end --- --- Reroute the mousepressed callback to the currently active screen. --- @param x --- @param y --- @param button +-- Reroutes the visible callback to all screens. +-- @param visible (boolean) True if the window is visible, false if it isn't. -- -function ScreenManager.mousepressed(x, y, button) - ScreenManager.peek():mousepressed(x, y, button); +function ScreenManager.visible( visible ) + for i = 1, #stack do + stack[i]:visible( visible ); + end end --- --- Reroute the mousereleased callback to the currently active screen. --- @param x --- @param y --- @param button +-- Reroutes the wheelmoved callback to the currently active screen. +-- @param x (number) Amount of horizontal mouse wheel movement. Positive values +-- indicate movement to the right. +-- @param y (number) Amount of vertical mouse wheel movement. Positive values +-- indicate upward movement. -- -function ScreenManager.mousereleased(x, y, button) - ScreenManager.peek():mousereleased(x, y, button); +function ScreenManager.wheelmoved( x, y ) + ScreenManager.peek():wheelmoved( x, y ); end --- --- Reroute the mousefocus callback to the currently active screen. --- @param button +-- Reroutes the gamepadaxis callback to the currently active screen. +-- @param joystick (Joystick) The joystick object. +-- @param axis (GamepadAxis) The joystick object. +-- @param value (number) The new axis value. -- -function ScreenManager.mousefocus(focus) - ScreenManager.peek():mousefocus(focus); +function ScreenManager.gamepadaxis( joystick, axis, value ) + ScreenManager.peek():gamepadaxis( joystick, axis, value ); end --- --- Callback function triggered when the mouse is moved. --- @param x - Mouse x position. --- @param y - Mouse y position. --- @param dx - The amount moved along the x-axis since the last time love.mousemoved was called. --- @param dy - The amount moved along the y-axis since the last time love.mousemoved was called. +-- Reroutes the gamepadpressed callback to the currently active screen. +-- @param joystick (Joystick) The joystick object. +-- @param button (GamepadButton) The virtual gamepad button. -- -function ScreenManager.mousemoved(x, y, dx, dy) - ScreenManager.peek():mousemoved(x, y, dx, dy); +function ScreenManager.gamepadpressed( joystick, button ) + ScreenManager.peek():gamepadpressed( joystick, button ); end --- --- Reroute the quit callback to the currently active screen. --- @param dquit +-- Reroutes the gamepadreleased callback to the currently active screen. +-- @param joystick (Joystick) The joystick object. +-- @param button (GamepadButton) The virtual gamepad button. -- -function ScreenManager.quit(dquit) - ScreenManager.peek():quit(dquit); +function ScreenManager.gamepadreleased( joystick, button ) + ScreenManager.peek():gamepadreleased( joystick, button ); end --- --- Callback function triggered when a touch press moves inside the touch screen. --- @param id - The identifier for the touch press. --- @param x - The x-axis position of the touch press inside the window, in pixels. --- @param y - The y-axis position of the touch press inside the window, in pixels. --- @param pressure - The amount of pressure being applied. Most touch screens aren't pressure sensitive, in which case the pressure will be 1. +-- Reroutes the joystickadded callback to the currently active screen. +-- @param joystick (Joystick) The newly connected Joystick object. -- -function ScreenManager.touchmoved(id, x, y, pressure) - ScreenManager.peek():touchmoved(id, x, y, pressure); +function ScreenManager.joystickadded( joystick ) + ScreenManager.peek():joystickadded( joystick ); end --- --- Callback function triggered when the touch screen is touched. --- @param id - The identifier for the touch press. --- @param x - The x-axis position of the touch press inside the window, in pixels. --- @param y - The y-axis position of the touch press inside the window, in pixels. --- @param pressure - The amount of pressure being applied. Most touch screens aren't pressure sensitive, in which case the pressure will be 1. +-- Reroutes the joystickhat callback to the currently active screen. +-- @param joystick (Joystick) The newly connected Joystick object. +-- @param hat (number) The hat number. +-- @param direction (JoystickHat) The new hat direction. -- -function ScreenManager.touchpressed(id, x, y, pressure) - ScreenManager.peek():touchpressed(id, x, y, pressure); +function ScreenManager.joystickhat( joystick, hat, direction ) + ScreenManager.peek():joystickhat( joystick, hat, direction ); end --- --- Callback function triggered when the touch screen stops being touched. --- @param id - The identifier for the touch press. --- @param x - The x-axis position of the touch press inside the window, in pixels. --- @param y - The y-axis position of the touch press inside the window, in pixels. --- @param pressure - The amount of pressure being applied. Most touch screens aren't pressure sensitive, in which case the pressure will be 1. +-- Reroutes the joystickpressed callback to the currently active screen. +-- @param joystick (Joystick) The newly connected Joystick object. +-- @param button (number) The button number. -- -function ScreenManager.touchreleased(id, x, y, pressure) - ScreenManager.peek():touchreleased(id, x, y, pressure); +function ScreenManager.joystickpressed( joystick, button ) + ScreenManager.peek():joystickpressed( joystick, button ); end --- --- Callback function triggered when the mouse wheel is moved. --- @param x - Amount of horizontal mouse wheel movement. Positive values indicate movement to the right. --- @param y - Amount of vertical mouse wheel movement. Positive values indicate upward movement. -function ScreenManager.wheelmoved(x, y) - ScreenManager.peek():wheelmoved(x, y); +-- Reroutes the joystickreleased callback to the currently active screen. +-- @param joystick (Joystick) The newly connected Joystick object. +-- @param button (number) The button number. +-- +function ScreenManager.joystickreleased( joystick, button ) + ScreenManager.peek():joystickreleased( joystick, button ); +end + +--- +-- Reroutes the joystickremoved callback to the currently active screen. +-- @param joystick (Joystick) The now-disconnected Joystick object. +-- +function ScreenManager.joystickremoved( joystick ) + ScreenManager.peek():joystickremoved( joystick ); end -- ------------------------------------------------ diff --git a/lib/serpent/LICENSE b/lib/serpent/LICENSE new file mode 100644 index 0000000..36ef758 --- /dev/null +++ b/lib/serpent/LICENSE @@ -0,0 +1,21 @@ +Serpent source is released under the MIT License + +Copyright (c) 2011-2013 Paul Kulchenko (paul@kulchenko.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/serpent/Serpent.lua b/lib/serpent/Serpent.lua new file mode 100644 index 0000000..ca52392 --- /dev/null +++ b/lib/serpent/Serpent.lua @@ -0,0 +1,129 @@ +local n, v = "serpent", 0.285 -- (C) 2012-15 Paul Kulchenko; MIT License +local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" +local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} +local badtype = {thread = true, userdata = true, cdata = true} +local getmetatable = debug and debug.getmetatable or getmetatable +local keyword, globals, G = {}, {}, (_G or _ENV) +for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', + 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', + 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end +for k,v in pairs(G) do globals[v] = k end -- build func to name mapping +for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do + for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end + +local function s(t, opts) + local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum + local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge + local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) + local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) + local numformat = opts.numformat or "%.17g" + local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 + local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", + -- tostring(val) is needed because __tostring may return a non-string value + function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end + local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s)) + or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 + or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end + local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end + local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal + and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end + local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] + local n = name == nil and '' or name + local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] + local safe = plain and n or '['..safestr(n)..']' + return (path or '')..(plain and path and '.' or '')..safe, safe end + local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding + local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} + local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end + table.sort(k, function(a,b) + -- sort numeric keys first: k[key] is not nil for numerical keys + return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) + < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end + local function val2str(t, name, indent, insref, path, plainindex, level) + local ttype, level, mt = type(t), (level or 0), getmetatable(t) + local spath, sname = safename(path, name) + local tag = plainindex and + ((type(name) == "number") and '' or name..space..'='..space) or + (name ~= nil and sname..space..'='..space or '') + if seen[t] then -- already seen this element + sref[#sref+1] = spath..space..'='..space..seen[t] + return tag..'nil'..comment('ref', level) end + -- protect from those cases where __tostring may fail + if type(mt) == 'table' and pcall(function() return mt.__tostring and mt.__tostring(t) end) + and (mt.__serialize or mt.__tostring) then -- knows how to serialize itself + seen[t] = insref or spath + if mt.__serialize then t = mt.__serialize(t) else t = tostring(t) end + ttype = type(t) end -- new value falls through to be serialized + if ttype == "table" then + if level >= maxl then return tag..'{}'..comment('max', level) end + seen[t] = insref or spath + if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty + local maxn, o, out = math.min(#t, maxnum or #t), {}, {} + for key = 1, maxn do o[key] = key end + if not maxnum or #o < maxnum then + local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables + for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end + if maxnum and #o > maxnum then o[maxnum+1] = nil end + if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end + local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) + for n, key in ipairs(o) do + local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse + if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing + or opts.keyallow and not opts.keyallow[key] + or opts.keyignore and opts.keyignore[key] + or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types + or sparse and value == nil then -- skipping nils; do nothing + elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then + if not seen[key] and not globals[key] then + sref[#sref+1] = 'placeholder' + local sname = safename(iname, gensym(key)) -- iname is table for local variables + sref[#sref] = val2str(key,sname,indent,sname,iname,true) end + sref[#sref+1] = 'placeholder' + local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']' + sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path)) + else + out[#out+1] = val2str(value,key,indent,insref,seen[t],plainindex,level+1) + end + end + local prefix = string.rep(indent or '', level) + local head = indent and '{\n'..prefix..indent or '{' + local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) + local tail = indent and "\n"..prefix..'}' or '}' + return (custom and custom(tag,head,body,tail) or tag..head..body..tail)..comment(t, level) + elseif badtype[ttype] then + seen[t] = insref or spath + return tag..globerr(t, level) + elseif ttype == 'function' then + seen[t] = insref or spath + local ok, res = pcall(string.dump, t) + local func = ok and ((opts.nocode and "function() --[[..skipped..]] end" or + "((loadstring or load)("..safestr(res)..",'@serialized'))")..comment(t, level)) + return tag..(func or globerr(t, level)) + else return tag..safestr(t) end -- handle all other types + end + local sepr = indent and "\n" or ";"..space + local body = val2str(t, name, indent) -- this call also populates sref + local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' + local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' + return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" +end + +local function deserialize(data, opts) + local env = (opts and opts.safe == false) and G + or setmetatable({}, { + __index = function(t,k) return t end, + __call = function(t,...) error("cannot call functions") end + }) + local f, res = (loadstring or load)('return '..data, nil, nil, env) + if not f then f, res = (loadstring or load)(data, nil, nil, env) end + if not f then return f, res end + if setfenv then setfenv(f, env) end + return pcall(f) +end + +local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end +return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, + load = deserialize, + dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, + line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, + block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } diff --git a/main.lua b/main.lua index 7180f1f..5af086c 100644 --- a/main.lua +++ b/main.lua @@ -1,119 +1,145 @@ -local ScreenManager = require('lib.screenmanager.ScreenManager'); - --- ------------------------------------------------ --- Local Variables --- ------------------------------------------------ - -local showDebug = false; - --- ------------------------------------------------ --- Local Functions --- ------------------------------------------------ +local ScreenManager = require( 'lib.screenmanager.ScreenManager' ); --- --- Check if the hardware supports certain features. +-- This function is called exactly once at the beginning of the game. -- -local function checkSupport() - print("\n---- RENDERER ---- "); - local name, version, vendor, device = love.graphics.getRendererInfo(); - print(string.format("Name: %s \nVersion: %s \nVendor: %s \nDevice: %s", name, version, vendor, device)); - - print("\n---- SYSTEM ---- "); - print(love.system.getOS()); -end - -local function drawStats() - local h = love.graphics.getHeight(); - love.graphics.setColor(100, 100, 100, 255); - love.graphics.rectangle('fill', 5, h - 185, 200, 200); - love.graphics.setColor(255, 255, 255, 255); - love.graphics.print(string.format("FT: %.3f ms", 1000 * love.timer.getAverageDelta()), 10, h - 180); - love.graphics.print(string.format("FPS: %.3f fps", love.timer.getFPS()), 10, h - 160); - love.graphics.print(string.format("MEM: %.3f kb", collectgarbage("count")), 10, h - 140); - - local stats = love.graphics.getStats(); - love.graphics.print(string.format("Drawcalls: %d", stats.drawcalls), 10, h - 120); - love.graphics.print(string.format("Canvas Switches: %d", stats.canvasswitches), 10, h - 100); - love.graphics.print(string.format("TextureMemory: %.2f kb", stats.texturememory / 1024), 10, h - 80); - love.graphics.print(string.format("Images: %d", stats.images), 10, h - 60); - love.graphics.print(string.format("Canvases: %d", stats.canvases), 10, h - 40); - love.graphics.print(string.format("Fonts: %d", stats.fonts), 10, h - 20); -end - --- ------------------------------------------------ --- Callbacks --- ------------------------------------------------ - function love.load() - print("===================") - print(string.format("Title: '%s'", getTitle())); - print(string.format("Version: %.4d", getVersion())); - print(string.format("LOVE Version: %d.%d.%d (%s)", love.getVersion())); - print(string.format("Resolution: %dx%d", love.graphics.getDimensions())); + print( "===================" ); + print( string.format( "Title: '%s'", getTitle() )); + print( string.format( "Version: %s", getVersion() )); + print( string.format( "LOVE Version: %d.%d.%d (%s)", love.getVersion() )); + print( string.format( "Resolution: %dx%d", love.graphics.getDimensions() )); -- Check the user's hardware. - checkSupport(); - print("===================") - print(os.date('%c', os.time())); - print("===================") + print( "\n---- RENDERER ---- " ); + local name, version, vendor, device = love.graphics.getRendererInfo(); + print( string.format( "Name: %s \nVersion: %s \nVendor: %s \nDevice: %s", name, version, vendor, device )); + + print( "\n---- SYSTEM ---- " ); + print( love.system.getOS() ); + print( "===================" ); + print( os.date( '%c', os.time() )); + print( "===================" ); local screens = { - selection = require('src.screens.SelectionScreen'); - main = require('src.screens.MainScreen'); + loading = require( 'src.screens.LoadingScreen' ), + selection = require( 'src.screens.SelectionScreen' ), + main = require( 'src.screens.MainScreen' ), + input = require( 'src.screens.InputPanel' ) }; - ScreenManager.init(screens, 'selection'); + ScreenManager.init( screens, 'loading' ); end +--- +-- Callback function used to draw on the screen every frame. +-- function love.draw() ScreenManager.draw(); - - if showDebug then - drawStats(); - end end -function love.update(dt) - ScreenManager.update(dt); +--- +-- Callback function used to update the state of the game every frame. +-- @param dt (number) Time since the last update in seconds. +-- +function love.update( dt ) + ScreenManager.update( dt ); end -function love.quit(q) - ScreenManager.quit(q); +--- +-- Callback function triggered when the game is closed. +-- +function love.quit() + ScreenManager.quit(); end -function love.resize(x, y) - ScreenManager.resize(x, y); +--- +-- Called when the window is resized, for example if the user resizes the window, or if +-- love.window.setMode is called with an unsupported width or height in fullscreen and +-- the window chooses the closest appropriate size. +-- @param x (number) The new width, in pixels. +-- @param y (number) The new height, in pixels. +-- +function love.resize( x, y ) + ScreenManager.resize( x, y ); end -function love.keypressed(key) +--- +-- Callback function triggered when a key is pressed. +-- @param key (KeyConstant) Character of the pressed key. +-- @param scancode (Scancode) The scancode representing the pressed key. +-- @param isrepeat (boolean) Whether this keypress event is a repeat. The delay between +-- key repeats depends on the user's system settings. +-- +function love.keypressed( key, scancode, isrepeat ) -- Transform strings to numbers to fit the control values we read from the config file. - if tonumber(key) then - key = tonumber(key); + if tonumber( key ) then + key = tonumber( key ); end - if key == 'f1' then - showDebug = not showDebug; - end + ScreenManager.keypressed( key, scancode, isrepeat ); +end - ScreenManager.keypressed(key); +--- +-- Callback function triggered when a mouse button is pressed. +-- @param x (number) Mouse x position, in pixels. +-- @param y (number) Mouse y position, in pixels. +-- @param button (number) The button index that was pressed. 1 is the primary mouse button, +-- 2 is the secondary mouse button and 3 is the middle button. Further +-- buttons are mouse dependent. +-- @param istouch (boolean) True if the mouse button press originated from a touchscreen touch-press. +-- +function love.mousepressed( x, y, button, istouch ) + ScreenManager.mousepressed( x, y, button, istouch ); end -function love.mousepressed(x, y, b) - ScreenManager.mousepressed(x, y, b); +--- +-- Callback function triggered when a mouse button is released. +-- @param x (number) Mouse x position, in pixels. +-- @param y (number) Mouse y position, in pixels. +-- @param button (number) The button index that was pressed. 1 is the primary mouse button, +-- 2 is the secondary mouse button and 3 is the middle button. Further +-- buttons are mouse dependent. +-- @param istouch (boolean) True if the mouse button press originated from a touchscreen touch-release. +-- +function love.mousereleased( x, y, button, istouch ) + ScreenManager.mousereleased( x, y, button, istouch ); end -function love.mousereleased(x, y, b) - ScreenManager.mousereleased(x, y, b); +--- +-- Callback function triggered when the mouse is moved. +-- @param x (number) The mouse position on the x-axis. +-- @param y (number) The mouse position on the y-axis. +-- @param dx (number) The amount moved along the x-axis since the last time love.mousemoved was called. +-- @param dy (number) The amount moved along the y-axis since the last time love.mousemoved was called. +-- +function love.mousemoved( x, y, dx, dy ) + ScreenManager.mousemoved( x, y, dx, dy ); end -function love.mousemoved(x, y, dx, dy) - ScreenManager.mousemoved(x, y, dx, dy); +--- +-- Callback function triggered when the mouse wheel is moved. +-- @param x (number) Amount of horizontal mouse wheel movement. Positive values indicate movement to the right. +-- @param y (number) Amount of vertical mouse wheel movement. Positive values indicate upward movement. +-- +function love.wheelmoved( x, y ) + ScreenManager.wheelmoved( x, y ); end -function love.wheelmoved(x, y) - ScreenManager.wheelmoved(x, y); +--- +-- Callback function triggered when a directory is dragged and dropped onto the window. +-- @param path (string) The full platform-dependent path to the directory. It can be used as an argument to +-- love.filesystem.mount, in order to gain read access to the directory with love.filesystem. +-- +function love.directorydropped( path ) + ScreenManager.directorydropped( path ); end -function love.directorydropped(path) - ScreenManager.directorydropped(path); +--- +-- Called when text has been entered by the user. For example if shift-2 is pressed on an American keyboard +-- layout, the text "@" will be generated. +-- @param text (string) The UTF-8 encoded unicode text. +-- +function love.textinput( text ) + ScreenManager.textinput( text ); end diff --git a/prepare-release.sh b/prepare-release.sh new file mode 100644 index 0000000..481c2a5 --- /dev/null +++ b/prepare-release.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Stop if no version flag is provided. +if ! [[ $1 == "major" || $1 == "minor" || $1 == "patch" ]] ; then + echo "FAILED: Use major, minor or patch to release a new version." + exit 1 +fi + +# Get the version numbers from the lua file and store them in an array. +i=0 +while read line ; do + no=${line//[!0-9]/} + if [ ! -z "$no" ]; then + version[$i]=${line//[!0-9]/} + i=$((i+1)) + fi +done < version.lua + +# Assign to variables. +major=${version[0]} +minor=${version[1]} +patch=${version[2]} +build=${version[3]} + +echo "Old Version: $major.$minor.$patch.$build" + +# Increment version based on command. +if [ $1 == "major" ] ; then + major=$((major+1)) + minor=0 + patch=0 +elif [[ $1 == "minor" ]]; then + minor=$((minor+1)) + patch=0 +elif [[ $1 == "patch" ]]; then + patch=$((patch+1)) +fi + +# Use the git count as build number. +build=$(git rev-list develop --count) + +formatted="$major.$minor.$patch.$build" + +echo "New Version: $formatted" + +echo "Creating a release branch using git flow ..." + +# Create a new feature branch via git flow. +git flow release start "$formatted" + +# Check if git flow branch was created. +if [ ! $? -eq 0 ]; then + echo "FAILED to create a release branch! Rolling back changes ..." + exit 1 +fi + +# Add new section to changelog. +echo "## Other Changes" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md +echo "## Fixes" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md +echo "## Removals" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md +echo "## Additions" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md +echo "# Version $formatted - $(date +"%Y-%m-%d")" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md + +# Replace in version.lua. +sed -e "s/.*major =.*/ major = $major,/" version.lua | tee version.lua +sed -e "s/.*minor =.*/ minor = $minor,/" version.lua | tee version.lua +sed -e "s/.*patch =.*/ patch = $patch,/" version.lua | tee version.lua +sed -e "s/.*build =.*/ build = $build,/" version.lua | tee version.lua + +# Commit the changes. +git commit -a -m "Prepare version $formatted" diff --git a/res/img/avatar.png b/res/img/avatar.png index 9fbac7b..c171747 100644 Binary files a/res/img/avatar.png and b/res/img/avatar.png differ diff --git a/res/img/icon/1024px.png b/res/img/icon/1024px.png new file mode 100644 index 0000000..d1e2b55 Binary files /dev/null and b/res/img/icon/1024px.png differ diff --git a/res/img/icon/128px.png b/res/img/icon/128px.png new file mode 100644 index 0000000..7d14a1f Binary files /dev/null and b/res/img/icon/128px.png differ diff --git a/res/img/icon/256px.png b/res/img/icon/256px.png new file mode 100644 index 0000000..b64abf2 Binary files /dev/null and b/res/img/icon/256px.png differ diff --git a/res/img/icon/32px.png b/res/img/icon/32px.png new file mode 100644 index 0000000..d77c73b Binary files /dev/null and b/res/img/icon/32px.png differ diff --git a/res/img/icon/512px.png b/res/img/icon/512px.png new file mode 100644 index 0000000..a9d2a28 Binary files /dev/null and b/res/img/icon/512px.png differ diff --git a/res/img/icon/64px.png b/res/img/icon/64px.png new file mode 100644 index 0000000..c350902 Binary files /dev/null and b/res/img/icon/64px.png differ diff --git a/res/img/step.png b/res/img/step.png deleted file mode 100644 index 951f4ad..0000000 Binary files a/res/img/step.png and /dev/null differ diff --git a/res/templates/example_log.txt b/res/templates/example_log.txt deleted file mode 100644 index 1975a1b..0000000 --- a/res/templates/example_log.txt +++ /dev/null @@ -1,981 +0,0 @@ -info: Robert Machmer|robert.machmer@gmail.com|1412164839 -A Readme.md -A conf.lua -A lib/Screen.lua -A lib/ScreenManager.lua -A main.lua -A src/FileHandler.lua -A src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412165331 -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412165654 -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412166102 -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412166546 -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412168721 -A res/fileNode.png -A src/FileObject.lua -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412261158 -M src/FileHandler.lua -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412263695 -M src/FileHandler.lua -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412264556 -A res/folderNode.png -M src/FileHandler.lua -A src/FileNode.lua -D src/FileObject.lua -A src/FolderNode.lua -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412266778 -M src/FolderNode.lua -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412266994 -M src/FileHandler.lua -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412338837 -M src/FileNode.lua -M src/FolderNode.lua -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412338894 -D src/FileNode.lua -D src/FolderNode.lua -M src/MainScreen.lua -A src/nodes/FileNode.lua -A src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412339571 -M src/nodes/FileNode.lua -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1412344563 -M src/MainScreen.lua -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1413460386 -M src/nodes/FileNode.lua -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1413543677 -M src/nodes/FileNode.lua -M src/nodes/FolderNode.lua -A src/nodes/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1413545829 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1413641316 -M main.lua - -info: Robert Machmer|robert.machmer@gmail.com|1413647269 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1413746796 -M src/MainScreen.lua -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1420035671 -M src/FileHandler.lua - -info: Robert Machmer|robert.machmer@gmail.com|1420035782 -M src/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1420036758 -M Readme.md - -info: Robert Machmer|robert.machmer@gmail.com|1420119343 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1420127901 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1420128096 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1420128221 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1420142059 -M lib/Screen.lua -M lib/ScreenManager.lua -M main.lua -D src/MainScreen.lua -A src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1420148522 -A lib/Camera.lua -M src/nodes/FolderNode.lua -M src/nodes/Node.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421154008 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421154091 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421170328 -A src/Authors.lua -M src/FileHandler.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421170600 -M conf.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421198671 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421202411 -A src/FileManager.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421205955 -D src/FileHandler.lua -A src/LogReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421206481 -M src/LogReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421227556 -M res/fileNode.png -M src/FileManager.lua -M src/nodes/FileNode.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421231021 -M Readme.md - -info: Robert Machmer|robert.machmer@gmail.com|1421396416 -M src/Authors.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421401857 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421402864 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421408663 -M Readme.md - -info: Robert Machmer|robert.machmer@gmail.com|1421509527 -A src/AuthorManager.lua -D src/Authors.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421519652 -M src/nodes/FileNode.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421523219 -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421528929 -A src/Author.lua -M src/AuthorManager.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421539184 -M src/Author.lua -M src/AuthorManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421539872 -M src/Author.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421540698 -M src/AuthorManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421547891 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421548797 -M conf.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421549312 -M conf.lua -M main.lua -M src/Author.lua -M src/AuthorManager.lua -M src/FileManager.lua -M src/LogReader.lua -M src/nodes/FileNode.lua -M src/nodes/FolderNode.lua -M src/nodes/Node.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421550122 -M main.lua -M src/Author.lua -M src/AuthorManager.lua -M src/FileManager.lua -M src/LogReader.lua -M src/nodes/FileNode.lua -M src/nodes/FolderNode.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421579809 -M src/LogReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421581498 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421587878 -M conf.lua -M main.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421589831 -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421590288 -D res/folderNode.png -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421591304 -M src/nodes/FolderNode.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421596235 -M src/AuthorManager.lua -A src/ConfigReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421596666 -M src/ConfigReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421597404 -M src/AuthorManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421600497 -D lib/Screen.lua -D lib/ScreenManager.lua -A lib/screenmanager/Readme.md -A lib/screenmanager/Screen.lua -A lib/screenmanager/ScreenManager.lua -M main.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421603864 -M main.lua -M src/ConfigReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421719866 -M conf.lua -M src/ConfigReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421833270 -M lib/Camera.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421838188 -A res/file.png -D res/fileNode.png -M src/LogReader.lua -A src/graph/File.lua -A src/graph/Graph.lua -A src/graph/Node.lua -D src/nodes/FileNode.lua -D src/nodes/FolderNode.lua -D src/nodes/Node.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421972644 -A res/user.png -M src/AuthorManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1421975143 -M src/AuthorManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1422185971 -M src/graph/Graph.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1422186427 -M src/Author.lua -M src/AuthorManager.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1422786015 -M src/AuthorManager.lua -M src/ConfigReader.lua -M src/LogReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1422837441 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1422960161 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1422962666 -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1422971825 -M lib/screenmanager/ScreenManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1423320202 -M src/graph/File.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1423403469 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1423445606 -M src/FileManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1423445842 -M src/FileManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1423446229 -M main.lua - -info: Robert Machmer|robert.machmer@gmail.com|1423900936 -M src/FileManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1423901214 -M src/graph/File.lua -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1423992268 -M src/LogReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427529076 -M res/file.png -M src/graph/Node.lua - -info: rmcode|robert.machmer@gmail.com|1427660215 -M Readme.md - -info: Robert Machmer|robert.machmer@gmail.com|1427660238 -M main.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427662791 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427663671 -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427663911 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427664089 -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427664992 -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427665463 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427665685 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427665887 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427667043 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427667629 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427667755 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427667862 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427669309 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427670123 -D lib/Camera.lua -A lib/camera/Camera.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427674743 -M res/user.png - -info: Robert Machmer|robert.machmer@gmail.com|1427674989 -M conf.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427706266 -M main.lua -M src/AuthorManager.lua -M src/graph/File.lua -M src/graph/Graph.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427706761 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427711700 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427728116 -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427728127 -M src/graph/File.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427729076 -M src/ConfigReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427730189 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427753588 -M conf.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427753963 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427755389 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427808901 -M lib/screenmanager/Screen.lua -M lib/screenmanager/ScreenManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427816735 -M src/Author.lua -M src/AuthorManager.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427818995 -M main.lua -M src/FileManager.lua -M src/screens/MainScreen.lua -A src/ui/Panel.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427819306 -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427837032 -M src/Author.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427840420 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427847257 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427848414 -D src/ConfigReader.lua -A src/conf/ConfigReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427849379 -M src/conf/ConfigReader.lua -A src/conf/Template.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427850457 -M src/conf/ConfigReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427850600 -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427851668 -M src/AuthorManager.lua -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427851674 -M main.lua -M src/AuthorManager.lua -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427851917 -M src/AuthorManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427880295 -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427882656 -M src/conf/Template.lua -M src/graph/Graph.lua -M src/graph/Node.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427883181 -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427883392 -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427884293 -M src/Author.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427884826 -M src/Author.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427885169 -A LICENSE - -info: rmcode|robert.machmer@gmail.com|1427885243 -A README.md -D Readme.md - -info: Robert Machmer|robert.machmer@gmail.com|1427886399 -M README.md - -info: Robert Machmer|robert.machmer@gmail.com|1427886841 -D res/file.png -A res/img/file.png -A res/img/user.png -D res/user.png -M src/AuthorManager.lua -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427892008 -A res/fonts/SIL Open Font License.txt -A res/fonts/SourceCodePro-Medium.otf -M src/graph/Graph.lua -M src/graph/Node.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427892194 -M src/FileManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427892445 -M src/FileManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427892644 -M conf.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427894190 -M conf.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427894264 -M conf.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427897659 -M src/conf/Template.lua -M src/graph/Graph.lua -M src/graph/Node.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427898068 -M src/conf/Template.lua -M src/graph/Graph.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427936097 -M src/LogReader.lua -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427936619 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427936897 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427976666 -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427976679 -M src/Author.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427993275 -M src/AuthorManager.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427994856 -M src/LogReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427995775 -M src/LogReader.lua -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1427995792 -M src/LogReader.lua -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428005944 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428015869 -M src/LogReader.lua -M src/conf/Template.lua -M src/graph/Graph.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428015967 -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428018314 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428018880 -M src/LogReader.lua -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428053610 -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428073565 -M src/conf/Template.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428104660 -M src/Author.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428105158 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428105654 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428106654 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428163411 -M src/FileManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428188837 -M src/LogReader.lua -A src/Timeline.lua -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428191406 -D src/Timeline.lua -M src/screens/MainScreen.lua -A src/ui/Timeline.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428196160 -M src/graph/Graph.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428197088 -M src/ui/Panel.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428198089 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428198761 -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428224954 -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428277319 -M src/LogReader.lua -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428279440 -M src/LogReader.lua -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428280587 -M src/Author.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428309651 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428339053 -M src/LogReader.lua -M src/screens/MainScreen.lua -M src/ui/Timeline.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428350427 -M README.md - -info: Robert Machmer|robert.machmer@gmail.com|1428357571 -M src/LogReader.lua -M src/ui/Timeline.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428363849 -M src/FileManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428370108 -M src/FileManager.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428399939 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428400533 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428401257 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428521580 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428535218 -M src/AuthorManager.lua -M src/LogReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428535519 -M src/AuthorManager.lua -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428571917 -M src/LogReader.lua -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428578992 -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428579312 -M src/AuthorManager.lua -M src/graph/Graph.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428579477 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428581353 -M src/AuthorManager.lua -M src/LogReader.lua -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428581987 -M src/LogReader.lua -M src/graph/Graph.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428583714 -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428583741 -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428583812 -M src/graph/Graph.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428591387 -M src/graph/File.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428592678 -M src/graph/File.lua -M src/graph/Node.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428662955 -M src/ui/Timeline.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428667958 -M src/LogReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428670174 -M conf.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428706037 -M src/ui/Panel.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428713834 -M src/screens/MainScreen.lua - -info: EntranceJew|EntranceJew@gmail.com|1428715691 -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428715835 -M src/conf/Template.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428716450 -A src/InputHandler.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428717805 -M src/screens/MainScreen.lua -A src/ui/CamWrapper.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428747481 -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428762289 -A src/LogLoader.lua -M src/LogReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428762480 -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428762751 -D src/LogLoader.lua -D src/LogReader.lua -A src/logfactory/LogLoader.lua -A src/logfactory/LogReader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428833130 -M src/logfactory/LogLoader.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428833413 -M main.lua -M src/logfactory/LogLoader.lua -M src/screens/MainScreen.lua -A src/screens/SelectionScreen.lua -A src/ui/Button.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428833628 -M src/screens/SelectionScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428834468 -M src/conf/ConfigReader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428835134 -M src/screens/MainScreen.lua -M src/screens/SelectionScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428837440 -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428840719 -M src/screens/SelectionScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428846771 -M src/screens/SelectionScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428857142 -M src/ui/Button.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428857574 -M src/screens/SelectionScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428863026 -M src/screens/SelectionScreen.lua -M src/ui/Button.lua -A src/ui/ButtonList.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428865168 -M src/ui/Button.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428865771 -A res/fonts/SourceCodePro-Bold.otf -M src/screens/SelectionScreen.lua -M src/ui/ButtonList.lua -A src/ui/InfoPanel.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428867077 -M src/screens/SelectionScreen.lua -M src/ui/Button.lua -M src/ui/ButtonList.lua -M src/ui/InfoPanel.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428867121 -M src/ui/Button.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428867160 -M src/ui/Button.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428915681 -M src/conf/Template.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428916708 -M src/logfactory/LogLoader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428918486 -M src/logfactory/LogLoader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428919369 -A res/log/example_log.txt -M src/logfactory/LogLoader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428919664 -M src/logfactory/LogLoader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428919695 -M src/logfactory/LogLoader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428919952 -M src/logfactory/LogLoader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428921032 -M src/logfactory/LogLoader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428931073 -M src/screens/SelectionScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428931540 -M main.lua -M src/screens/SelectionScreen.lua -M src/ui/Button.lua -M src/ui/InfoPanel.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428937253 -M src/screens/SelectionScreen.lua -M src/ui/ButtonList.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428937278 -M src/screens/SelectionScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428956825 -M src/screens/SelectionScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428957125 -M src/screens/SelectionScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428957337 -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428957688 -M README.md - -info: Robert Machmer|robert.machmer@gmail.com|1428959105 -M src/FileManager.lua -M src/conf/Template.lua -M src/screens/MainScreen.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428996976 -M src/logfactory/LogLoader.lua - -info: Robert Machmer|robert.machmer@gmail.com|1428997224 -M README.md diff --git a/res/templates/settings_template.cfg b/res/templates/settings_template.cfg index 1bd3565..f030699 100644 --- a/res/templates/settings_template.cfg +++ b/res/templates/settings_template.cfg @@ -1,30 +1,44 @@ -; ------------------------------- +; ---------------------------------------------- ; LoGiVi - Configuration File -; ------------------------------- - -[repositories] -; project = path/to/repository +; ---------------------------------------------- +; ---------------------------------------------- [options] +; ---------------------------------------------- +; The play mode to use: +; 'default' Start at the first commit and move forward in time. +; 'rewind' Start at the last commit and move backwards in time. mode = default + +; Wether or not the visualisation should start right after the log has been loaded. autoplay = true + +; Wether to show or hide the ui elements (can also be toggled via hotkeys). +showAuthorIcons = true +showAuthorLabels = false showFileList = true -showAuthors = true -showLabels = true +showFileLabels = false showTimeline = true + +; The time to wait before loading a new commit. commitDelay = 0.2 + edgeWidth = 5 backgroundColor = 0, 0, 0 -removeTmpFiles = false -screenWidth = 0 -screenHeight = 0 -fullscreen = false + +fullscreen = true fullscreenType = desktop +screenWidth = 800 +screenHeight = 600 + vsync = true msaa = 0 + display = 1 +; ---------------------------------------------- [keyBindings] +; ---------------------------------------------- camera_n = w camera_w = a camera_s = s @@ -34,10 +48,11 @@ camera_rotateR = e camera_zoomIn = +, up camera_zoomOut = -, down -toggleAuthors = 1 -toggleFileList = 2 -toggleLabels = 3 -toggleTimeline = 4 +toggleAuthorIcons = 1 +toggleAuthorLabels = 2 +toggleFileList = 3 +toggleFileLabels = 4 +toggleTimeline = 5 toggleSimulation = space toggleRewind = backspace diff --git a/src/Author.lua b/src/Author.lua deleted file mode 100644 index 3b5739d..0000000 --- a/src/Author.lua +++ /dev/null @@ -1,148 +0,0 @@ -local Resources = require('src.Resources'); - --- ------------------------------------------------ --- Module --- ------------------------------------------------ - -local Author = {}; - --- ------------------------------------------------ --- Constants --- ------------------------------------------------ - -local LABEL_FONT = Resources.loadFont('SourceCodePro-Medium.otf', 20); -local DEFAULT_FONT = Resources.loadFont('default', 12); - -local AVATAR_SIZE = 48; -local AUTHOR_INACTIVITY_TIMER = 2; -local LINK_INACTIVITY_TIMER = 2; -local FADE_FACTOR = 125; -local DEFAULT_AVATAR_ALPHA = 255; -local DEFAULT_LINK_ALPHA = 100; -local DAMPING_FACTOR = 0.90; -local FORCE_MAX = 2; -local FORCE_SPRING = -0.5; -local BEAM_WIDTH = 3; - -local LINK_COLOR = { - A = { 0, 255, 0 }, - D = { 255, 0, 0 }, - M = { 254, 140, 0 }, -}; - --- ------------------------------------------------ --- Constructor --- ------------------------------------------------ - -function Author.new(name, avatar, cx, cy) - local self = {}; - - local active = true; - - local posX, posY = cx + love.math.random(5, 200) * (love.math.random(0, 1) == 0 and -1 or 1), cy + love.math.random(5, 200) * (love.math.random(0, 1) == 0 and -1 or 1); - local accX, accY = 0, 0; - local velX, velY = 0, 0; - - local links = {}; - local inactivity = 0; - local avatarAlpha = DEFAULT_AVATAR_ALPHA; - local linkAlpha = DEFAULT_LINK_ALPHA; - - -- Avatar's width and height. - local aw, ah = avatar:getWidth(), avatar:getHeight(); - - -- ------------------------------------------------ - -- Private Functions - -- ------------------------------------------------ - - local function clamp(min, val, max) - return math.max(min, math.min(val, max)); - end - - local function reactivate() - inactivity = 0; - active = true; - avatarAlpha = DEFAULT_AVATAR_ALPHA; - linkAlpha = DEFAULT_LINK_ALPHA; - end - - local function move(dt) - velX = (velX + accX * dt * 32) * DAMPING_FACTOR; - velY = (velY + accY * dt * 32) * DAMPING_FACTOR; - posX = posX + velX; - posY = posY + velY; - end - - local function applyForce(fx, fy) - accX = clamp(-FORCE_MAX, accX + fx, FORCE_MAX); - accY = clamp(-FORCE_MAX, accY + fy, FORCE_MAX); - end - - -- ------------------------------------------------ - -- Public Functions - -- ------------------------------------------------ - - function self:draw(rotation, scale) - if active then - love.graphics.setLineWidth(BEAM_WIDTH); - for i = 1, #links do - love.graphics.setColor(LINK_COLOR[links[i].mod][1], LINK_COLOR[links[i].mod][2], LINK_COLOR[links[i].mod][3], linkAlpha); - love.graphics.line(posX, posY, links[i].file:getX(), links[i].file:getY()); - end - love.graphics.setLineWidth(1); - - love.graphics.setColor(255, 255, 255, avatarAlpha); - love.graphics.draw(avatar, posX, posY, -rotation, AVATAR_SIZE / aw, AVATAR_SIZE / ah, aw * 0.5, ah * 0.5); - love.graphics.setFont(LABEL_FONT); - love.graphics.print(name, posX, posY, -rotation, 1 / scale, 1 / scale, LABEL_FONT:getWidth(name) * 0.5, - AVATAR_SIZE * scale); - love.graphics.setFont(DEFAULT_FONT); - love.graphics.setColor(255, 255, 255, 255); - end - end - - function self:update(dt) - if active then - move(dt); - - inactivity = inactivity + dt; - if inactivity > AUTHOR_INACTIVITY_TIMER then - avatarAlpha = clamp(0, avatarAlpha - dt * FADE_FACTOR, 255); - end - if inactivity > LINK_INACTIVITY_TIMER then - linkAlpha = clamp(0, linkAlpha - dt * FADE_FACTOR, 255); - end - if inactivity > 0.5 then - accX, accY = 0, 0; - end - if avatarAlpha <= 0 then - active = false; - self:resetLinks(); - end - end - end - - function self:addLink(file, modifier) - reactivate(); - links[#links + 1] = { file = file, mod = modifier }; - - local dx, dy = posX - file:getX(), posY - file:getY(); - local distance = math.sqrt(dx * dx + dy * dy); - dx = dx / distance; - dy = dy / distance; - - local strength = FORCE_SPRING * distance; - applyForce(dx * strength, dy * strength); - end - - function self:resetLinks() - links = {}; - end - - return self; -end - --- ------------------------------------------------ --- Return Module --- ------------------------------------------------ - -return Author; diff --git a/src/AuthorManager.lua b/src/AuthorManager.lua deleted file mode 100644 index 24c273e..0000000 --- a/src/AuthorManager.lua +++ /dev/null @@ -1,193 +0,0 @@ -local Author = require('src.Author'); -local http = require('socket.http'); - --- ------------------------------------------------ --- Module --- ------------------------------------------------ - -local AuthorManager = {}; - --- ------------------------------------------------ --- Constants --- ------------------------------------------------ - -local PATH_AVATARS = 'tmp/avatars/'; -local PATH_DEFAULT_AVATAR = 'res/img/avatar.png'; - --- ------------------------------------------------ --- Local Variables --- ------------------------------------------------ - -local authors; -local avatars; -local aliases; -local addresses; -local visible; - -local activeAuthor; - -local graphCenterX, graphCenterY; - --- ------------------------------------------------ --- Local Functions --- ------------------------------------------------ - ---- --- Tries to load user avatars from the local filesystem or the internet. --- @param urlList --- -local function grabAvatars(urlList) - local counter = 0; - local avatars = {}; - for author, url in pairs(urlList) do - -- If the file exists locally we load it as usual. - -- If it doesn't we see if the url returns something useful. - if love.filesystem.isFile(url) then - avatars[author] = love.graphics.newImage(url); - else - local body = http.request(url); - if body then - -- Set up the temporary folder if we don't have one yet. - if not love.filesystem.isDirectory(PATH_AVATARS) then - love.filesystem.createDirectory(PATH_AVATARS); - end - - -- Write file to a temporary folder. - love.filesystem.write(string.format(PATH_AVATARS .. "tmp_%03d.png", counter), body); - - local ok, image = pcall(love.graphics.newImage, string.format(PATH_AVATARS .. "tmp_%03d.png", counter)); - if ok then - avatars[author] = image; - counter = counter + 1; - else - print("Couldn't load avatar from " .. url .. " - A default avatar will be used instead."); - end - counter = counter + 1; - end - end - end - - -- Load the default user avatar. - avatars['default'] = love.graphics.newImage(PATH_DEFAULT_AVATAR); - - return avatars; -end - --- ------------------------------------------------ --- Public Functions --- ------------------------------------------------ - -function AuthorManager.init(naliases, avatarUrls, visibility) - -- Set up the table to store all authors. - authors = {}; - - addresses = {}; - aliases = naliases; - - -- Load avatars from the local filesystem or an online location. - avatars = grabAvatars(avatarUrls); - - visible = visibility; - - graphCenterX, graphCenterY = 0, 0; -end - ---- --- Draws a list of all authors working on the project. --- -function AuthorManager.drawLabels(rotation, scale) - if visible then - for _, author in pairs(authors) do - author:draw(rotation, scale); - end - end -end - ---- --- Updates all authors. --- @param dt --- -function AuthorManager.update(dt) - for name, author in pairs(authors) do - author:update(dt); - end -end - ---- --- Adds a link from the current author to a file. --- @param file --- -function AuthorManager.addFileLink(file, modifier) - activeAuthor:addLink(file, modifier) -end - ---- --- Receives a notification from an observable. --- @param self --- @param event --- @param ... --- -function AuthorManager.receive(self, event, ...) - if event == 'NEW_COMMIT' then - AuthorManager.setCommitAuthor(...); - elseif event == 'GRAPH_UPDATE_FILE' then - AuthorManager.addFileLink(...) - elseif event == 'GRAPH_UPDATE_CENTER' then - AuthorManager.setGraphCenter(...); - end -end - ---- --- Sets the author of the currently processed commit and resets the previously --- active one. If he doesn't exist yet he will be created and added to the list --- of authors for. Before storing the author the function checks the config file --- to see if an alias is associated with the specific email address. --- If it is, it will use the alias and ignore the name found in the log file. --- If there isn't an alias, it will check if there already is another nickname --- stored for that email address. If there isn't, it will use the nickname found --- in the log. --- @param nemail --- @param nauthor --- @param cx --- @param cy --- -function AuthorManager.setCommitAuthor(nemail, nauthor) - if activeAuthor then activeAuthor:resetLinks() end - - local nickname = aliases[nemail] or addresses[nemail] or nauthor; - if not authors[nickname] then - addresses[nemail] = nauthor; -- Store this name as the default for this email address. - authors[nickname] = Author.new(nickname, avatars[nickname] or avatars['default'], graphCenterX, graphCenterY); - end - - activeAuthor = authors[nickname]; -end - ---- --- Shows / Hides authors. --- @param nv --- -function AuthorManager.setVisible(nv) - visible = nv; -end - ---- --- Returns visibility of authors. --- -function AuthorManager.isVisible() - return visible; -end - ---- --- @param ncx --- @param ncy --- -function AuthorManager.setGraphCenter(ncx, ncy) - graphCenterX, graphCenterY = ncx, ncy; -end - --- ------------------------------------------------ --- Return Module --- ------------------------------------------------ - -return AuthorManager; diff --git a/src/FileManager.lua b/src/FileManager.lua index 9b48461..cfdb3f7 100644 --- a/src/FileManager.lua +++ b/src/FileManager.lua @@ -1,12 +1,5 @@ local FileManager = {}; --- ------------------------------------------------ --- Constants --- ------------------------------------------------ - -local FRST_OFFSET = 10; -local SCND_OFFSET = 50; - -- ------------------------------------------------ -- Local Variables -- ------------------------------------------------ @@ -21,82 +14,73 @@ local colors; -- ------------------------------------------------ --- --- Takes the extensions list and creates a list --- which is sorted by the amount of files per extension. --- @param extensions +-- Sorts the list of extensions and sorts them based on the amount of files +-- which currently exist in the repository. -- -local function createSortedList(extensions) - for k in pairs(sortedList) do +local function createSortedList() + for k in pairs( sortedList ) do sortedList[k] = nil; end - for ext, tbl in pairs(extensions) do + for _, tbl in pairs( extensions ) do sortedList[#sortedList + 1] = tbl; end - table.sort(sortedList, function(a, b) + table.sort( sortedList, function( a, b ) return a.amount > b.amount; end); end --- ------------------------------------------------ --- Public Functions --- ------------------------------------------------ - --- --- Draws a counter of all files in the project and --- a separate counter for each used file extension. +-- Creates a new custom color for the extension if it doesn't have one yet. +-- @param ext (string) The extension to add a new file for. +-- @return (table) A table containing the RGB values for this extension. -- -function FileManager.draw(x, y) - love.graphics.print(totalFiles, x + FRST_OFFSET, y + 10); - love.graphics.print('Files', x + SCND_OFFSET, y + 10); - for i, tbl in ipairs(sortedList) do - love.graphics.setColor(tbl.color.r, tbl.color.g, tbl.color.b); - love.graphics.print(tbl.amount, x + FRST_OFFSET, y + 10 + i * 20); - love.graphics.print(tbl.extension, x + SCND_OFFSET, y + 10 + i * 20); - love.graphics.setColor(255, 255, 255); +local function assignColor( ext ) + if not colors[ext] then + colors[ext] = { + r = love.math.random( 0, 255 ), + g = love.math.random( 0, 255 ), + b = love.math.random( 0, 255 ) + }; end + return colors[ext]; end -function FileManager.update(dt) - return 0, 0, 0, 10 + (#sortedList + 1) * 20; -end +-- ------------------------------------------------ +-- Public Functions +-- ------------------------------------------------ --- --- Adds a new file extension to the list. --- @param fileName --- @param ext +-- Adds a new file belonging to a certain extension to the list. If the +-- extension doesn't exist yet we allocate a new table for it. +-- @param ext (string) The extension to add a new file for. +-- @return (table) The table containing RGB values for this extension. +-- @return (string) The extension string. -- -function FileManager.add(fileName, ext) +function FileManager.add( ext ) if not extensions[ext] then extensions[ext] = {}; extensions[ext].extension = ext; extensions[ext].amount = 0; - extensions[ext].color = colors[ext] or { - r = love.math.random(0, 255), - g = love.math.random(0, 255), - b = love.math.random(0, 255) - }; + extensions[ext].color = assignColor( ext ); end extensions[ext].amount = extensions[ext].amount + 1; totalFiles = totalFiles + 1; - createSortedList(extensions); + createSortedList( extensions ); return extensions[ext].color, ext; end --- --- Reduce the amount of counted files of the --- same extension. If there are no more files --- of that extension, it will remove it from --- the list. --- @param fileName --- @param ext +-- Decrements the counter for a certain extension. If there are no more files +-- of that extension, it will remove it from the table. +-- @param ext (string) The extension to remove a file from. -- -function FileManager.remove(fileName, ext) +function FileManager.remove( ext ) if not extensions[ext] then - error('Tried to remove the non existing file extension "' .. ext .. '".'); + error( 'Tried to remove the non existing file extension "' .. ext .. '".' ); end extensions[ext].amount = extensions[ext].amount - 1; @@ -105,9 +89,12 @@ function FileManager.remove(fileName, ext) extensions[ext] = nil; end - createSortedList(extensions); + createSortedList( extensions ); end +--- +-- Resets the state of the FileManager. +-- function FileManager.reset() extensions = {}; sortedList = {}; @@ -120,20 +107,41 @@ end -- ------------------------------------------------ --- --- @param ext +-- Gets the color table for a certain file extension. +-- @param ext (string) The extension to return the color for. +-- @return (table) A table containing the RGB values for the extension. -- -function FileManager.getColor(ext) +function FileManager.getColor( ext ) return extensions[ext].color; end +--- +-- Returns the sorted list of file extensions. +-- @return (table) The sorted list of file extensions. +-- +function FileManager.getSortedList() + return sortedList; +end + +--- +-- Returns the total amount of files in the repository. +-- @return (number) The total amount of files in the repository. +-- +function FileManager.getTotalFiles() + return totalFiles; +end + -- ------------------------------------------------ -- Setters -- ------------------------------------------------ --- --- @param ncol +-- Sets the default color table. This can be used to specify colors for +-- certain extensions (instead of randomly creating them). +-- @param ncol (table) The table containing RGBA values belonging to a certain +-- file extension. -- -function FileManager.setColorTable(ncol) +function FileManager.setColorTable( ncol ) colors = ncol; end diff --git a/src/InputHandler.lua b/src/InputHandler.lua index d54924f..6dbd7e0 100644 --- a/src/InputHandler.lua +++ b/src/InputHandler.lua @@ -2,29 +2,31 @@ local InputHandler = {}; --- -- Determines if a key constant or if any in a table of key constants are down. --- @param constant - The key constant or table of constants. +-- @param constant (string) The key constant or table of constants. +-- @return (boolean) True if the key or the keys are down. -- -function InputHandler.isDown(constant) - if type(constant) == 'table' then - for _, keyToCheck in ipairs(constant) do - if love.keyboard.isDown(keyToCheck) then +function InputHandler.isDown( constant ) + if type( constant ) == 'table' then + for _, keyToCheck in ipairs( constant ) do + if love.keyboard.isDown( keyToCheck ) then return true; end end return false; else - return love.keyboard.isDown(constant); + return love.keyboard.isDown( constant ); end end --- -- Determines if a key constant or if any in a table of key constants was pressed. --- @param key - The key to check for. --- @param constant - The key constant or table of constants. +-- @param key (string) The key to check. +-- @param constant (string) The key constant or table of constants to check for. +-- @return (boolean) True if the key or the keys are pressed. -- -function InputHandler.isPressed(key, constant) - if type(constant) == 'table' then - for _, keyToCheck in ipairs(constant) do +function InputHandler.isPressed( key, constant ) + if type( constant ) == 'table' then + for _, keyToCheck in ipairs( constant ) do if key == keyToCheck then return true; end diff --git a/src/RepositoryInfos.lua b/src/RepositoryInfos.lua new file mode 100644 index 0000000..d4df3c9 --- /dev/null +++ b/src/RepositoryInfos.lua @@ -0,0 +1,64 @@ +local Serpent = require( 'lib.serpent.Serpent' ); + +local RepositoryInfos = {}; + +local INFO_FILE = 'logs/%s/info.lua'; +local COUNT_FILE = 'logs/%s/.commits'; + +--- +-- Returns true if an info file for the given project already exists. +-- @param projectname (string) The name under which to store the info file. +-- @return (boolean) True if the file already exists. +-- +function RepositoryInfos.hasCommitCountFile( projectname ) + return love.filesystem.isFile( string.format( INFO_FILE, projectname )); +end + +--- +-- Creates an info file for a certain project / repository. This file keeps +-- track of things like the total amount of commits in the repository, custom +-- author names, custom colors, etc. +-- @param projectname (string) The name under which to store the info file. +-- +function RepositoryInfos.createInfoFile( projectname ) + local output = { + name = projectname, + aliases = {}, + colors = {} + }; + love.filesystem.write( string.format( INFO_FILE, projectname ), Serpent.block( output, { comment = false })); +end + +--- +-- Creates a file which stores the amount of commits in the repository. This +-- will be used to check if the log file needs to be updated. +-- @param projectname (string) The name under which to store the info file. +-- @param commits (number) The count of commits to write to the file. +-- +function RepositoryInfos.createCommitCountFile( projectname, commits ) + local output = { + commits = commits or 0 + } + love.filesystem.write( string.format( COUNT_FILE, projectname ), Serpent.block( output, { comment = false })); +end + +--- +-- Loads information about a git repository. +-- @param name (string) The name of the git log to load the info file for. +-- @return (table) A table containing information about the git log. +-- +function RepositoryInfos.loadInfo( name ) + local content = love.filesystem.read( string.format( INFO_FILE, name )); + local ok, tbl = Serpent.load( content ); + assert( ok, "Couldn't read info file for " .. name ); + return tbl; +end + +function RepositoryInfos.loadCommitCount( name ) + local content = love.filesystem.read( string.format( COUNT_FILE, name )); + local ok, tbl = Serpent.load( content ); + assert( ok, "Couldn't read count file for " .. name ); + return tbl; +end + +return RepositoryInfos; diff --git a/src/Resources.lua b/src/Resources.lua index f5fed1b..f9c57c0 100644 --- a/src/Resources.lua +++ b/src/Resources.lua @@ -8,13 +8,13 @@ local IMG_PATH = 'res/img/'; local FONT_PATH = 'res/fonts/'; -- ------------------------------------------------ --- Local Variables +-- Private Class Variables -- ------------------------------------------------ local images = {}; local fonts = { default = { - [12] = love.graphics.newFont(12) + [12] = love.graphics.newFont( 12 ) } }; @@ -24,26 +24,26 @@ local fonts = { --- -- Loads an image or returns an already loaded image. --- @param name - The name of the file to load. +-- @param name (string) The name of the file to load. -- -function Resources.loadImage(name) +function Resources.loadImage( name ) if not images[name] then - images[name] = love.graphics.newImage(IMG_PATH .. name); + images[name] = love.graphics.newImage( IMG_PATH .. name ); end - return images[name] + return images[name]; end --- -- Loads a font or returns an already loaded font. --- @param name - The name of the font to load. --- @param size - The size of the font to load. +-- @param name (string) The name of the font to load. +-- @param size (number) The size of the font to load. -- -function Resources.loadFont(name, size) +function Resources.loadFont( name, size ) if not fonts[name] then fonts[name] = {}; - fonts[name][size] = love.graphics.newFont(FONT_PATH .. name, size); + fonts[name][size] = love.graphics.newFont( FONT_PATH .. name, size ); elseif not fonts[name][size] then - fonts[name][size] = love.graphics.newFont(FONT_PATH .. name, size); + fonts[name][size] = love.graphics.newFont( FONT_PATH .. name, size ); end return fonts[name][size]; end diff --git a/src/Utility.lua b/src/Utility.lua new file mode 100644 index 0000000..ab77754 --- /dev/null +++ b/src/Utility.lua @@ -0,0 +1,33 @@ +local Utility = {}; + +--- +-- Clamps a value to a certain range. +-- @param min (number) The minimum value to clamp to. +-- @param val (number) The value to clamp. +-- @param max (number) The maximum value to clamp to. +-- @return (number) The clamped value. +-- +function Utility.clamp( min, val, max ) + return math.max( min, math.min( val, max )); +end + +--- +-- Linear interpolation between a and b. +-- @param a (number) The current value. +-- @param b (number) The target value. +-- @param t (number) The time value. +-- @return (number) The interpolated value. +-- +function Utility.lerp( a, b, t ) + return a + ( b - a ) * t; +end + +--- +-- Returns a random sign (+ or -). +-- @return (number) Randomly returns either -1 or 1. +-- +function Utility.randomSign() + return love.math.random( 0, 1 ) == 0 and -1 or 1; +end + +return Utility; diff --git a/src/authors/Author.lua b/src/authors/Author.lua new file mode 100644 index 0000000..eab7afb --- /dev/null +++ b/src/authors/Author.lua @@ -0,0 +1,206 @@ +local Resources = require( 'src.Resources' ); +local Utility = require( 'src.Utility' ); + +-- ------------------------------------------------ +-- Constants +-- ------------------------------------------------ + +local LABEL_FONT = Resources.loadFont( 'SourceCodePro-Medium.otf', 20 ); +local DEFAULT_FONT = Resources.loadFont( 'default', 12 ); + +local AVATAR_SIZE = 48; +local INACTIVITY_TIMER = 2; +local MOVEMENT_TIMER = 0.5; +local FADE_FACTOR = 125; +local DEFAULT_AVATAR_ALPHA = 255; +local DEFAULT_LINK_ALPHA = 100; +local DAMPING_FACTOR = 0.90; +local FORCE_MAX = 2; +local FORCE_SPRING = -0.5; +local BEAM_WIDTH = 3; +local MOVEMENT_SPEED = 32; + +local LINK_COLOR = { + A = { r = 0, g = 255, b = 0 }, + D = { r = 255, g = 0, b = 0 }, + M = { r = 254, g = 140, b = 0 }, +}; + +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ + +local Author = {}; + +-- ------------------------------------------------ +-- Constructor +-- ------------------------------------------------ + +function Author.new( name, avatar, spritebatch, cx, cy ) + local self = {}; + + local active = true; + + local posX, posY = cx + love.math.random( 5, 200 ) * ( love.math.random( 0, 1 ) == 0 and -1 or 1 ), cy + love.math.random( 5, 200 ) * ( love.math.random( 0, 1 ) == 0 and -1 or 1 ); + local accX, accY = 0, 0; + local velX, velY = 0, 0; + + local links = {}; + local inactivity = 0; + local avatarAlpha = DEFAULT_AVATAR_ALPHA; + local linkAlpha = DEFAULT_LINK_ALPHA; + + local color = { + r = love.math.random( 0, 255 ), + g = love.math.random( 0, 255 ), + b = love.math.random( 0, 255 ) + }; + + -- Avatar's width and height. + local aw, ah = avatar:getWidth(), avatar:getHeight(); + + -- ------------------------------------------------ + -- Private Functions + -- ------------------------------------------------ + + --- + -- Resets an author's state. + -- + local function reactivate() + inactivity = 0; + active = true; + avatarAlpha = DEFAULT_AVATAR_ALPHA; + linkAlpha = DEFAULT_LINK_ALPHA; + end + + --- + -- Deactivates an author and hides resets his links. + -- + local function deactivate() + active = false; + self:resetLinks(); + end + + --- + -- Moves the author. + -- @param dt (number) The delta time between frames. + -- + local function move( dt ) + velX = ( velX + accX * dt * MOVEMENT_SPEED ) * DAMPING_FACTOR; + velY = ( velY + accY * dt * MOVEMENT_SPEED ) * DAMPING_FACTOR; + posX = posX + velX; + posY = posY + velY; + end + + --- + -- Changes the acceleration of an author based on the force values. + -- The actual moving is handled by the move function. + -- @param fx (number) The force along the x-axis. + -- @param fy (number) The force along the y-axis. + -- + local function applyForce( fx, fy ) + accX = Utility.clamp( -FORCE_MAX, accX + fx, FORCE_MAX ); + accY = Utility.clamp( -FORCE_MAX, accY + fy, FORCE_MAX ); + end + + -- ------------------------------------------------ + -- Public Functions + -- ------------------------------------------------ + + --- + -- Draws the author. + -- @param rotation (number) The camera's rotation. + -- @param scale (number) The camera's zoom factor. + -- @param showLabel (boolean) Wether to show or hide the name label. + -- + function self:draw( rotation, scale, showLabel ) + if active then + love.graphics.setLineWidth( BEAM_WIDTH ); + for i = 1, #links do + local link = links[i]; + local type = link.mod; + love.graphics.setColor( LINK_COLOR[type].r, LINK_COLOR[type].g, LINK_COLOR[type].b, linkAlpha ); + love.graphics.line( posX, posY, link.file:getX(), link.file:getY() ); + end + love.graphics.setLineWidth( 1 ); + love.graphics.setColor( 255, 255, 255, avatarAlpha ); + + if showLabel then + love.graphics.setFont( LABEL_FONT ); + love.graphics.print( name, posX, posY, -rotation, 1 / scale, 1 / scale, LABEL_FONT:getWidth(name) * 0.5, - AVATAR_SIZE * scale ); + love.graphics.setFont( DEFAULT_FONT ); + end + + love.graphics.setColor( 255, 255, 255, 255 ); + end + end + + --- + -- Updates the author. + -- This function checks how much time has passed since the author last was + -- active. If it was inactive too long it starts fading out and eventually + -- is deactivated. It can be reactivated via reactivate(). + -- @param dt (number) The delta time between frames. + -- @param cameraRotation (number) The camera's rotation. + -- + function self:update( dt, cameraRotation ) + if active then + move( dt ); + + -- Fade out the author after it has been inactive for too long. + if inactivity > INACTIVITY_TIMER then + avatarAlpha = Utility.clamp( 0, avatarAlpha - dt * FADE_FACTOR, 255 ); + linkAlpha = Utility.clamp( 0, linkAlpha - dt * FADE_FACTOR, 255 ); + end + + -- Stop the author's movement after a short inactivity. + if inactivity > MOVEMENT_TIMER then + accX, accY = 0, 0; + end + + -- Deactivate the author when it becomes fully invisible. + if avatarAlpha <= 0 then + deactivate(); + end + + spritebatch:setColor( color.r, color.g, color.b, avatarAlpha ); + spritebatch:add( posX, posY, -cameraRotation, AVATAR_SIZE / aw, AVATAR_SIZE / ah, aw * 0.5, ah * 0.5 ); + + inactivity = inactivity + dt; + end + end + + --- + -- Adds a link to the author. + -- This represents a file the author has either added, modified or deleted. + -- @param file (table) The file to link to. + -- @param modifier (string) The kind of modifier used on the file. + -- + function self:addLink( file, modifier ) + reactivate(); + links[#links + 1] = { file = file, mod = modifier }; + + local dx, dy = posX - file:getX(), posY - file:getY(); + local distance = math.sqrt( dx * dx + dy * dy ); + dx = dx / distance; + dy = dy / distance; + + local strength = FORCE_SPRING * distance; + applyForce( dx * strength, dy * strength ); + end + + --- + -- Removes an old set of links by allocating a new table. + -- + function self:resetLinks() + links = {}; + end + + return self; +end + +-- ------------------------------------------------ +-- Return Module +-- ------------------------------------------------ + +return Author; diff --git a/src/authors/AuthorManager.lua b/src/authors/AuthorManager.lua new file mode 100644 index 0000000..af8ffe7 --- /dev/null +++ b/src/authors/AuthorManager.lua @@ -0,0 +1,154 @@ +local Resources = require( 'src.Resources' ); +local Author = require( 'src.authors.Author' ); +local Messenger = require( 'src.messenger.Messenger' ); + +-- ------------------------------------------------ +-- Constants +-- ------------------------------------------------ + +local EVENT = require('src.messenger.Event'); +local AVATAR_SPRITE = Resources.loadImage( 'avatar.png' ); + +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ + +local AuthorManager = {}; + +-- ------------------------------------------------ +-- Private Class Variables +-- ------------------------------------------------ + +local authors; +local aliases; +local addresses; +local showIcons; +local showLabels; +local spritebatch; + +local activeAuthor; + +local graphCenterX, graphCenterY; + +-- ------------------------------------------------ +-- Public Functions +-- ------------------------------------------------ + +--- +-- Initialises the AuthorManager. +-- @param naliases (table) Aliases used to replace author names. +-- @param nShowIcons (boolean) Wether to hide or show the author icons. +-- @param nShowLabels (boolean) Wether to hide or show the labels below each author. +-- +function AuthorManager.init( naliases, nShowIcons, nShowLabels ) + -- Set up the table to store all authors. + authors = {}; + + addresses = {}; + aliases = naliases; + + showIcons = nShowIcons; + showLabels = nShowLabels; + + graphCenterX, graphCenterY = 0, 0; + + spritebatch = love.graphics.newSpriteBatch( AVATAR_SPRITE, 1000, 'stream' ); +end + +--- +-- Draws a list of all authors working on the project. +-- @param rotation (number) The camera's current rotation. +-- @param scale (number) The camera's current scale. +-- +function AuthorManager.draw( rotation, scale ) + if showIcons then + for _, author in pairs( authors ) do + author:draw( rotation, scale, showLabels ); + end + love.graphics.draw( spritebatch ); + end +end + +--- +-- Updates all authors. +-- @param dt (number) The delta time between frames. +-- +function AuthorManager.update( dt, cameraRotation ) + spritebatch:clear(); + for _, author in pairs( authors ) do + author:update( dt, cameraRotation ); + end +end + +--- +-- Adds a link from the current author to a file. +-- @param file (table) The file to link to. +-- @param modifier (string) The kind of modifier used on the file. +-- +function AuthorManager.addFileLink( file, modifier ) + activeAuthor:addLink( file, modifier ) +end + +--- +-- Sets the author of the currently processed commit to be the active author. +-- @param email (string) The email adress of the author to set. +-- @param name (string) The name of the author to set. +-- +function AuthorManager.setCommitAuthor( email, name ) + -- Reset the previous author. + if activeAuthor then activeAuthor:resetLinks() end + + -- Check if we already have an alias or a name for this email address. If + -- not we use the author's name. + local nickname = aliases[email] or addresses[email] or name; + + -- If we don't have an author for that name yet, we create it. + if not authors[nickname] then + addresses[email] = name; -- Store this name as the default for this email address. + authors[nickname] = Author.new( nickname, AVATAR_SPRITE, spritebatch, graphCenterX, graphCenterY ); + end + + activeAuthor = authors[nickname]; +end + +--- +-- Toggles the visibility of name labels. +-- +function AuthorManager.toggleLabels() + showLabels = not showLabels; +end + +--- +-- Toggles the visibility of author icons. +-- +function AuthorManager.toggleIcons() + showIcons = not showIcons; +end + +--- +-- Sets the graph's center coordinates. +-- @param ncx (number) The graph's center along the x-axis. +-- @param ncy (number) The graph's center along the y-axis. +-- +function AuthorManager.setGraphCenter( ncx, ncy ) + graphCenterX, graphCenterY = ncx, ncy; +end + + +Messenger.observe( EVENT.NEW_COMMIT, function( ... ) + AuthorManager.setCommitAuthor( ... ); +end) + +Messenger.observe( EVENT.GRAPH_UPDATE_FILE, function( ... ) + AuthorManager.addFileLink( ... ); +end) + +Messenger.observe( EVENT.GRAPH_UPDATE_CENTER, function( ... ) + AuthorManager.setGraphCenter( ... ); +end) + +-- ------------------------------------------------ +-- Return Module +-- ------------------------------------------------ + +return AuthorManager; diff --git a/src/conf/ConfigReader.lua b/src/conf/ConfigReader.lua index 350f758..fcb0972 100644 --- a/src/conf/ConfigReader.lua +++ b/src/conf/ConfigReader.lua @@ -15,7 +15,6 @@ local MISSING_VALUE_WARNING = 'Seems like the loaded configuration file is mis -- Local Variables -- ------------------------------------------------ -local default; local config; -- ------------------------------------------------ @@ -26,109 +25,101 @@ local config; -- Checks if the settings file exists on the user's system. -- local function hasConfigFile() - return love.filesystem.isFile(FILE_NAME); + return love.filesystem.isFile( FILE_NAME ); end --- -- Creates a new settings file on the user's system based on the default template. --- @param name - The file name to use for the config file. --- @param default - The path to the default settings file. +-- @param filename (string) The file name to use for the config file. +-- @param templatePath (string) The path to the template settings file. -- -local function createConfigFile(name, default) - for line in love.filesystem.lines(default) do - love.filesystem.append(name, line .. '\r\n'); +local function createConfigFile( filename, templatePath ) + for line in love.filesystem.lines( templatePath ) do + love.filesystem.append( filename, line .. '\r\n' ); end end --- -- Tries to transform strings to their actual types if possible. --- @param value - The value to transform. +-- @param value (string) The value to transform. +-- @return (various) The actual type of the setting. -- -local function toType(value) - value = value:match('^%s*(.-)%s*$'); +local function toType( value ) + value = value:match( '^%s*(.-)%s*$' ); if value == 'true' then return true; elseif value == 'false' then return false; - elseif tonumber(value) then - return tonumber(value); + elseif tonumber( value ) then + return tonumber( value ); else return value; end end -local function loadFile(file) - local config = {}; +--- +-- Parses the config file and stores the values in a table. +-- @param filePath (string) The path to the config file. +-- @return (table) The loaded config stored in a table. +-- +local function loadFile( filePath ) + local loadedConfig = {}; local section; - for line in love.filesystem.lines(file) do - if line == '' or line:find(';') == 1 then + for line in love.filesystem.lines( filePath ) do + if line == '' or line:find( ';' ) == 1 then -- Ignore comments and empty lines. - elseif line:match('^%[(%w*)%]$') then + elseif line:match( '^%[(%w*)%]$' ) then -- Create a new section. - local header = line:match('^%[(%w*)%]$'); - config[header] = {}; - section = config[header]; + local header = line:match( '^%[(%w*)%]$' ); + loadedConfig[header] = {}; + section = loadedConfig[header]; else -- Store values in the section. - local key, value = line:match('^([%g]+)%s-=%s-(.+)'); + local key, value = line:match( '^([%g]+)%s-=%s-(.+)' ); -- Store multiple values in a table. - if value and value:find(',') then + if value and value:find( ',' ) then section[key] = {}; - for val in value:gmatch('[^, ]+') do - section[key][#section[key] + 1] = toType(val); + for val in value:gmatch( '[^, ]+' ) do + section[key][#section[key] + 1] = toType( val ); end elseif value then - section[key] = toType(value); + section[key] = toType( value ); end end end - return config; + return loadedConfig; end --- -- Validates a loaded config file by comparing it to the default config file. -- It checks if the file contains all the necessary sections and values. If it --- doesn't a warning is displayed and the default config will be used. --- @param default - The default file to use for comparison. --- @param loaded - The settings file loaded from the user's system. +-- doesn't, a warning is displayed and the default config will be used. +-- @param default (table) The default config file to use for comparison. -- -local function validateFile(default, loaded) - print('Validating configuration file ... '); - for skey, section in pairs(default) do +local function validateFile( default ) + print( 'Validating configuration file ... ' ); + for skey, section in pairs( default ) do -- If loaded config file doesn't contain section return default. - if loaded[skey] == nil then - love.window.showMessageBox(INVALID_CONFIG_HEADER, string.format(MISSING_SECTION_WARNING, skey), 'warning', false); + if config[skey] == nil then + love.window.showMessageBox( INVALID_CONFIG_HEADER, string.format( MISSING_SECTION_WARNING, skey ), 'warning', false ); return default; end -- If the loaded config file is missing a value, display warning and return default. - if type(section) == 'table' then - for vkey, _ in pairs(section) do - if loaded[skey][vkey] == nil then - love.window.showMessageBox(INVALID_CONFIG_HEADER, string.format(MISSING_VALUE_WARNING, vkey, skey), 'warning', false); + if type( section ) == 'table' then + for vkey, _ in pairs( section ) do + if config[skey][vkey] == nil then + love.window.showMessageBox( INVALID_CONFIG_HEADER, string.format( MISSING_VALUE_WARNING, vkey, skey ), 'warning', false ); return default; end end end end - - print('Done!'); - return loaded; -end - ---- --- Replaces backslashes in paths with forwardslashes. --- @param The loaded config. --- -local function validateRepositoryPaths(config) - for project, path in pairs(config.repositories) do - config.repositories[project] = path:gsub('\\+', '/'); - end - return config; + print( 'Done!' ); end -- ------------------------------------------------ @@ -136,53 +127,21 @@ end -- ------------------------------------------------ function ConfigReader.init() - default = loadFile(TEMPLATE_PATH); + local default = loadFile( TEMPLATE_PATH ); if not hasConfigFile() then - createConfigFile(FILE_NAME, TEMPLATE_PATH); + createConfigFile( FILE_NAME, TEMPLATE_PATH ); end -- If the config hasn't been loaded yet, load and validate it. if not config then - config = loadFile(FILE_NAME); - config = validateFile(default, config); - config = validateRepositoryPaths(config); + config = loadFile( FILE_NAME ); + validateFile( default ); end return config; end -function ConfigReader.removeTmpFiles() - print('Removing temporary files...'); - local function recursivelyDelete(item, depth) - local ws = ''; - for _ = 1, depth do - ws = ws .. ' '; - end - print(ws .. item); - if love.filesystem.isDirectory(item) then - for _, child in pairs(love.filesystem.getDirectoryItems(item)) do - recursivelyDelete(item .. '/' .. child, depth + 1); - love.filesystem.remove(item .. '/' .. child); - end - elseif love.filesystem.isFile(item) then - love.filesystem.remove(item); - end - love.filesystem.remove(item); - end - - recursivelyDelete('tmp', 0); - print('... Done!'); -end - --- ------------------------------------------------ --- Getters --- ------------------------------------------------ - -function ConfigReader.getConfig(section) - return config[section]; -end - -- ------------------------------------------------ -- Return Module -- ------------------------------------------------ diff --git a/src/conf/RepositoryHandler.lua b/src/conf/RepositoryHandler.lua new file mode 100644 index 0000000..514f9f9 --- /dev/null +++ b/src/conf/RepositoryHandler.lua @@ -0,0 +1,117 @@ +local FILE_NAME = 'repositories.cfg'; + +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ + +local RepositoryHandler = {}; + +-- ------------------------------------------------ +-- Local Variables +-- ------------------------------------------------ + +local repositories = {}; + +-- ------------------------------------------------ +-- Local Functions +-- ------------------------------------------------ + +--- +-- Checks if the repository file exists on the user's system. +-- @return (boolean) Wether the file exists or not. +-- +local function hasRepositoryFile() + return love.filesystem.isFile( FILE_NAME ); +end + +--- +-- Loads the repository file. +-- @return (table) The repository file as a lua table. +-- +local function load() + for line in love.filesystem.lines( FILE_NAME ) do + if line == '' or line:find( ';' ) == 1 then + -- Ignore comments and empty lines. + else + -- Store values in the section. + local key, value = line:match( '^%s*([%g%s]*%g)%s*=%s*(.+)$' ); + repositories[key] = value; + end + end +end + +--- +-- Saves the repositories-table to the hard disk. +-- +local function save() + local file = love.filesystem.newFile( FILE_NAME, 'w' ); + + file:write( '; This file keeps track of the paths where the repositories\r\n' ); + file:write( '; are located on the user\'s hard drive.\r\n' ); + file:write( '; Name = Path\r\n' ); + for key, value in pairs( repositories ) do + local line = string.format( '%s = %s\r\n', key, value ); + file:write( line ); + end + file:close(); +end + +-- ------------------------------------------------ +-- Public Functions +-- ------------------------------------------------ + +--- +-- Initialises the handler and creates an empty repository file on the hard +-- disk if it doesn't exist already. +-- +function RepositoryHandler.init() + if not hasRepositoryFile() then + save(); -- Create empty file. + end + load(); +end + +--- +-- Adds a new repository to the list of repositories and saves the data to the +-- hard drive. +-- @param name (string) The repository's name. +-- @param path (string) The repository's location. +-- +function RepositoryHandler.add( name, path ) + if repositories[name] then + name = name .. '_' .. os.time(); + end + repositories[name] = path; + save(); +end + +--- +-- Removes a repository from the list of repositories and saves the data to the +-- hard drive. +-- @param name (string) The repository's name. +-- +function RepositoryHandler.remove( name ) + repositories[name] = nil; + save(); +end + +--- +-- Returns the repositories-table. +-- @return (table) The repositories. +-- +function RepositoryHandler.getRepositories() + return repositories; +end + +--- +-- Returns true if at least one repository exists. +-- @return (boolean) Wether or not a repository exists. +-- +function RepositoryHandler.hasRepositories() + for _, v in pairs( repositories ) do + if v then return true end + end + return false; +end + +return RepositoryHandler; diff --git a/src/git/GitHandler.lua b/src/git/GitHandler.lua new file mode 100644 index 0000000..6c04c03 --- /dev/null +++ b/src/git/GitHandler.lua @@ -0,0 +1,78 @@ +local GitHandler = {}; + +-- ------------------------------------------------ +-- Constants +-- ------------------------------------------------ + +local GIT_VERSION_COMMAND = 'git version'; +local GIT_STATUS_COMMAND = 'git -C "%s" status'; +local GIT_LOG_COMMAND = 'git -C "%s" log --reverse --numstat --pretty=format:"info: %%an|%%ae|%%ct" --name-status --no-merges'; +local GIT_COUNT_COMMAND = 'git -C "%s" rev-list HEAD --count'; + +local LOG_FOLDER = 'logs/'; +local LOG_FILE = '/.log'; + +-- ------------------------------------------------ +-- Public Functions +-- ------------------------------------------------ + +--- +-- Creates a git log if git is available and no log has been +-- created in the target folder yet. +-- @param projectname (string) The name under which to store the git log. +-- @param path (string) The path pointing to the repository. +-- +function GitHandler.createGitLog( projectname, path ) + love.filesystem.createDirectory( LOG_FOLDER .. projectname ); + local handle = io.popen( string.format( GIT_LOG_COMMAND, path )); + love.filesystem.write( LOG_FOLDER .. projectname .. LOG_FILE, handle:read( '*all' )); + handle:close(); +end + +--- +-- Checks if git is available on the system. +-- @return (boolean) Returns true if git was found on the user's system. +-- +function GitHandler.isGitAvailable() + local handle = io.popen( GIT_VERSION_COMMAND ); + local result = handle:read( '*a' ); + handle:close(); + return result:find( GIT_VERSION_COMMAND ); +end + +--- +-- Checks if a path points to a valid git repository. +-- @param path (string) The path to check. +-- @return (boolean) Returns true if the the path points to a git repository. +-- +function GitHandler.isGitRepository( path ) + local handle = io.popen( string.format( GIT_STATUS_COMMAND, path )); + local result = handle:read( '*a' ); + handle:close(); + return result ~= ''; +end + +--- +-- Checks wether a repository needs to be updated. This is the case if the +-- total amount of commits has changed since the last time LoGiVi was started. +-- @param path (string) A path pointing to a repository. +-- @param totalCommits (number) The total amount of commits to check for. +-- @return (boolean) Returns true if the total amount of commits has changed. +-- +function GitHandler.isRepositoryUpToDate( path, totalCommits ) + return GitHandler.getTotalCommits( path ) == tonumber( totalCommits ); +end + +--- +-- Returns the total amount of commits in the specified repository. +-- @param path (string) The path pointing to a repository. +-- @Returns (number) The total amount of commits in the repository. +-- +function GitHandler.getTotalCommits( path ) + local handle = io.popen( string.format( GIT_COUNT_COMMAND, path )); + local totalCommits = handle:read( '*a' ):gsub( '[%s]+', '' ); + handle:close(); + return tonumber( totalCommits ); +end + +return GitHandler; diff --git a/src/graph/File.lua b/src/graph/File.lua index 2c1cc15..ec3ce4a 100644 --- a/src/graph/File.lua +++ b/src/graph/File.lua @@ -1,3 +1,9 @@ +local Utility = require( 'src.Utility' ); + +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ + local File = {}; -- ------------------------------------------------ @@ -18,7 +24,15 @@ local MOD_COLOR = { -- Constructor -- ------------------------------------------------ -function File.new(posX, posY, defaultColor, extension) +--- +-- Creates a new File object. +-- @param parentX (number) The position of the file's parent node along the x-axis. +-- @param parentY (number) The position of the file's parent node along the y-axis. +-- @param defaultColor (table) A table containing the RGB values for this file type. +-- @param extension (string) The file's extension. +-- @return (File) A new file instance. +-- +function File.new( parentX, parentY, defaultColor, extension ) local self = {}; local state; @@ -35,24 +49,17 @@ function File.new(posX, posY, defaultColor, extension) -- Private Functions -- ------------------------------------------------ - --- - -- Linear interpolation between a and b. - -- - local function lerp(a, b, t) - return a + (b - a) * t; - end - --- -- Lerps the file from its current offset position to the target offset. -- This adds a nice animation effect when files are rearranged around their -- parent nodes. - -- @param dt - The delta time between frames. - -- @param tarX - The target offset on the x-axis. - -- @param tarY - The target offset on the y-axis. + -- @param dt (number) The delta time between frames. + -- @param tarX (number) The target offset on the x-axis. + -- @param tarY (number) The target offset on the y-axis. -- - local function animate(dt, tarX, tarY) - currentOffsetX = lerp(currentOffsetX, tarX, dt * ANIM_TIMER); - currentOffsetY = lerp(currentOffsetY, tarY, dt * ANIM_TIMER); + local function animate( dt, tarX, tarY ) + currentOffsetX = Utility.lerp( currentOffsetX, tarX, dt * ANIM_TIMER ); + currentOffsetY = Utility.lerp( currentOffsetY, tarY, dt * ANIM_TIMER ); end -- ------------------------------------------------ @@ -60,21 +67,21 @@ function File.new(posX, posY, defaultColor, extension) -- ------------------------------------------------ --- - -- If the file is marked as modified the color will be lerped from - -- the modified color to the default file color. - -- @param dt + -- If the file is marked as modified the color will be lerped from the + -- modified color to the default file color. + -- @param dt (number) The delta time between frames. -- - function self:update(dt) - animate(dt, targetOffsetX, targetOffsetY); + function self:update( dt ) + animate( dt, targetOffsetX, targetOffsetY ); -- Slowly change the color from the modified color back to the default. - currentColor.r = lerp(currentColor.r, defaultColor.r, dt * MOD_TIMER); - currentColor.g = lerp(currentColor.g, defaultColor.g, dt * MOD_TIMER); - currentColor.b = lerp(currentColor.b, defaultColor.b, dt * MOD_TIMER); + currentColor.r = Utility.lerp( currentColor.r, defaultColor.r, dt * MOD_TIMER ); + currentColor.g = Utility.lerp( currentColor.g, defaultColor.g, dt * MOD_TIMER ); + currentColor.b = Utility.lerp( currentColor.b, defaultColor.b, dt * MOD_TIMER ); -- Slowly fade out the file when it has been marked for deletion. if state == 'del' then - currentColor.a = math.max(0, math.min(currentColor.a - FADE_TIMER, 255)); + currentColor.a = Utility.clamp( 0, currentColor.a - FADE_TIMER, 255 ); if currentColor.a == 0 then state = 'dead'; end @@ -84,9 +91,9 @@ function File.new(posX, posY, defaultColor, extension) --- -- Sets the state of the file and changes the current color to a specific -- color based on the used modifier. - -- @param mod - The modifier used on the file. + -- @param mod (string) The modifier used on the file. -- - function self:setState(mod) + function self:setState( mod ) state = mod; currentColor.r = MOD_COLOR[mod].r; @@ -102,22 +109,25 @@ function File.new(posX, posY, defaultColor, extension) --- -- Returns the real position of the node on the x-axis. -- This is the sum of the parent-node's position and the offset of the file. + -- @return (number) The position of the file along the x-axis. -- function self:getX() - return posX + currentOffsetX; + return parentX + currentOffsetX; end --- -- Returns the real position of the node on the y-axis. -- This is the sum of the parent-node's position and the offset of the file. + -- @return (number) The position of the file along the y-axis. -- function self:getY() - return posY + currentOffsetY; + return parentY + currentOffsetY; end --- - -- Returns the current color of the file. - -- The table uses rgba keys to store the color. + -- Returns the current color of the file. The table uses rgba keys to store + -- the color. + -- @return (table) A table containing the RGB values of the file. -- function self:getColor() return currentColor; @@ -125,6 +135,7 @@ function File.new(posX, posY, defaultColor, extension) --- -- Returns the extension of the file as a string. + -- @return (string) The extension of the file. -- function self:getExtension() return extension; @@ -132,6 +143,7 @@ function File.new(posX, posY, defaultColor, extension) --- -- Returns true if the file is marked as dead. + -- @return (boolean) True if the file is marked as dead. -- function self:isDead() return state == 'dead'; @@ -144,20 +156,20 @@ function File.new(posX, posY, defaultColor, extension) --- -- Sets the target offset of the file from its parent node. -- This distance is used to plot all the files in a circle around the node. - -- @param ox - The offset on the x-axis. - -- @param oy - The offset on the y-axis. + -- @param ox (number) The offset from the parent along the x-axis. + -- @param oy (number) The offset from the parent along the y-axis. -- - function self:setOffset(ox, oy) + function self:setOffset( ox, oy ) targetOffsetX, targetOffsetY = ox, oy; end --- -- Sets the position of the parent node on which the file is located. - -- @param nx - The new position on the x-axis. - -- @param ny - The new position on the y-axis. + -- @param nx (number) The position of the parent along the x-axis. + -- @param ny (number) The position of the parent along the y-axis. -- - function self:setPosition(nx, ny) - posX, posY = nx, ny; + function self:setPosition( nx, ny ) + parentX, parentY = nx, ny; end return self; diff --git a/src/graph/Graph.lua b/src/graph/Graph.lua index def9e00..bae7e4c 100644 --- a/src/graph/Graph.lua +++ b/src/graph/Graph.lua @@ -1,5 +1,8 @@ local Node = require('src.graph.Node'); local Resources = require('src.Resources'); +local GraphLibrary = require('lib.graphoon.Graphoon').Graph; +local Messenger = require('src.messenger.Messenger'); +local Utility = require( 'src.Utility' ); -- ------------------------------------------------ -- Module @@ -11,228 +14,213 @@ local Graph = {}; -- Constants -- ------------------------------------------------ -local ROOT_FOLDER = 'root'; -local MOD_ADD = 'A'; -local MOD_COPY = 'C'; -local MOD_DELETE = 'D'; -local MOD_MODIFY = 'M'; -local MOD_RENAME = 'R'; -local MOD_CHANGE = 'T'; -local MOD_UNMERGE = 'U'; -local MOD_UNKNOWN = 'X'; -local MOD_BROKEN_PAIRING = 'B'; - -local EVENT_UPDATE_DIMENSIONS = 'GRAPH_UPDATE_DIMENSIONS'; -local EVENT_UPDATE_CENTER = 'GRAPH_UPDATE_CENTER'; -local EVENT_UPDATE_FILE = 'GRAPH_UPDATE_FILE'; +local EVENT = require('src.messenger.Event'); --- ------------------------------------------------ --- Local Variables --- ------------------------------------------------ +local ROOT_FOLDER = ''; +local MOD_ADD = 'A'; +local MOD_DELETE = 'D'; +local MOD_MODIFY = 'M'; + +local LABEL_FONT = Resources.loadFont( 'SourceCodePro-Medium.otf', 20 ); +local DEFAULT_FONT = Resources.loadFont( 'default', 12 ); +local FILE_SPRITE = Resources.loadImage( 'file.png' ); -local fileSprite = Resources.loadImage('file.png'); -local spritebatch = love.graphics.newSpriteBatch(fileSprite, 10000, 'stream'); +local EDGE_COLOR = { 60, 60, 60, 255 }; -- ------------------------------------------------ -- Constructor -- ------------------------------------------------ -function Graph.new(ewidth, showLabels) +--- +-- Creates a new graph object. +-- @param edgeWidth (number) The width of the connecting edges. +-- @param showLabels (boolean) Wether or not to show labels. +-- @return (Graph) A new instance of the graph. +-- +function Graph.new( edgeWidth, showLabels ) local self = {}; - local observers = {}; + local spritebatch = love.graphics.newSpriteBatch( FILE_SPRITE, 10000, 'stream' ); - local nodes = { [ROOT_FOLDER] = Node.new(nil, ROOT_FOLDER, ROOT_FOLDER, 300, 200, spritebatch); }; - local root = nodes[ROOT_FOLDER]; + -- Create a new graph class. + GraphLibrary.setNodeClass( Node ); -- Use custom class for Nodes. + local graph = GraphLibrary.new(); + graph:addNode( ROOT_FOLDER, love.graphics.getWidth() * 0.5, love.graphics.getHeight() * 0.5, false, nil, spritebatch, ROOT_FOLDER ); - local minX, maxX, minY, maxY = root:getX(), root:getX(), root:getY(), root:getY(); + local subscription; -- ------------------------------------------------ -- Local Functions -- ------------------------------------------------ --- - -- Notify observers about the event. - -- @param event - -- @param ... - -- - local function notify(event, ...) - for i = 1, #observers do - observers[i]:receive(event, ...); - end - end - - --- - -- @param minX - The current minimum x position. - -- @param maxX - The current maximum y position. - -- @param minY - The current minimum x position. - -- @param maxY - The current maximum y position. - -- @param nx - The new x position to check. - -- @param ny - The new y position to check. + -- Spawns a new node. + -- @param name (string) The node's name based on the folder's name. + -- @param id (string) The node's unqiue id based on the folder's full path. + -- @param parent (Node) The parent of the node to spawn. + -- @param parentID (string) The parent's id. + -- @return (Node) The newly spawned node. -- - local function updateBoundaries(minX, maxX, minY, maxY, radius, nx, ny) - return math.min(nx - radius, minX), math.max(nx + radius, maxX), math.min(ny - radius, minY), math.max(ny + radius, maxY); + local function spawnNode( name, id, parent, parentID ) + local parentX, parentY = parent:getPosition(); + local offsetX = love.math.random( 100 ) * Utility.randomSign(); + local offsetY = love.math.random( 100 ) * Utility.randomSign(); + return graph:addNode( id, parentX + offsetX, parentY + offsetY, false, parentID, spritebatch, name ); end --- - -- Returns the center of the graph. The center is calculated - -- by forming a rectangle that encapsulates all nodes and then - -- dividing its sides by two. + -- Removes a node from the graph. + -- @param node (Node) The node to remove. -- - local function updateCenter() - return minX + (maxX - minX) * 0.5, minY + (maxY - minY) * 0.5; - end - - --- - -- Creates a new node and stores it in our list, using the name - -- as the identifier or returns an already existing node. - -- @param parentPath - -- @param nodePath - -- @param x - -- @param y - -- - local function addNode(parentPath, nodePath, folder) - if not nodes[nodePath] then - local parent = nodes[parentPath]; - nodes[nodePath] = Node.new(parent, nodePath, folder, - parent:getX() + love.math.random(5, 40) * (love.math.random(0, 1) == 0 and -1 or 1), - parent:getY() + love.math.random(5, 40) * (love.math.random(0, 1) == 0 and -1 or 1), - spritebatch); - parent:addChild(nodePath, nodes[nodePath]); + local function removeNode( node ) + local parent = graph:getNode( node:getParent() ); + if parent then + parent:decrementChildCount(); + graph:removeNode( node ); end - return nodes[nodePath], nodePath; end --- - -- Returns the node of the specified path if it already exists. - -- If the string is empty it points to the root node. If the path - -- doesn't already belong to a node it creates nodes for each sub- - -- folder in the path until it is done with the whole path. Then - -- it returns the node for the the path. - -- @param path + -- Creates all nodes belonging to a path if they don't exist yet. + -- @param path (string) The path to resolve. + -- @return (Node) The last node in the path. -- - local function getNode(path) - if nodes[path] then - return nodes[path]; - else - local node; - local ppath = ROOT_FOLDER; - for part in path:gmatch('[^/]+') do - if part ~= ROOT_FOLDER then - node, ppath = addNode(ppath, ppath .. '/' .. part, part); - end + local function createNodes( path ) + local parentID = ROOT_FOLDER; + for folder in path:gmatch('[^/]+') do + local nodeID = parentID .. '/' .. folder; + -- Create the node if doesn't exist in the graph yet. + if not graph:hasNode( nodeID ) then + local parentNode = graph:getNode( parentID ); + local newNode = spawnNode( folder, nodeID, parentNode, parentID ); + graph:connectNodes( parentNode, newNode ); + parentNode:incrementChildCount(); end - return node; + parentID = nodeID; end + return graph:getNode( path ); end --- - -- Checks if a node is dead. A node is considered dead if it doesn't contain - -- any files and doesn't link to any other nodes except for its own parent. - -- @param node - The node to check. + -- Returns the node a path is pointng to. If the node doesn't exist in the + -- graph yet, it will be created. + -- @param path (string) The path to resolve. + -- @return (Node) The last node in the path. -- - local function removeDeadNode(node) - if node:isDead() then - -- print('DEL node [' .. path .. ']'); - local parent = node:getParent(); - if parent then - local path = node:getPath(); - parent:removeChild(path); - nodes[path] = nil; - end + local function resolvePath( path ) + if graph:hasNode( path ) then + return graph:getNode( path ); end + return createNodes( path ); end --- -- This function will take a git modifier and apply it to a file. - -- If it encounters the 'A' modifier it will create a file at the - -- specified path. If it encounters the 'D' modifier it will remove - -- the file from the path. Nodes will be created and removed based - -- along the way. - -- @param modifier - -- @param path - -- @param filename + -- @param modifier (string) The modifier to apply to the file. + -- @param path (string) The path pointing to the modified file. + -- @param filename (string) The file's name. + -- @param extension (string) The file's extension. + -- @param mode (string) The current play mode. -- - local function applyGitModifier(modifier, path, filename, extension, mode) - local targetNode = getNode(path); + local function applyGitModifier( modifier, path, filename, extension, mode ) + local targetNode = resolvePath( path ); local modifiedFile; if modifier == MOD_ADD then - modifiedFile = targetNode:addFile(filename, extension); + modifiedFile = targetNode:addFile( filename, extension ); elseif modifier == MOD_DELETE then if mode == 'normal' then - modifiedFile = targetNode:markFileForDeletion(filename); + modifiedFile = targetNode:markFileForDeletion( filename ); else - modifiedFile = targetNode:removeFile(filename, extension); + modifiedFile = targetNode:removeFile( filename, extension ); end elseif modifier == MOD_MODIFY then - modifiedFile = targetNode:modifyFile(filename); + modifiedFile = targetNode:modifyFile( filename ); end -- We only notify observers if the graph isn't modifed in fast forward / rewind mode. if mode == 'normal' and modifiedFile then - notify(EVENT_UPDATE_FILE, modifiedFile, modifier); + Messenger.publish( EVENT.GRAPH_UPDATE_FILE, modifiedFile, modifier ); end end + --- + -- Small hacky fix to make sure empty nodes still have their labels at the + -- correct position. This simply takes the node's radius and checks if it + -- is 0 (if it has at least one file) or -24 (if it is empty). + -- @see https://github.com/rm-code/logivi/issues/69 + -- @param node (Node) The node for which to generate the radius. + -- @return (number) Returns the position at which the node's label should be placed. + -- + local function getLabelRadius( node ) + local radius = node:getRadius(); + return ( radius == 0 or radius == -24 ) and 12 or radius; + end + -- ------------------------------------------------ -- Public Functions -- ------------------------------------------------ - function self:draw(camrot, camscale) - root:draw(ewidth); - love.graphics.draw(spritebatch); - - if showLabels then - root:drawLabel(camrot, camscale); - end + --- + -- Draws the graph. + -- @param camrot (number) The current camera rotation. + -- @param camscale (number) The current camera scale. + -- + function self:draw( camrot, camscale ) + graph:draw( function( node ) + if showLabels then + local x, y = node:getPosition(); + local radius = getLabelRadius( node ); + love.graphics.setFont( LABEL_FONT ); + love.graphics.print( node:getName(), x, y, -camrot, 1 / camscale, 1 / camscale, -radius * camscale, -radius * camscale ); + love.graphics.setFont( DEFAULT_FONT ); + end + end, + function( edge ) + love.graphics.setColor( EDGE_COLOR ); + love.graphics.setLineWidth( edgeWidth ); + love.graphics.line( edge.origin:getX(), edge.origin:getY(), edge.target:getX(), edge.target:getY() ); + love.graphics.setLineWidth( 1 ); + love.graphics.setColor( 255, 255, 255, 255 ); + end); + love.graphics.draw( spritebatch ); end - function self:update(dt) - minX, maxX, minY, maxY = root:getX(), root:getX(), root:getY(), root:getY(); - + --- + -- Updates the graph. + -- @param dt (number) The delta time passed since the last frame. + -- + function self:update( dt ) spritebatch:clear(); - for _, nodeA in pairs(nodes) do - for _, nodeB in pairs(nodes) do - nodeA:calculateForces(nodeB); + graph:update( dt, function( node ) + node:update( dt ) + if node:isDead() then + removeNode( node ); end + end); - -- Remove the node if it doesn't contain files and only - -- has a link to its parent. - removeDeadNode(nodeA); - - minX, maxX, minY, maxY = updateBoundaries(minX, maxX, minY, maxY, nodeA:getRadius(), nodeA:update(dt)); - end - - notify(EVENT_UPDATE_CENTER, updateCenter()); - notify(EVENT_UPDATE_DIMENSIONS, minX, maxX, minY, maxY); + Messenger.publish( EVENT.GRAPH_UPDATE_CENTER, graph:getCenter() ); + Messenger.publish( EVENT.GRAPH_UPDATE_DIMENSIONS, graph:getBoundaries() ); end --- - -- Activate / Deactivate folder labels. + -- Toggles folder labels. -- function self:toggleLabels() showLabels = not showLabels; end - --- - -- Register an observer. - -- @param observer - -- - function self:register(observer) - observers[#observers + 1] = observer; + function self:reset() + Messenger.remove( subscription ); end - --- - -- Receives a notification from an observable. - -- @param event - -- @param ... - -- - function self:receive(event, ...) - if event == 'LOGREADER_CHANGED_FILE' then - applyGitModifier(...) - end - end + -- ------------------------------------------------ + -- Observed Events + -- ------------------------------------------------ + + subscription = Messenger.observe( EVENT.LOGREADER_CHANGED_FILE, function( ... ) + applyGitModifier( ... ); + end) return self; end diff --git a/src/graph/Node.lua b/src/graph/Node.lua index c2cf971..8f9d6d2 100644 --- a/src/graph/Node.lua +++ b/src/graph/Node.lua @@ -1,4 +1,4 @@ -local Resources = require('src.Resources'); +local GraphLibraryNode = require('lib.graphoon.Graphoon').Node; local FileManager = require('src.FileManager'); local File = require('src.graph.File'); @@ -12,232 +12,170 @@ local Node = {}; -- Constants -- ------------------------------------------------ -local FORCE_MAX = 4; - local SPRITE_SIZE = 24; local SPRITE_SCALE_FACTOR = SPRITE_SIZE / 256; local SPRITE_OFFSET = 128; local MIN_ARC_SIZE = SPRITE_SIZE; -local FORCE_SPRING = -0.001; -local FORCE_CHARGE = 1000000; - -local LABEL_FONT = Resources.loadFont('SourceCodePro-Medium.otf', 20); -local DEFAULT_FONT = Resources.loadFont('default', 12); - -local DAMPING_FACTOR = 0.95; - -local EDGE_COLOR = { 60, 60, 60, 255 }; - -- ------------------------------------------------ -- Constructor -- ------------------------------------------------ -function Node.new(parent, path, name, x, y, spritebatch) - local self = {}; +--- +-- Creates a new node object. +-- @param id (string) The id to use for this node. +-- @param x (number) The position at which to spawn the node along the x-axis. +-- @param y (number) The position at which to spawn the node along the y-axis. +-- @param anchor (boolean) Wether the node is anchored or not. +-- @param parent (Node) The node's parent node. +-- @param spritebatch (SpriteBatch) The spritebatch to use for drawing the node's files. +-- @param name (string) The node's name. +-- @return (Node) A new node instance. +-- +function Node.new( id, x, y, anchor, parent, spritebatch, name ) + local self = GraphLibraryNode.new( id, x, y, anchor ); -- ------------------------------------------------ -- Local Variables -- ------------------------------------------------ - local children = {}; local childCount = 0; local files = {}; local fileCount = 0; - local speed = 64; - - local posX, posY = x, y; - local velX, velY = 0, 0; - local accX, accY = 0, 0; - local radius = 0; -- ------------------------------------------------ - -- Public Functions + -- Local Functions -- ------------------------------------------------ --- - -- Clamps a value to a certain range. - -- @param min - -- @param val - -- @param max - -- - local function clamp(min, val, max) - return math.max(min, math.min(val, max)); - end - - --- - -- Calculates the new xy-acceleration for this node. - -- The values are clamped to keep the graph from "exploding". - -- @param fx - The force to apply in x-direction. - -- @param fy - The force to apply in y-direction. + -- Calculates the arc between files on a layer for a certain angle. + -- @param layerRadius (number) The current layer's radius. + -- @param angle (number) The angle between files on the same layer. + -- @return (number) The arc at which to place a certain file around the node. -- - local function applyForce(fx, fy) - accX = clamp(-FORCE_MAX, accX + fx, FORCE_MAX); - accY = clamp(-FORCE_MAX, accY + fy, FORCE_MAX); + local function calcArc( layerRadius, angle ) + return math.pi * layerRadius * ( angle / 180 ); end --- - -- Calculates the arc for a certain angle. - -- @param radius - -- @param angle + -- Calculates how many layers we need and how many files can be placed on + -- each layer. This basically generates a blueprint of how the files need to + -- be arranged. + -- @param count (number) The total amount of files in this node. + -- @return (table) A table containing all layers around the node. + -- @return (number) The radius of the biggest layer. -- - local function calcArc(radius, angle) - return math.pi * radius * (angle / 180); - end - - --- - -- Calculates how many layers we need and how many files - -- can be placed on each layer. This basically generates a - -- blueprint of how the files need to be arranged. - -- - local function createOnionLayers(count) + local function createOnionLayers( count ) local fileCounter = 0; - local radius = -SPRITE_SIZE; -- Radius of the circle around the node. + local layerRadius = -SPRITE_SIZE; -- Radius of the circle around the node. local layers = { - { radius = radius, amount = fileCounter } + { radius = layerRadius, amount = fileCounter } }; - for i = 1, count do + for _ = 1, count do fileCounter = fileCounter + 1; -- Calculate the arc between the file nodes on the current layer. -- The more files are on it the smaller it gets. - local arc = calcArc(layers[#layers].radius, 360 / fileCounter); + local arc = calcArc( layers[#layers].radius, 360 / fileCounter ); -- If the arc is smaller than the allowed minimum we store the radius -- of the current layer and the number of nodes that can be placed -- on that layer and move to the next layer. if arc < MIN_ARC_SIZE then - radius = radius + SPRITE_SIZE; + layerRadius = layerRadius + SPRITE_SIZE; -- Create a new layer. - layers[#layers + 1] = { radius = radius, amount = 1 }; + layers[#layers + 1] = { radius = layerRadius, amount = 1 }; fileCounter = 1; else layers[#layers].amount = fileCounter; end end - return layers, radius; + return layers, layerRadius; + end + + --- + -- Calculates the new position of a file on its layer around the folder node. + -- @param fileNumber (number) The n-th file on the layer. + -- @param layerFileCount (number) The total amount of files on the same layer. + -- @param layerRadius (number) The radius of the layer. + -- @return (number) The position of the file around the node along the x-axis. + -- @return (number) The position of the file around the node along the y-axis. + -- + local function calculateFilePosition( fileNumber, layerFileCount, layerRadius ) + local angle = 360 / layerFileCount; + local slice = angle * ( fileNumber - 1 ) * ( math.pi / 180 ); + local fx = layerRadius * math.cos( slice ); + local fy = layerRadius * math.sin( slice ); + return fx, fy; end --- - -- Distributes files nodes evenly on a circle around the parent node. - -- @param files + -- Distributes files evenly on a circle around the parent node. + -- @param count (number) The total amount of files in this node. + -- @return (number) The radius of the biggest layer around the node. -- - local function plotCircle(files, count) + local function plotCircle( count ) -- Sort files based on their extension before placing them. local toSort = {}; - for _, file in pairs(files) do + for _, file in pairs( files ) do toSort[#toSort + 1] = { extension = file:getExtension(), file = file }; end - table.sort(toSort, function(a, b) + table.sort(toSort, function( a, b ) return a.extension > b.extension; end) -- Get a blueprint of how the file nodes need to be distributed amongst different layers. - local layers, maxradius = createOnionLayers(count); + local layers, maxradius = createOnionLayers( count ); -- Update the position of the file nodes based on the previously calculated onion-layers. - local fileCounter = 0; + local fileNumber = 0; local layer = 1; for i = 1, #toSort do local file = toSort[i].file; - fileCounter = fileCounter + 1; + fileNumber = fileNumber + 1; -- If we have more files on the current layer than allowed, we "move" -- the file to the next layer (this is why we reset the counter to one -- instead of zero). - if fileCounter > layers[layer].amount then + if fileNumber > layers[layer].amount then layer = layer + 1; - fileCounter = 1; + fileNumber = 1; end -- Calculate the new position of the file on its layer around the folder node. - local angle = 360 / layers[layer].amount; - local x = (layers[layer].radius * math.cos((angle * (fileCounter - 1)) * (math.pi / 180))); - local y = (layers[layer].radius * math.sin((angle * (fileCounter - 1)) * (math.pi / 180))); - file:setOffset(x, y); + file:setOffset( calculateFilePosition( fileNumber, layers[layer].amount, layers[layer].radius )); end return maxradius; end - --- - -- Update the node's position based on the calculated velocity and - -- acceleration. - -- - local function move(dt) - velX = (velX + accX * dt * speed) * DAMPING_FACTOR; - velY = (velY + accY * dt * speed) * DAMPING_FACTOR; - posX = posX + velX; - posY = posY + velY; - accX, accY = 0, 0; - end - -- ------------------------------------------------ -- Public Functions -- ------------------------------------------------ --- - -- Adds a child node to this node and increments the child counter. - -- @param name - The name of the node to add. - -- @param node - The actual node object. + -- Updates the node. + -- @param dt (number) Time since the last update in seconds. -- - function self:addChild(name, node) - children[name] = node; - childCount = childCount + 1; - return children[name]; - end - - --- - -- Removes a child node from this node and decrements the child counter. - -- @param name - The name of the node to remove. - -- - function self:removeChild(name) - children[name] = nil; - childCount = childCount - 1; - end - - function self:draw(ewidth) - for _, node in pairs(children) do - love.graphics.setColor(EDGE_COLOR); - love.graphics.setLineWidth(ewidth); - love.graphics.line(posX, posY, node:getX(), node:getY()); - love.graphics.setLineWidth(1); - love.graphics.setColor(255, 255, 255, 255); - node:draw(ewidth); - end - end - - function self:drawLabel(camrot, camscale) - love.graphics.setFont(LABEL_FONT); - love.graphics.print(name, posX, posY, -camrot, 1 / camscale, 1 / camscale, -radius * camscale, -radius * camscale); - - for _, node in pairs(children) do - node:drawLabel(camrot, camscale); - end - - love.graphics.setFont(DEFAULT_FONT); - end - - function self:update(dt) - move(dt); - for name, file in pairs(files) do + function self:update( dt ) + self:setMass( fileCount + childCount ); + for fileName, file in pairs( files ) do if file:isDead() then - self:removeFile(name, file:getExtension()); + self:removeFile( fileName, file:getExtension() ); end file:update(dt); - file:setPosition(posX, posY); + file:setPosition( self:getPosition() ); local color = file:getColor(); - spritebatch:setColor(color.r, color.g, color.b, color.a); + spritebatch:setColor( color.r, color.g, color.b, color.a ); - spritebatch:add(file:getX(), file:getY(), 0, SPRITE_SCALE_FACTOR, SPRITE_SCALE_FACTOR, SPRITE_OFFSET, SPRITE_OFFSET); + spritebatch:add( file:getX(), file:getY(), 0, SPRITE_SCALE_FACTOR, SPRITE_SCALE_FACTOR, SPRITE_OFFSET, SPRITE_OFFSET ); end - return posX, posY; end --- @@ -247,36 +185,38 @@ function Node.new(parent, path, name, x, y, spritebatch) -- requested from the FileManager and a new File object is created. After -- the file object has been added to the file list of this node, the layout -- of the files around the nodes is recalculated. - -- @param name - The name of the file to add. - -- @param extension - The extension of the file to add. + -- @param fileName (string) The name of the file to add. + -- @param extension (string) The extension of the file to add. + -- @return (File) The newly added File. -- - function self:addFile(name, extension) + function self:addFile( fileName, extension ) -- Exit early if the file already exists. - if files[name] then - files[name]:setState('add'); - return files[name]; + if files[fileName] then + files[fileName]:setState( 'add' ); + return files[fileName]; end -- Get the file color and extension from the FileManager and create the actual file object. - local color = FileManager.add(name, extension); - files[name] = File.new(posX, posY, color, extension); - files[name]:setState('add'); + local color = FileManager.add( extension ); + files[fileName] = File.new( self:getX(), self:getY(), color, extension ); + files[fileName]:setState( 'add' ); fileCount = fileCount + 1; -- Update layout of the files. - radius = plotCircle(files, fileCount); - return files[name]; + radius = plotCircle( fileCount ); + return files[fileName]; end --- -- Sets a file's modifier to deletion. - -- @param name - The name of the file to modify. + -- @param name (string) The name of the file to modify. + -- @return (File) The File marked for deletion. -- - function self:markFileForDeletion(name) - local file = files[name]; + function self:markFileForDeletion( fileName ) + local file = files[fileName]; if not file then - print('- Can not rem file: ' .. name .. ' - It doesn\'t exist.'); + print('- Can not rem file: ' .. fileName .. ' - It doesn\'t exist.'); return; end @@ -290,119 +230,89 @@ function Node.new(parent, path, name, x, y, spritebatch) -- FileManager that it also needs to be removed from the global file -- list. Once the file is removed, the layout of the files around the nodes -- is recalculated. - -- @param name - The name of the file to remove. - -- @param extension - The extension of the file to remove. + -- @param fileName (string) The name of the file to remove. + -- @param extension (string) The extension of the file to remove. + -- @return (File) The removed File. -- - function self:removeFile(name, extension) - local file = files[name]; + function self:removeFile( fileName, extension ) + local file = files[fileName]; if not file then - print('- Can not rem file: ' .. name .. ' - It doesn\'t exist.'); + print('- Can not rem file: ' .. fileName .. ' - It doesn\'t exist.'); return; end - -- Store a reference to the file which can be returned - -- after the file has been removed from the table. - FileManager.remove(name, extension); - files[name] = nil; + FileManager.remove( extension ); + files[fileName] = nil; fileCount = fileCount - 1; - radius = plotCircle(files, fileCount); + radius = plotCircle( fileCount ); return file; end --- -- Sets a file's modifier to "modification" and returns the file object. - -- @param name - The file to modify. + -- @param name (string) The file to modify. + -- @return (File) The modified File. -- - function self:modifyFile(name) - local file = files[name] + function self:modifyFile( fileName ) + local file = files[fileName] if not file then - print('~ Can not mod file: ' .. name .. ' - It doesn\'t exist.'); + print('~ Can not mod file: ' .. fileName .. ' - It doesn\'t exist.'); return; end - file:setState('mod'); + file:setState( 'mod' ); return file; end --- - -- Calculate and apply attraction and repulsion forces. - -- @param node + -- Increments the child counter. -- - function self:calculateForces(node) - if self == node then return end - - -- Calculate distance vector and normalise it. - local dx, dy = posX - node:getX(), posY - node:getY(); - local distance = math.sqrt(dx * dx + dy * dy); - dx = dx / distance; - dy = dy / distance; - - -- Attract to node if they are connected. - local strength; - if self:isConnectedTo(node) then - strength = FORCE_SPRING * distance; - applyForce(dx * strength, dy * strength); - end + function self:incrementChildCount() + childCount = childCount + 1; + end - -- Repel unconnected nodes. - strength = FORCE_CHARGE * ((self:getMass() * node:getMass()) / (distance * distance)); - applyForce(dx * strength, dy * strength); + --- + -- Decrements the child counter. + -- + function self:decrementChildCount() + childCount = childCount - 1; end -- ------------------------------------------------ -- Getters -- ------------------------------------------------ - function self:getFileCount() - return fileCount; - end - - function self:getChildCount() - return childCount; - end - - function self:getPosition() - return posX, posY; - end - - function self:getX() - return posX; - end - - function self:getY() - return posY; - end - - function self:getPath() - return path; + --- + -- Returns the node's name. + -- @return (string) The node's name. + -- + function self:getName() + return name; end + --- + -- Returns the node's parent node. + -- @return (Node) The node's parent. + -- function self:getParent() return parent; end - function self:getMass() - return 0.015 * (childCount + math.log(math.max(SPRITE_SIZE, radius))); - end - + --- + -- Returns the node's maximum radius. + -- @return (number) The node's maximum radius. + -- + -- function self:getRadius() return radius; end - function self:isConnectedTo(node) - for _, child in pairs(children) do - if node == child then - return true; - end - end - return parent == node; - end - --- -- Returns true if the node doesn't contain any files and doesn't have any -- children. + -- @return (boolean) True if the node is empty. -- function self:isDead() return fileCount == 0 and childCount == 0; diff --git a/src/logfactory/LogCreationThread.lua b/src/logfactory/LogCreationThread.lua new file mode 100644 index 0000000..4ca26d1 --- /dev/null +++ b/src/logfactory/LogCreationThread.lua @@ -0,0 +1,45 @@ +require( 'love.system' ); +require( 'love.timer' ); + +local GitHandler = require( 'src.git.GitHandler' ); +local RepositoryInfos = require( 'src.RepositoryInfos' ); + +local repositories = unpack( { ... } ); +local startTime = love.timer.getTime(); + +-- Exit early if git isn't available. +if not GitHandler.isGitAvailable() then + local channel = love.thread.getChannel( 'error' ); + channel:push( { msg = 'git_not_found' } ); + return; +end + +for name, path in pairs( repositories ) do + -- Check if the path points to a valid git repository before attempting + -- to create a git log and the info file for it. + if GitHandler.isGitRepository( path ) then + local count; + + if RepositoryInfos.hasCommitCountFile( name ) then + count = RepositoryInfos.loadCommitCount( name ); + end + + if not count or not GitHandler.isRepositoryUpToDate( path, count.commits ) then + print( " Writing log for " .. name ); + GitHandler.createGitLog( name, path ); + RepositoryInfos.createInfoFile( name ); + RepositoryInfos.createCommitCountFile( name, GitHandler.getTotalCommits( path )); + else + print( " Repository " .. name .. " is up to date!" ); + end + + local channel = love.thread.getChannel( 'info' ); + channel:push( name ); + else + local channel = love.thread.getChannel( 'error' ); + channel:push( { msg = 'no_repository', name = name, data = path } ); + end +end + +local endTime = love.timer.getTime(); +print( string.format( 'Loaded git logs in %.3f seconds!', endTime - startTime )); diff --git a/src/logfactory/LogCreator.lua b/src/logfactory/LogCreator.lua deleted file mode 100644 index 0bdc24d..0000000 --- a/src/logfactory/LogCreator.lua +++ /dev/null @@ -1,102 +0,0 @@ -local LogCreator = {}; - --- ------------------------------------------------ --- Constants --- ------------------------------------------------ - -local GIT_COMMAND = 'git -C "' -local LOG_COMMAND = '" log --reverse --numstat --pretty=format:"info: %an|%ae|%ct" --name-status --no-merges'; -local STATUS_COMMAND = '" status'; -local FIRST_COMMIT_COMMAND = '" log --pretty=format:%ct|tail -1'; -local LATEST_COMMIT_COMMAND = '" log --pretty=format:%ct|head -1'; -local TOTAL_COMMITS_COMMAND = '" rev-list HEAD --count'; -local LOG_FOLDER = 'logs/'; -local LOG_FILE = '/log.txt'; -local INFO_FILE = '/info.lua'; - --- ------------------------------------------------ --- Public Functions --- ------------------------------------------------ - ---- --- Creates a git log if git is available and no log has been --- created in the target folder yet. --- @param projectname --- @param path --- -function LogCreator.createGitLog(projectname, path, force) - if not force and love.filesystem.isFile(LOG_FOLDER .. projectname .. LOG_FILE) then - io.write('Git log for ' .. projectname .. ' already exists!\r\n'); - else - io.write('Writing log for ' .. projectname .. '.\r\n'); - love.filesystem.createDirectory(LOG_FOLDER .. projectname); - - local cmd = GIT_COMMAND .. path .. LOG_COMMAND; - local handle = io.popen(cmd); - love.filesystem.write(LOG_FOLDER .. projectname .. LOG_FILE, handle:read('*all')); - handle:close(); - io.write('Done!\r\n'); - end -end - -function LogCreator.createInfoFile(projectname, path, force) - if not force and love.filesystem.isFile(LOG_FOLDER .. projectname .. INFO_FILE) then - io.write('Info file for ' .. projectname .. ' already exists!\r\n'); - elseif love.system.getOS() ~= 'Windows' then - local fileContent = ''; - fileContent = fileContent .. 'return {\r\n'; - - -- Project name. - fileContent = fileContent .. ' name = "' .. projectname .. '",\r\n'; - - -- First commit. - local handle = io.popen(GIT_COMMAND .. path .. FIRST_COMMIT_COMMAND); - fileContent = fileContent .. ' firstCommit = ' .. handle:read('*a'):gsub('[%s]+', '') .. ',\r\n'; - handle:close(); - - -- Latest commit. - local handle = io.popen(GIT_COMMAND .. path .. LATEST_COMMIT_COMMAND); - fileContent = fileContent .. ' latestCommit = ' .. handle:read('*a'):gsub('[%s]+', '') .. ',\r\n'; - handle:close(); - - -- Number of commits. - local handle = io.popen(GIT_COMMAND .. path .. TOTAL_COMMITS_COMMAND); - fileContent = fileContent .. ' totalCommits = ' .. handle:read('*a'):gsub('[%s]+', '') .. ',\r\n'; - handle:close(); - - fileContent = fileContent .. ' aliases = {},\r\n'; - fileContent = fileContent .. ' avatars = {},\r\n'; - fileContent = fileContent .. ' colors = {}\r\n'; - - fileContent = fileContent .. '};\r\n'; - - love.filesystem.write(LOG_FOLDER .. projectname .. INFO_FILE, fileContent); - end -end - --- ------------------------------------------------ --- Getters --- ------------------------------------------------ - ---- --- Checks if git is available on the system. --- -function LogCreator.isGitAvailable() - local handle = io.popen('git version'); - local result = handle:read('*a'); - handle:close(); - return result:find('git version'); -end - ---- --- Checks if a path points to a valid git repository. --- @param path - The path to check. --- -function LogCreator.isGitRepository(path) - local handle = io.popen(GIT_COMMAND .. path .. STATUS_COMMAND); - local result = handle:read('*a'); - handle:close(); - return result ~= ''; -end - -return LogCreator; diff --git a/src/logfactory/LogLoader.lua b/src/logfactory/LogLoader.lua index d02e83d..59309f8 100644 --- a/src/logfactory/LogLoader.lua +++ b/src/logfactory/LogLoader.lua @@ -5,23 +5,9 @@ local LogLoader = {}; -- ------------------------------------------------ local LOG_FOLDER = 'logs'; -local LOG_FILE = 'log.txt'; -local INFO_FILE = 'info.lua'; +local LOG_FILE = '.log'; local TAG_INFO = 'info: '; -local ROOT_FOLDER = 'root'; - -local WARNING_TITLE = 'No git log found.'; -local WARNING_MESSAGE = [[ -Looks like you are using LoGiVi for the first time. An example git log has been created in the save directory. Press 'Yes' to open the save directory. - -Press 'Show Help' to view the wiki (online) for more information on how to generate a proper log. - -Press 'No' to proceed to the selection screen from where you can view the example project. -]]; - -local EXAMPLE_TEMPLATE_PATH = 'res/templates/example_log.txt'; -local EXAMPLE_TARGET_PATH = 'logs/example/'; -- ------------------------------------------------ -- Local variables @@ -34,20 +20,25 @@ local list; -- ------------------------------------------------ --- --- Remove the specified tag from the line. --- @param line --- @param tag +-- Removes the specified tag from the line. +-- @param line (string) The line to edit. +-- @param tag (string) The tag to remove. +-- @return (string) The edited line with the tag removed. -- -local function removeTag(line, tag) - return line:gsub(tag, ''); +local function removeTag( line, tag ) + return line:gsub( tag, '' ); end --- --- @param author +-- Splits the line at the specified delimiter. +-- @param line (string) The line to edit. +-- @param delimiter (string) The delimiter which marks the the position at which +-- to split the line. +-- @return (table) A sequence containing the split parts of the line. -- -local function splitLine(line, delimiter) +local function splitLine( line, delimiter ) local tmp = {} - for part in line:gmatch('[^' .. delimiter .. ']+') do + for part in line:gmatch( '[^' .. delimiter .. ']+' ) do tmp[#tmp + 1] = part; end return tmp; @@ -55,30 +46,29 @@ end --- -- Creates a list of all folders found in the LOG_FOLDER directory, which --- contain a LOG_FILE. Returns a sequence which contains the names of the folders --- and the path to the log files in those folders. --- @param dir +-- contain a LOG_FILE. +-- @param dir (string) The path to the directory which contains the log files. +-- @return (table) The table containing the name and the path to each log file. -- -local function fetchProjectFolders(dir) +local function fetchProjectFolders( dir ) local folders = {}; - - for _, name in ipairs(love.filesystem.getDirectoryItems(dir)) do + for _, name in ipairs( love.filesystem.getDirectoryItems( dir )) do local subdir = dir .. '/' .. name; - if love.filesystem.isDirectory(subdir) and love.filesystem.isFile(subdir .. '/' .. LOG_FILE) then + if love.filesystem.isDirectory( subdir ) and love.filesystem.isFile( subdir .. '/' .. LOG_FILE ) then folders[#folders + 1] = { name = name, path = subdir .. '/' .. LOG_FILE }; end end - return folders; end --- --- Reads the whole log file and stores each line in a sequence. --- @param path +-- Reads a git log file and stores each line in a sequence. +-- @param path (string) The path pointing to a log file. +-- @return (table) A sequence containing each line of the git log. -- -local function parseLog(path) +local function parseLog( path ) local file = {}; - for line in love.filesystem.lines(path) do + for line in love.filesystem.lines( path ) do if line ~= '' then file[#file + 1] = line; end @@ -87,150 +77,103 @@ local function parseLog(path) end --- --- Turns a unix timestamp into a human readable date string. --- @param timestamp +-- Turns a unix timestamp into a human-readable date string. +-- @param timestamp (string) The unix timestamp read from the git log. +-- @return (string) The newly created human-readable date string. -- -local function createDateFromUnixTimestamp(timestamp) - local date = os.date('*t', tonumber(timestamp)); - return string.format("%02d:%02d:%02d - %02d-%02d-%04d", date.hour, date.min, date.sec, date.day, date.month, date.year); +local function createDateFromUnixTimestamp( timestamp ) + local date = os.date( '*t', tonumber( timestamp )); + return string.format( "%02d:%02d:%02d - %02d-%02d-%04d", date.hour, date.min, date.sec, date.day, date.month, date.year ); end --- --- Splits the log table into commits. Each commit is a new nested table. --- @param log +-- Splits a commit line into modifier, path, file and extension. This basically +-- extracts the essential information about which changes have been performed +-- on a certain file in a commit. +-- @param line (string) The line to split. +-- @return (table) A table containing the split parts. -- -local function splitCommits(log) +local function buildCommitLine( line ) + local modifier = line:sub( 1, 1 ); + local path = line:gsub( '^(%a)%s*', '' ); + local file = path:match( '/?([^/]+)$' ); + local extension = file:match( '(%.[^.]+)$' ) or '.?'; + + path = path:gsub( '/?([^/]+)$', '' ); -- Remove the filename from the path. + path = path ~= '' and '/' .. path or path; + + return { modifier = modifier, path = path, file = file, extension = extension }; +end + +--- +-- Splits the log table into commits. Each commit is stored a new nested table. +-- The has part of the table contains the author, the author's email adress and +-- the date at which the changes have been commited. The array part of the table +-- contains the changes which have been made in the commit. This contains the +-- info about which modifier has been applied to a certain file in the +-- repository. +-- @param log (table) A sequence containing each line of the git log. +-- @return (table) A sequence containing each commit of the git log. +-- +local function splitCommits( log ) local commits = {}; - local index = 0; + local commitIndex = 0; for i = 1, #log do local line = log[i]; - if line:find(TAG_INFO) then - index = index + 1; - commits[index] = {}; - - local info = splitLine(removeTag(line, TAG_INFO), '|'); - commits[index].author, commits[index].email, commits[index].date = info[1], info[2], info[3]; - - -- Transform unix timestamp to a table containing a human-readable date. - commits[index].date = createDateFromUnixTimestamp(commits[index].date); - elseif commits[index] then - -- Split the whole change line into modifier, file name and file path fields. - local path = line:gsub("^(%a)%s*", ''); -- Remove modifier and whitespace. - local file = path:match("/?([^/]+)$"); -- Get the the filename at the end. - path = path:gsub("/?([^/]+)$", ''); -- Remove the filename from the path. - if path ~= '' then - path = '/' .. path; - end - local extension = file:match("(%.[^.]+)$") or '.?'; -- Get the file's extension. - - commits[index][#commits[index] + 1] = { modifier = line:sub(1, 1), path = ROOT_FOLDER .. path, file = file, extension = extension }; + if line:find( TAG_INFO ) then -- Look for the start of a new commit. + local commit = {}; + + local info = splitLine( removeTag( line, TAG_INFO ), '|' ); + commit.author = info[1]; + commit.email = info[2]; + commit.date = createDateFromUnixTimestamp( info[3] ); + + commitIndex = commitIndex + 1; + commits[commitIndex] = commit; + elseif commits[commitIndex] then + commits[commitIndex][#commits[commitIndex] + 1] = buildCommitLine( line ); end end - return commits; end --- -- Returns the index of a stored log if it can be found. --- @param name +-- @param name (string) The name of the log to search. +-- @return (number) The index at which the log was found. -- -local function searchLog(name) - for i, log in ipairs(list) do +local function searchLog( name ) + for i, log in ipairs( list ) do if log.name == name then return i; end end end ---- --- Checks if the log folder exists and if it is empty or not. --- -local function hasLogs() - return (love.filesystem.isDirectory('logs') and #list ~= 0); -end - ---- --- Displays a warning message for the user which gives him the option --- to open the wiki page or the folder in which the logs need to be placed. --- -local function showWarning() - local buttons = { "Yes", "No", "Show Help (Online)", enterbutton = 1, escapebutton = 2 }; - - local pressedbutton = love.window.showMessageBox(WARNING_TITLE, WARNING_MESSAGE, buttons, 'warning', false); - if pressedbutton == 1 then - love.system.openURL('file://' .. love.filesystem.getSaveDirectory() .. '/logs'); - elseif pressedbutton == 3 then - love.system.openURL('https://github.com/rm-code/logivi/wiki#instructions'); - end -end - ---- --- Write an example log file to the save directory. --- -local function createExample() - love.filesystem.createDirectory(EXAMPLE_TARGET_PATH); - if not love.filesystem.isFile(EXAMPLE_TARGET_PATH .. LOG_FILE) then - local example = love.filesystem.read(EXAMPLE_TEMPLATE_PATH); - love.filesystem.write(EXAMPLE_TARGET_PATH .. LOG_FILE, example); - end -end - -- ------------------------------------------------ -- Public Functions -- ------------------------------------------------ --- --- Try to load a certain log stored in the list. +-- Try to load a certain log stored in the list and create a table which can +-- be processed by LoGiVi. +-- @param log (string) The name of the log to load. +-- @return (table) A sequence containing each commit of the git log. -- -function LogLoader.load(log) - local index = searchLog(log); - local rawLog = parseLog(list[index].path); - return splitCommits(rawLog); +function LogLoader.load( log ) + local index = searchLog( log ); + local rawLog = parseLog( list[index].path ); + return splitCommits( rawLog ); end --- --- Loads information about a git repository. --- @param name --- -function LogLoader.loadInfo(name) - if love.filesystem.isFile(LOG_FOLDER .. '/' .. name .. '/' .. INFO_FILE) then - local successful, info = pcall(love.filesystem.load, LOG_FOLDER .. '/' .. name .. '/' .. INFO_FILE); - if successful then - info = info(); -- Run the lua file. - info.firstCommit = createDateFromUnixTimestamp(info.firstCommit); - info.latestCommit = createDateFromUnixTimestamp(info.latestCommit); - info.aliases = info.aliases or {}; - info.avatars = info.avatars or {}; - info.colors = info.colors or {}; - return info; - end - end - return { - name = name, - firstCommit = '', - latestCommit = '', - totalCommits = '', - aliases = {}, - avatars = {}, - colors = {}, - }; -end - ---- --- Initialises the LogLoader. It will fetch a list of all folders --- containing a log file. If the list is empty it will display a --- warning to the user. +-- Initialises the LogLoader. It will fetch a list of all folders containing a +-- log file. +-- @return (table) A sequence containing the names and paths of all stored git logs. -- function LogLoader.init() - list = fetchProjectFolders(LOG_FOLDER); - - if not hasLogs() then - createExample(); - showWarning(); - list = fetchProjectFolders(LOG_FOLDER); - end - + list = fetchProjectFolders( LOG_FOLDER ); return list; end diff --git a/src/logfactory/LogReader.lua b/src/logfactory/LogReader.lua index 91a8e3f..2e84d95 100644 --- a/src/logfactory/LogReader.lua +++ b/src/logfactory/LogReader.lua @@ -1,13 +1,25 @@ +local Messenger = require('src.messenger.Messenger'); + +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ + local LogReader = {}; -- ------------------------------------------------ -- Constants -- ------------------------------------------------ -local EVENT_NEW_COMMIT = 'NEW_COMMIT'; -local EVENT_CHANGED_FILE = 'LOGREADER_CHANGED_FILE'; +local EVENT = require('src.messenger.Event'); local MOD_ADD = 'A'; local MOD_DELETE = 'D'; +local MOD_MODIFY = 'M'; + +local PLAYBACK_NORMAL = 'normal'; +local PLAYBACK_FAST = 'fast'; + +local PLAYMODE_NORMAL = 'default'; +local PLAYMODE_REWIND = 'rewind'; -- ------------------------------------------------ -- Local Variables @@ -19,29 +31,19 @@ local commitTimer; local commitDelay; local play; local rewind; -local observers; -- ------------------------------------------------ -- Local Functions -- ------------------------------------------------ --- --- Notify observers about the event. --- @param event --- @param ... +-- This function will take a git modifier and return the direct opposite of it. +-- This means the add modifier will be reversed to delete, whereas the delete +-- modifier will be reversed to add. +-- @param modifier (string) The git modifier to reverse. +-- @return (string) The reversed git modifier. -- -local function notify(event, ...) - for i = 1, #observers do - observers[i]:receive(event, ...); - end -end - ---- --- This function will take a git modifier and return the direct --- opposite of it. --- @param modifier --- -local function reverseGitStatus(modifier) +local function reverseGitStatus( modifier ) if modifier == MOD_ADD then return MOD_DELETE; elseif modifier == MOD_DELETE then @@ -50,42 +52,55 @@ local function reverseGitStatus(modifier) return modifier; end +--- +-- Loads the new commit and publishes the necessary events. +-- local function applyNextCommit() + -- Stop if we reach the end of the log. if index == #log then return; end index = index + 1; - notify(EVENT_NEW_COMMIT, log[index].email, log[index].author); + -- Notify listeners of the new commit. + Messenger.publish( EVENT.NEW_COMMIT, log[index].email, log[index].author ); + -- Notify listeners of file changes made in the new commit. for i = 1, #log[index] do local change = log[index][i]; - notify(EVENT_CHANGED_FILE, change.modifier, change.path, change.file, change.extension, 'normal'); + Messenger.publish( EVENT.LOGREADER_CHANGED_FILE, change.modifier, change.path, change.file, change.extension, PLAYBACK_NORMAL ); end end +--- +-- Reverses the current commit. This basically means we read a commit and reverse +-- all the modifications made by it. Added files will be removed for example. +-- local function reverseCurCommit() + -- Stop if we reach the beginning of the log. if index == 0 then return; end - notify(EVENT_NEW_COMMIT, log[index].email, log[index].author); + -- Notify listeners of the new commit. + Messenger.publish( EVENT.NEW_COMMIT, log[index].email, log[index].author ); + -- Notify listeners of file changes made in the new commit. for i = 1, #log[index] do local change = log[index][i]; - notify(EVENT_CHANGED_FILE, reverseGitStatus(change.modifier), change.path, change.file, change.extension, 'normal'); + Messenger.publish( EVENT.LOGREADER_CHANGED_FILE, reverseGitStatus( change.modifier ), change.path, change.file, change.extension, PLAYBACK_NORMAL ); end index = index - 1; end --- --- Fast forwards the graph from the current position to the --- target position. We ignore author assigments and modifications --- and only are interested in additions and deletions. --- @param to -- The index of the commit to go to. +-- Fast forwards the graph from the current position to the target position. We +-- ignore author assigments and modifications and only are interested in +-- additions and deletions. +-- @param to (number) The index of the commit to go to. -- -local function fastForward(to) +local function fastForward( to ) -- We start at index + 1 because the current index has already -- been loaded (or it was 0 and therefore nonrelevant anyway). for i = index + 1, to do @@ -93,21 +108,21 @@ local function fastForward(to) local commit = log[index]; for j = 1, #commit do local change = commit[j]; - -- Ignore modifications we just need to know about additions and deletions. - if change.modifier ~= 'M' then - notify(EVENT_CHANGED_FILE, change.modifier, change.path, change.file, change.extension, 'fast'); + -- Ignore modifications, we just need to know about additions and deletions. + if change.modifier ~= MOD_MODIFY then + Messenger.publish( EVENT.LOGREADER_CHANGED_FILE, change.modifier, change.path, change.file, change.extension, PLAYBACK_FAST ); end end end end --- --- Quickly rewinds the graph from the current position to the --- target position. We ignore author assigments and modifications --- and only are interested in additions and deletions. --- @param to -- The index of the commit to go to. +-- Quickly rewinds the graph from the current position to the target position. +-- We ignore author assigments and modifications and only are interested in +-- additions and deletions. +-- @param to (number) The index of the commit to go to. -- -local function fastBackward(to) +local function fastBackward( to ) -- We start at the current index, because it has already been loaded -- and we have to reverse it too. for i = index, to, -1 do @@ -115,14 +130,16 @@ local function fastBackward(to) -- When we have reached the target commit, we update the index, but -- don't reverse the changes it made. - if index == to then break end + if index == to then + break + end local commit = log[index]; for j = #commit, 1, -1 do local change = commit[j]; -- Ignore modifications we just need to know about additions and deletions. - if change.modifier ~= 'M' then - notify(EVENT_CHANGED_FILE, reverseGitStatus(change.modifier), change.path, change.file, change.extension, 'fast'); + if change.modifier ~= MOD_MODIFY then + Messenger.publish( EVENT.LOGREADER_CHANGED_FILE, reverseGitStatus( change.modifier ), change.path, change.file, change.extension, PLAYBACK_FAST ); end end end @@ -134,29 +151,34 @@ end --- -- Loads the file and stores it line for line in a lua table. --- @param logpath +-- @param gitlog (table) A sequence containing each commit of the git log. +-- @param delay (number) The amount of time to wait before loading the next commit. +-- @param playmode (string) The playmode (default or rewind). +-- @param autoplay (boolean) Wether to directly start playing the visualisation. -- -function LogReader.init(gitlog, delay, playmode, autoplay) +function LogReader.init( gitlog, delay, playmode, autoplay ) log = gitlog; -- Set default values. index = 0; - if playmode == 'default' then + if playmode == PLAYMODE_NORMAL then rewind = false; - elseif playmode == 'rewind' then - fastForward(#log); + elseif playmode == PLAYMODE_REWIND then + fastForward( #log ); -- Jump to the end of the log. rewind = true; else - error("Unsupported playmode '" .. playmode .. "' - please use either 'default' or 'rewind'"); + error( "Unsupported playmode '" .. playmode .. "' - please use either 'default' or 'rewind'" ); end commitTimer = 0; commitDelay = delay; play = autoplay; - - observers = {}; end -function LogReader.update(dt) +--- +-- Updates the LogReader. +-- @param dt (number) Time since the last update in seconds. +-- +function LogReader.update( dt ) if not play then return end commitTimer = commitTimer + dt; @@ -170,62 +192,76 @@ function LogReader.update(dt) end end +--- +-- Toggle the playback. +-- function LogReader.toggleSimulation() play = not play; end +--- +-- Reverses the playback direction. +-- function LogReader.toggleRewind() rewind = not rewind; end +--- +-- Advances the log by a single step. +-- function LogReader.loadNextCommit() play = false; applyNextCommit(); end +--- +-- Moves the log back a single step. +-- function LogReader.loadPrevCommit() play = false; reverseCurCommit(); end --- --- Sets the reader to a new commit index. If the --- index is the same as the current one, the input is --- ignored. If the target commit is smaller (aka older) --- as the current one we fast-rewind the graph to that --- position. If the target commit is bigger than the --- current one, we fast-forward instead. +-- Sets the reader to a new commit index. If the index is the same as the current +-- one, the input is ignored. If the target commit is smaller (aka older) as the +-- current one we fast-rewind the graph to that position. If the target commit is +-- bigger than the current one, we fast-forward instead. +-- @param ni (number) The new index to jump to. -- -function LogReader.setCurrentIndex(ni) +function LogReader.setCurrentIndex( ni ) if log[ni] then if index == ni then return; elseif index < ni then - fastForward(ni); + fastForward( ni ); elseif index > ni then - fastBackward(ni); + fastBackward( ni ); end end end +--- +-- Returns the total amount of commits in the git log. +-- @return (number) The total amount of commits. +-- function LogReader.getTotalCommits() return #log; end +--- +-- Returns the current index in the log. +-- @return (number) The current index. function LogReader.getCurrentIndex() return index; end -function LogReader.getCurrentDate() - return index ~= 0 and log[index].date or ''; -end - --- --- Register an observer. --- @param observer +-- Returns the date of the current commit. +-- @return (string) The current date or an empty string. -- -function LogReader.register(observer) - observers[#observers + 1] = observer; +function LogReader.getCurrentDate() + return index ~= 0 and log[index].date or ''; end -- ------------------------------------------------ diff --git a/src/messenger/Event.lua b/src/messenger/Event.lua new file mode 100644 index 0000000..8907719 --- /dev/null +++ b/src/messenger/Event.lua @@ -0,0 +1,17 @@ +local EVENT = {}; + +EVENT.GRAPH_UPDATE_DIMENSIONS = 'GRAPH_UPDATE_DIMENSIONS'; +EVENT.GRAPH_UPDATE_CENTER = 'GRAPH_UPDATE_CENTER'; +EVENT.GRAPH_UPDATE_FILE = 'GRAPH_UPDATE_FILE'; +EVENT.NEW_COMMIT = 'NEW_COMMIT'; +EVENT.LOGREADER_CHANGED_FILE = 'LOGREADER_CHANGED_FILE'; + +-- Make table read-only. +return setmetatable( EVENT, { + __index = function( _, key ) + error( "Can't access constant value at key: " .. key ); + end, + __newindex = function() + error( "Can't change a constant value." ); + end +}); diff --git a/src/messenger/Messenger.lua b/src/messenger/Messenger.lua new file mode 100644 index 0000000..990d87d --- /dev/null +++ b/src/messenger/Messenger.lua @@ -0,0 +1,39 @@ +local Messenger = {}; + +local subscriptions = {}; +local index = 0; + +--- +-- Publishes a message to all subscribers. +-- @param message (string) The message's type. +-- @param ... (vararg) One or multiple arguments passed to the subscriber. +-- +function Messenger.publish( message, ... ) + for _, subscription in pairs( subscriptions ) do + if subscription.message == message then + subscription.callback( ... ); + end + end +end + +--- +-- Registers a callback belonging to a certain subscriber. +-- @param message (string) The message to listen for. +-- @param callback (function) The function to call once the message is published. +-- @return (number) The index pointing to the subscription. +-- +function Messenger.observe( message, callback ) + index = index + 1; + subscriptions[index] = { message = message, callback = callback }; + return index; +end + +--- +-- Removes a subscription based on its index. +-- @param nindex (number) The index of the subscription to remove. +-- +function Messenger.remove( nindex ) + subscriptions[nindex] = nil; +end + +return Messenger; diff --git a/src/screens/InputPanel.lua b/src/screens/InputPanel.lua new file mode 100644 index 0000000..2525d0d --- /dev/null +++ b/src/screens/InputPanel.lua @@ -0,0 +1,115 @@ +local ScreenManager = require('lib.screenmanager.ScreenManager'); +local Screen = require('lib.screenmanager.Screen'); +local Resources = require('src.Resources'); +local RepositoryHandler = require('src.conf.RepositoryHandler'); + +-- ------------------------------------------------ +-- Constants +-- ------------------------------------------------ + +local INFO_FONT = Resources.loadFont( 'SourceCodePro-Medium.otf', 10 ); +local LABEL_FONT = Resources.loadFont( 'SourceCodePro-Medium.otf', 13 ); + +local PANEL_WIDTH = 280; +local PANEL_HEIGHT = 60; +local MAX_LENGTH = 30; + +local BG_COLOR = { 60, 60, 60 }; + +local INPUT_MESSAGE = "Please enter a name for the repository:"; + +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ + +local InputPanel = {}; + +-- ------------------------------------------------ +-- Constructor +-- ------------------------------------------------ + +function InputPanel.new() + local self = Screen.new(); + + local input = {}; + local index = 1; + local text = ''; + local config; + local path; + + -- ------------------------------------------------ + -- Public Functions + -- ------------------------------------------------ + + --- + -- Initialises the InputPanel. + -- @param npath (string) The path to the directory which was dropped on the application window. + -- @param params (table) A table containing the general configuration of LoGiVi. + -- + function self:init( npath, params ) + path = npath; + config = params.config; + end + + --- + -- Draws the InputPanel. + -- + function self:draw() + local sw, sh = love.graphics.getDimensions(); + + local x = sw * 0.5 - PANEL_WIDTH * 0.5; + local y = sh * 0.5 - PANEL_HEIGHT * 0.5; + + love.graphics.setColor( 0, 0, 0, 200 ); + love.graphics.rectangle( 'fill' , 0, 0, sw, sh ); + love.graphics.setColor( BG_COLOR ); + love.graphics.rectangle( 'fill', x, y, PANEL_WIDTH, PANEL_HEIGHT, 20, 20, 20 ); + love.graphics.setColor( 255, 255, 255 ); + love.graphics.setFont( INFO_FONT ); + love.graphics.print( INPUT_MESSAGE , x + 20, y + 15 ); + love.graphics.setFont( LABEL_FONT ); + love.graphics.print( text, x + 20, y + 35 ); + end + + --- + -- Handles keypressed events. + -- @param key (string) The pressed key. + -- + function self:keypressed( key ) + if key == 'escape' then + ScreenManager.pop(); + end + if key == 'backspace' then + self:remove(); + end + if key == 'return' then + RepositoryHandler.add( text, path ); + ScreenManager.switch( 'loading', { config = config } ); + end + end + + --- + -- Handles textinput events. + -- @param txt (string) The user's text input. + -- + function self:textinput( txt ) + if index < MAX_LENGTH then + table.insert( input, index, txt ); + text = table.concat( input ); + index = index + 1; + end + end + + --- + -- Removes the last character in the input table. + -- + function self:remove() + index = math.max( 1, index - 1 ); + table.remove( input, index ); + text = table.concat( input ); + end + + return self; +end + +return InputPanel; diff --git a/src/screens/LoadingScreen.lua b/src/screens/LoadingScreen.lua new file mode 100644 index 0000000..4d3e2df --- /dev/null +++ b/src/screens/LoadingScreen.lua @@ -0,0 +1,213 @@ +local ScreenManager = require('lib.screenmanager.ScreenManager'); +local Screen = require('lib.screenmanager.Screen'); +local ConfigReader = require('src.conf.ConfigReader'); +local RepositoryHandler = require('src.conf.RepositoryHandler'); +local GraphLibrary = require('lib.graphoon.Graphoon').Graph; +local Resources = require('src.Resources'); +local Utility = require( 'src.Utility' ); + +-- ------------------------------------------------ +-- Constants +-- ------------------------------------------------ + +local BUTTON_OK = 'Ok'; +local BUTTON_HELP = 'Help (online)'; + +local URL_INSTRUCTIONS = 'https://github.com/rm-code/logivi#generating-git-logs-automatically'; + +local WARNING_TITLE_NO_GIT = 'Git is not available'; +local WARNING_MESSAGE_NO_GIT = 'LoGiVi can\'t find git in your PATH. This means LoGiVi won\'t be able to create git logs automatically, but can still be used to view pre-generated logs.'; + +local WARNING_TITLE_NO_REPO = 'Not a valid git repository'; +local WARNING_MESSAGE_NO_REPO = 'The path "%s" does not point to a valid git repository.'; + +local SPRITE_SIZE = 24; +local SPRITE_SCALE_FACTOR = SPRITE_SIZE / 256; +local SPRITE_OFFSET = 128; + +local LABEL_FONT = Resources.loadFont( 'SourceCodePro-Medium.otf', 20 ); +local DEFAULT_FONT = Resources.loadFont( 'default', 12 ); +local FILE_SPRITE = Resources.loadImage( 'file.png' ); + +local VERSION_STRING = string.format( 'Version %s', getVersion() ); + +local LOADING_STRING = 'Loading'; +local LOADING_DOTS = { + '', + ' .', + ' . .', + ' . . .' +} +local LOADING_DOT_TIME = 0.15; + +local EDGE_COLOR = { 60, 60, 60, 255 }; + +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ + +local LoadingScreen = {}; + +-- ------------------------------------------------ +-- Constructor +-- ------------------------------------------------ + +function LoadingScreen.new() + local self = Screen.new(); + + local config; + local thread; + local graph; + + local dots; + local dotTimer; + local dotIndex; + + local loadingTimer; + + local colors; + + -- ------------------------------------------------ + -- Private Functions + -- ------------------------------------------------ + + --- + -- Updates the dot-animation indicating the running loading operations. + -- @param dt (number) Time since the last update in seconds. + -- + local function updateLoadingDots( dt ) + if dotTimer > LOADING_DOT_TIME then + dotIndex = dotIndex == #LOADING_DOTS and 1 or dotIndex + 1; + dots = LOADING_DOTS[dotIndex]; + dotTimer = 0; + end + dotTimer = dotTimer + dt; + end + + --- + -- Shows warning messages to the user in case something goes wrong while + -- loading a repository. + -- @param error (table) A table containing infos about the error. + -- + local function handleThreadErrors( error ) + if error.msg == 'git_not_found' then + local pressedbutton = love.window.showMessageBox( WARNING_TITLE_NO_GIT, WARNING_MESSAGE_NO_GIT, { BUTTON_OK, BUTTON_HELP, enterbutton = 1, escapebutton = 1 }, 'warning', false ); + if pressedbutton == 2 then + love.system.openURL( URL_INSTRUCTIONS ); + end + elseif error.msg == 'no_repository' then + love.window.showMessageBox( WARNING_TITLE_NO_REPO, string.format( WARNING_MESSAGE_NO_REPO, error.data ), 'warning', false ); + RepositoryHandler.remove( error.name ); + end + end + + --- + -- Adds a new node to the graph representing the loaded repository. + -- @param info (table) A table containing infos about the loaded repository. + -- + local function addNewNode( info ) + colors[info] = { + love.math.random( 0, 255 ), + love.math.random( 0, 255 ), + love.math.random( 0, 255 ) + }; + local spawnX = love.graphics.getWidth() * 0.5 + Utility.randomSign() * love.math.random( 5, 15 ); + local spawnY = love.graphics.getHeight() * 0.5 + Utility.randomSign() * love.math.random( 5, 15 ); + graph:addNode( info, spawnX, spawnY); + graph:connectIDs( '', info ); + end + + -- ------------------------------------------------ + -- Public Functions + -- ------------------------------------------------ + + --- + -- Initialises the loading screen. + -- @param param (table) A table containing certain parameters passed from a + -- previous screen / state. + -- + function self:init( param ) + config = ( param and param.config ) or ConfigReader.init(); + + love.window.setMode( config.options.screenWidth, config.options.screenHeight, { borderless = true, msaa = config.options.msaa, vsync = config.options.vsync } ); + + RepositoryHandler.init(); + + graph = GraphLibrary.new(); + graph:addNode( '', love.graphics.getWidth() * 0.5, love.graphics.getHeight() * 0.5, true ); + + thread = love.thread.newThread( 'src/logfactory/LogCreationThread.lua' ); + thread:start( RepositoryHandler.getRepositories() ); + + dotIndex = 1; + dotTimer = 0; + dots = LOADING_DOTS[dotIndex]; + + loadingTimer = 0; + + colors = { + [''] = { 255, 255, 255 } + }; + end + + --- + -- Updates the loading screen and its elements. + -- @param dt (number) Time since the last update in seconds. + -- + function self:update( dt ) + graph:update( dt ); + + local threadError = thread:getError(); + assert( not threadError, threadError ); + + local errChannel = love.thread.getChannel( 'error' ); + local err = errChannel:pop(); + if err then + handleThreadErrors( err ); + end + + local infoChannel = love.thread.getChannel( 'info' ); + local info = infoChannel:pop(); + if info then + addNewNode( info ); + end + + if not thread:isRunning() then + ScreenManager.switch( 'selection', { config = config, graph = graph, colors = colors } ); + end + + loadingTimer = loadingTimer + dt; + + updateLoadingDots( dt ); + end + + --- + -- Draws the loading screen. + -- + function self:draw() + graph:draw( function( node ) + local x, y = node:getPosition(); + love.graphics.setColor( colors[node:getID()] ); + love.graphics.draw( FILE_SPRITE, x, y, 0, SPRITE_SCALE_FACTOR, SPRITE_SCALE_FACTOR, SPRITE_OFFSET, SPRITE_OFFSET ); + love.graphics.setColor( 255, 255, 255 ); + love.graphics.setFont( LABEL_FONT ); + love.graphics.print( node:getID(), x, y, 0, 1, 1, -16, -16 ); + love.graphics.setFont( DEFAULT_FONT ); + end, + function( edge ) + love.graphics.setColor( EDGE_COLOR ); + love.graphics.setLineWidth( 5 ); + love.graphics.line( edge.origin:getX(), edge.origin:getY(), edge.target:getX(), edge.target:getY() ); + love.graphics.setLineWidth( 1 ); + love.graphics.setColor( 255, 255, 255, 255 ); + end); + + love.graphics.print( LOADING_STRING, 10, love.graphics.getHeight() - 20 ); + love.graphics.print( dots, DEFAULT_FONT:getWidth( LOADING_STRING ) + 10, love.graphics.getHeight() - 20 ); + love.graphics.print( VERSION_STRING, love.graphics.getWidth() - DEFAULT_FONT:getWidth( VERSION_STRING ) - 10, love.graphics.getHeight() - 20 ); + end + + return self; +end + +return LoadingScreen; diff --git a/src/screens/MainScreen.lua b/src/screens/MainScreen.lua index c41623d..278aedd 100644 --- a/src/screens/MainScreen.lua +++ b/src/screens/MainScreen.lua @@ -3,21 +3,22 @@ local Screen = require('lib.screenmanager.Screen'); local LogReader = require('src.logfactory.LogReader'); local LogLoader = require('src.logfactory.LogLoader'); local Camera = require('src.ui.CamWrapper'); -local ConfigReader = require('src.conf.ConfigReader'); -local AuthorManager = require('src.AuthorManager'); +local AuthorManager = require('src.authors.AuthorManager'); local FileManager = require('src.FileManager'); local Graph = require('src.graph.Graph'); -local FilePanel = require('src.ui.components.FilePanel'); +local FilePanel = require('src.ui.FilePanel'); local Timeline = require('src.ui.Timeline'); local InputHandler = require('src.InputHandler'); +local RepositoryInfos = require('src.RepositoryInfos'); -- ------------------------------------------------ -- Controls -- ------------------------------------------------ -local toggleAuthors; +local toggleAuthorIcons; +local toggleAuthorLabels; local toggleFilePanel; -local toggleLabels; +local toggleFileLabels; local toggleTimeline; local toggleSimulation; @@ -29,14 +30,14 @@ local toggleFullscreen; local exit; -local camera_zoomIn; -local camera_zoomOut; -local camera_rotateL; -local camera_rotateR; -local camera_n; -local camera_s; -local camera_e; -local camera_w; +local cameraZoomIn; +local cameraZoomOut; +local cameraRotateL; +local cameraRotateR; +local cameraN; +local cameraS; +local cameraE; +local cameraW; -- ------------------------------------------------ -- Module @@ -44,6 +45,12 @@ local camera_w; local MainScreen = {}; +-- ------------------------------------------------ +-- Constants +-- ------------------------------------------------ + +local FIXED_TIMESTEP = 0.016; + -- ------------------------------------------------ -- Constructor -- ------------------------------------------------ @@ -56,20 +63,21 @@ function MainScreen.new() local filePanel; local timeline; local log; + local config; -- ------------------------------------------------ -- Private Functions -- ------------------------------------------------ --- - -- Assigns keybindings loaded from the config file to a - -- local variable for faster access. - -- @param config + -- Assigns keybindings loaded from the config file to a local variable for + -- faster access. -- - local function assignKeyBindings(config) - toggleAuthors = config.keyBindings.toggleAuthors; + local function assignKeyBindings() + toggleAuthorIcons = config.keyBindings.toggleAuthorIcons; + toggleAuthorLabels = config.keyBindings.toggleAuthorLabels; toggleFilePanel = config.keyBindings.toggleFileList; - toggleLabels = config.keyBindings.toggleLabels; + toggleFileLabels = config.keyBindings.toggleFileLabels; toggleTimeline = config.keyBindings.toggleTimeline; toggleSimulation = config.keyBindings.toggleSimulation; @@ -81,36 +89,40 @@ function MainScreen.new() exit = config.keyBindings.exit; - camera_zoomIn = config.keyBindings.camera_zoomIn; - camera_zoomOut = config.keyBindings.camera_zoomOut; - camera_rotateL = config.keyBindings.camera_rotateL; - camera_rotateR = config.keyBindings.camera_rotateR; - camera_n = config.keyBindings.camera_n; - camera_s = config.keyBindings.camera_s; - camera_e = config.keyBindings.camera_e; - camera_w = config.keyBindings.camera_w; + cameraZoomIn = config.keyBindings.camera_zoomIn; + cameraZoomOut = config.keyBindings.camera_zoomOut; + cameraRotateL = config.keyBindings.camera_rotateL; + cameraRotateR = config.keyBindings.camera_rotateR; + cameraN = config.keyBindings.camera_n; + cameraS = config.keyBindings.camera_s; + cameraE = config.keyBindings.camera_e; + cameraW = config.keyBindings.camera_w; end - local function controlCamera(dt) - if InputHandler.isDown(camera_zoomIn) then - camera:zoom(dt, 1); - elseif InputHandler.isDown(camera_zoomOut) then - camera:zoom(dt, -1); + --- + -- Updates the camera controls. + -- @param dt (number) Time since the last update in seconds. + -- + local function controlCamera( dt ) + if InputHandler.isDown( cameraZoomIn ) then + camera:zoom( dt, 1 ); + elseif InputHandler.isDown( cameraZoomOut ) then + camera:zoom( dt, -1 ); end - if InputHandler.isDown(camera_rotateL) then - camera:rotate(dt, -1); - elseif InputHandler.isDown(camera_rotateR) then - camera:rotate(dt, 1); + if InputHandler.isDown( cameraRotateL ) then + camera:rotate( dt, -1 ); + elseif InputHandler.isDown( cameraRotateR ) then + camera:rotate( dt, 1 ); end - if InputHandler.isDown(camera_w) then - camera:move(dt, -1, 0); - elseif InputHandler.isDown(camera_e) then - camera:move(dt, 1, 0); + if InputHandler.isDown( cameraW ) then + camera:move( dt, -1, 0 ); + elseif InputHandler.isDown( cameraE ) then + camera:move( dt, 1, 0 ); end - if InputHandler.isDown(camera_n) then - camera:move(dt, 0, -1); - elseif InputHandler.isDown(camera_s) then - camera:move(dt, 0, 1); + if InputHandler.isDown( cameraN ) then + camera:move( dt, 0, -1 ); + elseif InputHandler.isDown( cameraS ) then + camera:move( dt, 0, 1 ); end end @@ -118,127 +130,174 @@ function MainScreen.new() -- Public Functions -- ------------------------------------------------ - function self:init(param) + --- + -- Initialises the MainScreen. + -- @param params (table) A table containing the configuration. + -- + function self:init( params ) + LogLoader.init(); + -- Store the name of the currently displayed log. - log = param.log; + log = params.log; - local config = ConfigReader.init(); - local info = LogLoader.loadInfo(log); + config = params.config; + + -- Load the info file belonging to the git log. + local info = RepositoryInfos.loadInfo( log ); -- Load keybindings. - assignKeyBindings(config); + assignKeyBindings( config ); - AuthorManager.init(info.aliases, info.avatars, config.options.showAuthors); + AuthorManager.init( info.aliases, config.options.showAuthorIcons, config.options.showAuthorLabels ); - -- Create the camera. - camera = Camera.new(); + -- Set custom colors. + FileManager.setColorTable( info.colors ); - -- Load custom colors. - FileManager.setColorTable(info.colors); + -- Create the graph. + graph = Graph.new( config.options.edgeWidth, config.options.showFileLabels ); - graph = Graph.new(config.options.edgeWidth, config.options.showLabels); - graph:register(AuthorManager); - graph:register(camera); + -- Create the camera. + camera = Camera.new(); + camera:setPosition( love.graphics.getWidth() * 0.5, love.graphics.getHeight() * 0.5 ); - -- Initialise LogReader and register observers. - LogReader.init(LogLoader.load(log), config.options.commitDelay, config.options.mode, config.options.autoplay); - LogReader.register(AuthorManager); - LogReader.register(graph); + -- Initialise the LogReader which handles the loading and "playing" of commits from a git log. + LogReader.init( LogLoader.load( log ), config.options.commitDelay, config.options.mode, config.options.autoplay ); - -- Create panel. - filePanel = FilePanel.new(FileManager.draw, FileManager.update, 0, 0, 150, love.graphics.getHeight() - 40); - filePanel:setActive(config.options.showFileList); + -- Create the file panel. + filePanel = FilePanel.new( config.options.showFileList, 0, 0, 150, love.graphics.getHeight() - 40 ); - timeline = Timeline.new(config.options.showTimeline, LogReader.getTotalCommits(), LogReader.getCurrentDate()); + -- Create the timeline. + timeline = Timeline.new( config.options.showTimeline, LogReader.getTotalCommits(), LogReader.getCurrentDate() ); -- Run one complete cycle of garbage collection. - collectgarbage('collect'); + collectgarbage( 'collect' ); end + --- + -- Draws the MainScreen. + -- function self:draw() - camera:draw(function() - graph:draw(camera:getRotation(), camera:getScale()); - AuthorManager.drawLabels(camera:getRotation(), camera:getScale()); + camera:draw( function() + graph:draw( camera:getRotation(), camera:getScale() ); + AuthorManager.draw( camera:getRotation(), camera:getScale() ); end); filePanel:draw(); timeline:draw(); end - function self:update(dt) - LogReader.update(dt); + --- + -- Updates the MainScreen. + -- @param dt (number) The time since the last update in seconds. + -- + function self:update( dt ) + LogReader.update( dt ); - graph:update(dt); + graph:update( FIXED_TIMESTEP ); - AuthorManager.update(dt); - filePanel:update(dt); - timeline:update(dt); - timeline:setCurrentCommit(LogReader.getCurrentIndex()); - timeline:setCurrentDate(LogReader.getCurrentDate()); + AuthorManager.update( FIXED_TIMESTEP, camera:getRotation() ); - controlCamera(dt); + filePanel:setTotalFiles( FileManager.getTotalFiles() ); + filePanel:setSortedList( FileManager.getSortedList() ); + filePanel:update( dt ); - camera:update(dt); + timeline:setCurrentCommit( LogReader.getCurrentIndex() ); + timeline:setCurrentDate( LogReader.getCurrentDate() ); + timeline:update( dt ); + + controlCamera( dt ); + + camera:update( dt ); end + --- + -- Called when the MainScreen closes. + -- function self:close() FileManager.reset(); + graph:reset(); + camera:reset(); end - function self:quit() - if ConfigReader.getConfig('options').removeTmpFiles then - ConfigReader.removeTmpFiles(); - end - end - - function self:keypressed(key) - if InputHandler.isPressed(key, toggleAuthors) then - AuthorManager.setVisible(not AuthorManager.isVisible()); - elseif InputHandler.isPressed(key, toggleFilePanel) then + --- + -- Handle keypressed events. + -- @param key (string) The pressed key. + -- + function self:keypressed( key ) + if InputHandler.isPressed( key, toggleAuthorIcons ) then + AuthorManager.toggleIcons(); + elseif InputHandler.isPressed( key, toggleFilePanel ) then filePanel:toggle(); - elseif InputHandler.isPressed(key, toggleLabels) then + elseif InputHandler.isPressed( key, toggleFileLabels ) then graph:toggleLabels(); - elseif InputHandler.isPressed(key, toggleSimulation) then + elseif InputHandler.isPressed( key, toggleAuthorLabels ) then + AuthorManager.toggleLabels(); + elseif InputHandler.isPressed( key, toggleSimulation ) then LogReader.toggleSimulation(); - elseif InputHandler.isPressed(key, toggleRewind) then + elseif InputHandler.isPressed( key, toggleRewind ) then LogReader.toggleRewind(); - elseif InputHandler.isPressed(key, loadNextCommit) then + elseif InputHandler.isPressed( key, loadNextCommit ) then LogReader.loadNextCommit(); - elseif InputHandler.isPressed(key, loadPrevCommit) then + elseif InputHandler.isPressed( key, loadPrevCommit ) then LogReader.loadPrevCommit(); - elseif InputHandler.isPressed(key, toggleFullscreen) then - love.window.setFullscreen(not love.window.getFullscreen()); - elseif InputHandler.isPressed(key, toggleTimeline) then + elseif InputHandler.isPressed( key, toggleFullscreen ) then + love.window.setFullscreen( not love.window.getFullscreen() ); + elseif InputHandler.isPressed( key, toggleTimeline ) then timeline:toggle(); - elseif InputHandler.isPressed(key, exit) then - ScreenManager.switch('selection', { log = log }); + elseif InputHandler.isPressed( key, exit ) then + love.window.setFullscreen( false ); + ScreenManager.switch( 'loading', { log = log, config = config } ); end end - function self:mousepressed(x, y, b) - local pos = timeline:getCommitAt(x, y); + --- + -- Handles mousepressed events. + -- @param x (number) The position of the mouse click along the x-axis. + -- @param _ (number) The position of the mouse click along the y-axis (unused). + -- + function self:mousepressed( x, _ ) + local pos = timeline:getCommitAt( x ); if pos then - LogReader.setCurrentIndex(pos); + LogReader.setCurrentIndex( pos ); end end - function self:mousemoved(x, y, dx, dy) - if love.mouse.isDown(1) then - camera:move(love.timer.getDelta(), dx * 0.5, dy * 0.5); + --- + -- Handles mousemoved events + -- @param x (number) Mouse x position. + -- @param y (number) Mouse y position. + -- @param dx (number) The amount moved along the x-axis since the last time + -- love.mousemoved was called. + -- @param dy (number) The amount moved along the y-axis since the last time + -- love.mousemoved was called. + -- + function self:mousemoved( _, _, dx, dy ) + if love.mouse.isDown( 1 ) then + camera:move( love.timer.getDelta(), dx * 0.5, dy * 0.5 ); end end - function self:wheelmoved(x, y) + --- + -- Handles mouse wheel events. + -- @param x (number) Amount of horizontal mouse wheel movement. + -- @param y (number) Amount of vertical mouse wheel movement. + -- + function self:wheelmoved( x, y ) local mx, my = love.mouse.getPosition(); - if filePanel:intersects(mx, my) then - filePanel:wheelmoved(x, y); + if filePanel:intersects( mx, my ) then + filePanel:scroll( x, y ); else - camera:zoom(love.timer.getDelta(), y); + camera:zoom( love.timer.getDelta(), y ); end end - function self:resize(nx, ny) - timeline:resize(nx, ny); + --- + -- Handles resize events called when the screen size changes. + -- @param w (number) The new width, in pixels. + -- @param h (number) The new height, in pixels. + -- + function self:resize( w, h ) + timeline:resize( w, h ); end return self; diff --git a/src/screens/SelectionScreen.lua b/src/screens/SelectionScreen.lua index d080cd4..9e62191 100644 --- a/src/screens/SelectionScreen.lua +++ b/src/screens/SelectionScreen.lua @@ -1,44 +1,35 @@ local ScreenManager = require('lib.screenmanager.ScreenManager'); local Screen = require('lib.screenmanager.Screen'); -local LogCreator = require('src.logfactory.LogCreator'); -local LogLoader = require('src.logfactory.LogLoader'); -local ButtonList = require('src.ui.ButtonList'); -local Button = require('src.ui.components.Button'); -local Header = require('src.ui.components.Header'); -local StaticPanel = require('src.ui.components.StaticPanel'); -local ConfigReader = require('src.conf.ConfigReader'); -local InputHandler = require('src.InputHandler'); -local OpenFolderCommand = require('src.ui.commands.OpenFolderCommand'); -local RefreshLogCommand = require('src.ui.commands.RefreshLogCommand'); -local WatchCommand = require('src.ui.commands.WatchCommand'); local Resources = require('src.Resources'); +local RepositoryHandler = require('src.conf.RepositoryHandler'); -- ------------------------------------------------ --- Module +-- Constants -- ------------------------------------------------ -local SelectionScreen = {}; +local EDGE_COLOR = { 60, 60, 60, 255 }; --- ------------------------------------------------ --- Constants --- ------------------------------------------------ +local SPRITE_SIZE = 24; +local SPRITE_SCALE_FACTOR = SPRITE_SIZE / 256; +local SPRITE_OFFSET = 128; -local TEXT_FONT = Resources.loadFont('SourceCodePro-Medium.otf', 15); -local DEFAULT_FONT = Resources.loadFont('default', 12); +local LABEL_FONT = Resources.loadFont( 'SourceCodePro-Medium.otf', 20 ); +local DEFAULT_FONT = Resources.loadFont( 'default', 12 ); +local FILE_SPRITE = Resources.loadImage( 'file.png' ); -local BUTTON_OK = 'Ok'; -local BUTTON_HELP = 'Help (online)'; +local MESSAGE_FONT = Resources.loadFont( 'SourceCodePro-Medium.otf', 15 ); +local NO_REPO_MESSAGE = "Add a repository by dragging its folder on this window!"; +local CONFIG_FOLDER_MESSAGE = "Click on the node above to open the config folder!"; -local URL_INSTRUCTIONS = 'https://github.com/rm-code/logivi#generating-git-logs-automatically'; +local HAND_CURSOR = love.mouse.getSystemCursor( 'hand' ); -local WARNING_TITLE_NO_GIT = 'Git is not available'; -local WARNING_MESSAGE_NO_GIT = 'LoGiVi can\'t find git in your PATH. This means LoGiVi won\'t be able to create git logs automatically, but can still be used to view pre-generated logs.'; +local VERSION_STRING = string.format( 'Version %s', getVersion() ); -local WARNING_TITLE_NO_REPO = 'Not a valid git repository'; -local WARNING_MESSAGE_NO_REPO = 'The path "%s" does not point to a valid git repository. Make sure you have specified the full path in the settings file.'; +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ -local UI_ELEMENT_PADDING = 20; -local UI_ELEMENT_MARGIN = 5; +local SelectionScreen = {}; -- ------------------------------------------------ -- Constructor @@ -47,69 +38,50 @@ local UI_ELEMENT_MARGIN = 5; function SelectionScreen.new() local self = Screen.new(); + local graph; + local colors; local config; - local logList; - - local buttonList; - local buttons; - local header; - local panel; - local info = {}; + local timer = 0; + local alpha = 0; -- ------------------------------------------------ -- Private Functions -- ------------------------------------------------ --- - -- Checks if git is available and attempts to create git logs based on the - -- list of repositories read from the user's config file. - -- @param options + -- Returns gradually changing values between 0 and 255 which can be used to + -- make elements slowly pulsate. + -- @param dt (number) The time since the last update in seconds. + -- @return (number) A value between 0 and 255. -- - local function createGitLogs(config) - -- Exit early if git isn't available. - if not LogCreator.isGitAvailable() then - -- Show a warning to the user. - local pressedbutton = love.window.showMessageBox(WARNING_TITLE_NO_GIT, WARNING_MESSAGE_NO_GIT, { BUTTON_OK, BUTTON_HELP, enterbutton = 1, escapebutton = 1 }, 'warning', false); - if pressedbutton == 2 then - love.system.openURL(URL_INSTRUCTIONS); - end - return; - end - - for name, path in pairs(config.repositories) do - -- Check if the path points to a valid git repository before attempting - -- to create a git log and the info file for it. - if LogCreator.isGitRepository(path) then - LogCreator.createGitLog(name, path); - LogCreator.createInfoFile(name, path); - else - love.window.showMessageBox(WARNING_TITLE_NO_REPO, string.format(WARNING_MESSAGE_NO_REPO, path), 'warning', false); - end + local function pulsate( dt ) + timer = timer + dt; + local sin = math.sin( timer ); + if sin < 0 then + timer = 0; + sin = 0; end + return sin * 255; end --- - -- Updates the project's window settings based on the config file. - -- @param options + -- Switches to fullscreen and loads the MainScreen. + -- @param name (string) The name of the log to watch. -- - local function setWindowMode(options) - local _, _, flags = love.window.getMode(); - - -- Only update the window when the values are different from the ones set in the config file. - if flags.fullscreen ~= options.fullscreen or flags.fullscreentype ~= options.fullscreenType or - flags.vsync ~= options.vsync or flags.msaa ~= options.msaa or flags.display ~= options.display then - - flags.fullscreen = options.fullscreen; - flags.fullscreentype = options.fullscreenType; - flags.vsync = options.vsync; - flags.msaa = options.msaa; - flags.display = options.display; - - love.window.setMode(options.screenWidth, options.screenHeight, flags); + local function watchLog( name ) + love.window.setFullscreen( config.options.fullscreen, config.options.fullscreenType ); + ScreenManager.switch( 'main', { log = name, config = config } ); + end - local sw, sh = love.window.getDesktopDimensions(); - love.window.setPosition(sw * 0.5 - love.graphics.getWidth() * 0.5, sh * 0.5 - love.graphics.getHeight() * 0.5); + --- + -- Changes the icon of the mouse cursor based on the element it is above. + -- + local function updateMouseCursor() + if graph:getNodeAt( love.mouse.getX(), love.mouse.getY(), 20 ) then + love.mouse.setCursor( HAND_CURSOR ); + else + love.mouse.setCursor(); end end @@ -117,136 +89,95 @@ function SelectionScreen.new() -- Public Functions -- ------------------------------------------------ - function self:init(param) - config = ConfigReader.init(); - - -- Set the background color based on the option in the config file. - love.graphics.setBackgroundColor(config.options.backgroundColor); - setWindowMode(config.options); - - -- Create git logs for repositories specified in the config file. - createGitLogs(config); - - -- Intitialise LogLoader. - logList = LogLoader.init(); - - -- A scrollable list of buttons which can be used to select a certain log. - buttonList = ButtonList.new(UI_ELEMENT_PADDING, UI_ELEMENT_PADDING, UI_ELEMENT_MARGIN); - buttonList:init(self, logList); - - -- Load info about currently selected log. - info = LogLoader.loadInfo(param and param.log or logList[1].name); - - local sw, sh = love.graphics.getDimensions(); - buttons = { - Button.new(OpenFolderCommand.new(love.filesystem.getSaveDirectory()), 'Open', UI_ELEMENT_PADDING + (2 * UI_ELEMENT_MARGIN) + 220, sh - UI_ELEMENT_PADDING - 10 - UI_ELEMENT_PADDING - 40, 100, 40); - Button.new(WatchCommand.new(self), 'Watch', sw - UI_ELEMENT_PADDING - 10 - 100, sh - UI_ELEMENT_PADDING - 10 - UI_ELEMENT_PADDING - 40, 100, 40); - Button.new(RefreshLogCommand.new(self), 'Refresh', sw - UI_ELEMENT_PADDING - 20 - 200, sh - UI_ELEMENT_PADDING - 10 - UI_ELEMENT_PADDING - 40, 100, 40); - }; - - header = Header.new(info.name, UI_ELEMENT_PADDING + (2 * UI_ELEMENT_MARGIN) + 200 + 25, UI_ELEMENT_PADDING + 25); - panel = StaticPanel.new(UI_ELEMENT_PADDING + (2 * UI_ELEMENT_MARGIN) + buttonList:getButtonWidth(), UI_ELEMENT_PADDING, sw - (UI_ELEMENT_PADDING + (2 * UI_ELEMENT_MARGIN) + 200) - 20, sh - UI_ELEMENT_PADDING - 40); + --- + -- Initialises the SelectionScreen. + -- @param params (table) A table containing the configuration. + -- + function self:init( params ) + graph = params.graph; + colors = params.colors; + config = params.config; end - function self:update(dt) - buttonList:update(dt); - for i = 1, #buttons do - buttons[i]:update(dt); - end - end + --- + -- Updates the SelectionScreen. + -- @param dt (number) The time since the last update in seconds. + -- + function self:update( dt ) + graph:update( dt ); + alpha = pulsate( dt ); - function self:resize(nw, nh) - panel:setDimensions(nw - (UI_ELEMENT_PADDING + (2 * UI_ELEMENT_MARGIN) + 200) - 20, nh - UI_ELEMENT_PADDING - 40) - buttons[1]:setPosition(UI_ELEMENT_PADDING + (2 * UI_ELEMENT_MARGIN) + 210, nh - UI_ELEMENT_PADDING - 10 - UI_ELEMENT_PADDING - 40); - buttons[2]:setPosition(nw - UI_ELEMENT_PADDING - 10 - 100, nh - UI_ELEMENT_PADDING - 10 - UI_ELEMENT_PADDING - 40); - buttons[3]:setPosition(nw - UI_ELEMENT_PADDING - 20 - 200, nh - UI_ELEMENT_PADDING - 10 - UI_ELEMENT_PADDING - 40); + updateMouseCursor(); end + --- + -- Draws the SelectionScreen. + -- function self:draw() - buttonList:draw(); - - local x = UI_ELEMENT_PADDING + (2 * UI_ELEMENT_MARGIN) + buttonList:getButtonWidth(); - local y = UI_ELEMENT_PADDING; - - panel:draw(); - header:draw(); - - love.graphics.setFont(TEXT_FONT); - love.graphics.print('First commit: ' .. info.firstCommit, x + 25, y + 100); - love.graphics.print('Latest commit: ' .. info.latestCommit, x + 25, y + 125); - love.graphics.print('Total commits: ' .. info.totalCommits, x + 25, y + 150); - - for i = 1, #buttons do - buttons[i]:draw(); + graph:draw( function( node ) + local x, y = node:getPosition(); + love.graphics.setColor( colors[node:getID()] ); + love.graphics.draw( FILE_SPRITE, x, y, 0, SPRITE_SCALE_FACTOR, SPRITE_SCALE_FACTOR, SPRITE_OFFSET, SPRITE_OFFSET ); + love.graphics.setColor( 255, 255, 255 ); + love.graphics.setFont( LABEL_FONT ); + love.graphics.print( node:getID(), x, y, 0, 1, 1, -16, -16 ); + love.graphics.setFont( DEFAULT_FONT ); + end, + function( edge ) + love.graphics.setColor( EDGE_COLOR ); + love.graphics.setLineWidth( 5 ); + love.graphics.line( edge.origin:getX(), edge.origin:getY(), edge.target:getX(), edge.target:getY() ); + love.graphics.setLineWidth( 1 ); + love.graphics.setColor( 255, 255, 255, 255 ); + end); + + -- Shows info text if no repository can be found. + if not RepositoryHandler.hasRepositories() then + love.graphics.setFont( MESSAGE_FONT ); + love.graphics.setColor( 255, 255, 255, alpha ); + love.graphics.print( NO_REPO_MESSAGE, love.graphics.getWidth() * 0.5 - MESSAGE_FONT:getWidth( NO_REPO_MESSAGE ) * 0.5, love.graphics.getHeight() * 0.5 - 60 ); + love.graphics.print( CONFIG_FOLDER_MESSAGE, love.graphics.getWidth() * 0.5 - MESSAGE_FONT:getWidth( CONFIG_FOLDER_MESSAGE ) * 0.5, love.graphics.getHeight() * 0.5 + 60 - MESSAGE_FONT:getHeight( CONFIG_FOLDER_MESSAGE ) ); + love.graphics.setFont( DEFAULT_FONT ); + love.graphics.setColor( 255, 255, 255, 255 ); end - love.graphics.setFont(DEFAULT_FONT); - love.graphics.print('Work in Progress (v' .. getVersion() .. ')', love.graphics.getWidth() - 180, love.graphics.getHeight() - UI_ELEMENT_PADDING); - end - - function self:watchLog() - ScreenManager.switch('main', { log = info.name }); - end - - function self:refreshLog() - if info.name and LogCreator.isGitAvailable() and config.repositories[info.name] then - local forceOverwrite = true; - LogCreator.createGitLog(info.name, config.repositories[info.name], forceOverwrite); - LogCreator.createInfoFile(info.name, config.repositories[info.name], forceOverwrite); - info = LogLoader.loadInfo(info.name); - end - end - - function self:selectLog(name) - info = LogLoader.loadInfo(name); - header = Header.new(info.name, UI_ELEMENT_PADDING + (2 * UI_ELEMENT_MARGIN) + 200 + 25, UI_ELEMENT_PADDING + 25); - end - - function self:mousepressed(x, y, b) - for i = 1, #buttons do - buttons[i]:mousepressed(x, y, b); - end - buttonList:mousepressed(x, y, b); + love.graphics.setColor( 255, 255, 255, 100 ); + love.graphics.print( VERSION_STRING, love.graphics.getWidth() - DEFAULT_FONT:getWidth( VERSION_STRING ) - 10, love.graphics.getHeight() - 20 ); + love.graphics.setColor( 255, 255, 255, 255 ); end - function self:mousereleased(x, y, b) - for i = 1, #buttons do - buttons[i]:mousereleased(x, y, b); + --- + -- Handles mousereleased events. + -- @param x (number) Mouse x position, in pixels. + -- @param y (number) Mouse y position, in pixels. + -- + function self:mousereleased( mx, my ) + local node = graph:getNodeAt( mx, my, 30 ); + if node then + if node:getID() == '' then + love.system.openURL( 'file://' .. love.filesystem.getSaveDirectory() ); + else + watchLog( node:getID() ); + end end end - function self:wheelmoved(x, y) - buttonList:wheelmoved(x, y); + --- + -- Handles directorydropped events. When the user drags a folder on the + -- application window the InputPanel is loaded. + -- @param path (string) The path of the folder dropped on the window. + -- + function self:directorydropped( path ) + ScreenManager.push( 'input', path, { config = config } ); end - function self:keypressed(key) - if InputHandler.isPressed(key, config.keyBindings.exit) then + --- + -- Handles keypressed events. + -- @param key (string) The pressed key. + -- + function self:keypressed( key ) + if key == 'escape' then love.event.quit(); - elseif InputHandler.isPressed(key, config.keyBindings.toggleFullscreen) then - love.window.setFullscreen(not love.window.getFullscreen()); - end - end - - function self:directorydropped(path) - local temporaryConfig = {}; - local name = path:match("/?([^/]+)$"); -- Use the folder's name to store the repo. - temporaryConfig.repositories = { - [name] = path - }; - - createGitLogs(temporaryConfig); - - -- Intitialise LogLoader. - logList = LogLoader.init(); - - -- A scrollable list of buttons which can be used to select a certain log. - buttonList = ButtonList.new(UI_ELEMENT_PADDING, UI_ELEMENT_PADDING, UI_ELEMENT_MARGIN); - buttonList:init(self, logList); - end - - function self:quit() - if ConfigReader.getConfig('options').removeTmpFiles then - ConfigReader.removeTmpFiles(); end end diff --git a/src/ui/ButtonList.lua b/src/ui/ButtonList.lua deleted file mode 100644 index a69d1b5..0000000 --- a/src/ui/ButtonList.lua +++ /dev/null @@ -1,87 +0,0 @@ -local Button = require('src.ui.components.Button'); -local SelectItemCommand = require('src.ui.commands.SelectItemCommand'); - --- ------------------------------------------------ --- Module --- ------------------------------------------------ - -local ButtonList = {}; - --- ------------------------------------------------ --- Constructor --- ------------------------------------------------ - -function ButtonList.new(offsetX, offsetY, margin) - local self = {}; - - local buttons; - - local scrollSpeed = 20; - local buttonW = 200; - local buttonH = 40; - local listLength = 0; - - -- ------------------------------------------------ - -- Public Functions - -- ------------------------------------------------ - - function self:init(screen, logList) - buttons = {}; - for i, log in ipairs(logList) do - buttons[#buttons + 1] = Button.new(SelectItemCommand.new(screen, log.name), - log.name, offsetX, offsetY + (i - 1) * (buttonH) + margin * (i - 1), buttonW, buttonH); - end - - listLength = listLength + offsetY + (#buttons - 1) * (buttonH) + margin * (#buttons - 1); - end - - function self:draw() - love.graphics.setScissor(offsetX, offsetY, buttonW, love.graphics.getHeight() - offsetY * 3); - for _, button in ipairs(buttons) do - button:draw(scrollOffset); - end - love.graphics.setScissor(); - end - - function self:update(dt) - for _, button in ipairs(buttons) do - button:update(dt); - end - end - - function self:scroll(mx, my, scrollOffset) - -- Deactivate scrolling if the list is smaller than the screen - if listLength < love.graphics.getHeight() - offsetY * 2 then - return; - end - - for _, button in ipairs(buttons) do - local px, py = button:getPosition(); - button:setPosition(px, py + scrollOffset); - end - end - - function self:mousepressed(x, y, b) - for _, button in ipairs(buttons) do - button:mousepressed(x, y, b); - end - end - - function self:wheelmoved(x, y) - if offsetX < love.mouse.getX() and offsetX + buttonW > love.mouse.getX() then - if y < 0 then - self:scroll(x, y, scrollSpeed); - else - self:scroll(x, y, -scrollSpeed); - end - end - end - - function self:getButtonWidth() - return buttonW; - end - - return self; -end - -return ButtonList; diff --git a/src/ui/CamWrapper.lua b/src/ui/CamWrapper.lua index 86cffe2..361fdfe 100644 --- a/src/ui/CamWrapper.lua +++ b/src/ui/CamWrapper.lua @@ -1,4 +1,6 @@ local Camera = require('lib.camera.Camera'); +local Messenger = require('src.messenger.Messenger'); +local Utility = require( 'src.Utility' ); -- ------------------------------------------------ -- Module @@ -10,6 +12,8 @@ local CamWrapper = {}; -- Constants -- ------------------------------------------------ +local EVENT = require('src.messenger.Event'); + local CAMERA_ROTATION_SPEED = 0.6; local CAMERA_TRANSLATION_SPEED = 400; local CAMERA_TRACKING_SPEED = 2; @@ -26,56 +30,52 @@ local GRAPH_PADDING = 100; function CamWrapper.new() local self = {}; - local camera = Camera.new(); -- The actual camera object. - local cx, cy = 0, 0; - local ox, oy = 0, 0; - local gx, gy = 0, 0; - local gw, gh = 0, 0; + local camera = Camera.new(); + local currentX, currentY = 0, 0; -- The actual position of the camera. + local targetX, targetY = 0, 0; -- The desired coordinates. + local graphCenterX, graphCenterY = 0, 0; + local graphWidth, graphHeight = 0, 0; local zoom = 1; local manualZoom = 0; + local subscriptions = {}; + -- ------------------------------------------------ -- Private Functions -- ------------------------------------------------ - local function clamp(min, val, max) - return math.max(min, math.min(val, max)); - end - - local function lerp(a, b, t) - return a + (b - a) * t; - end - --- -- Updates the position on which the camera offset builds. - -- @param ngx - -- @param ngy + -- @param ngx (number) The graph's center along the x-axis. + -- @param ngy (number) The graph's center along the y-axis. -- - local function updateCenter(ngx, ngy) - gx, gy = ngx, ngy; + local function updateCenter( ngx, ngy ) + graphCenterX, graphCenterY = ngx, ngy; end --- - -- Updates the dimensions of the graph and adds a padding value. - -- @param minX - -- @param maxX - -- @param minY - -- @param maxY + -- Updates the dimensions (width and height) of the graph. + -- @param minX (number) The minimum coordinate of the graph along the x-axis. + -- @param maxX (number) The maximum coordinate of the graph along the x-axis. + -- @param minY (number) The minimum coordinate of the graph along the y-axis. + -- @param maxY (number) The maximum coordinate of the graph along the y-axis. -- - local function updateGraphDimensions(minX, maxX, minY, maxY) - gw, gh = maxX - minX, maxY - minY; + local function updateGraphDimensions( minX, maxX, minY, maxY ) + graphWidth, graphHeight = maxX - minX, maxY - minY; end --- -- Calculates the automatic zoom factor needed to fit the whole graph on -- the user's screen. - -- - local function calculateAutoZoom(rot) - local w, h = GRAPH_PADDING + gw, GRAPH_PADDING + gh; + -- @param rot (number) The current rotation of the camera. + -- @return (number) Either the width or height to use for the zoom. + local function calculateAutoZoom( rot ) + local w, h = GRAPH_PADDING + graphWidth, GRAPH_PADDING + graphHeight; local sw, sh = love.graphics.getDimensions(); - local rw = h * math.abs(math.sin(rot)) + w * math.abs(math.cos(rot)); - local rh = h * math.abs(math.cos(rot)) + w * math.abs(math.sin(rot)); + -- Take rotation of the graph into account. + local rw = h * math.abs( math.sin( rot )) + w * math.abs( math.cos( rot )); + local rh = h * math.abs( math.cos( rot )) + w * math.abs( math.sin( rot )); -- Calculate the zoom factors for both width and height and use the -- smaller one to zoom. @@ -88,68 +88,108 @@ function CamWrapper.new() -- Public Functions -- ------------------------------------------------ - function self:zoom(dt, dir) - manualZoom = manualZoom + (dir * CAMERA_ZOOM_SPEED) * dt; + --- + -- Zooms the camera. + -- @param dt (number) Time since the last update in seconds. + -- @param dir (number) The direction in which the camera should be zoomed. + -- + function self:zoom( dt, dir ) + manualZoom = manualZoom + ( dir * CAMERA_ZOOM_SPEED ) * dt; end - function self:rotate(dt, dir) - camera:rotate(dir * CAMERA_ROTATION_SPEED * dt); + --- + -- Rotates the camera. + -- @param dt (number) Time since the last update in seconds. + -- @param dir (number) The direction in which the camera should be rotated. + -- + function self:rotate( dt, dir ) + camera:rotate( dir * CAMERA_ROTATION_SPEED * dt ); end - function self:move(dt, dx, dy) - dx = (dx * dt * CAMERA_TRANSLATION_SPEED); - dy = (dy * dt * CAMERA_TRANSLATION_SPEED); - ox = ox + (math.cos(-camera.rot) * dx - math.sin(-camera.rot) * dy); - oy = oy + (math.sin(-camera.rot) * dx + math.cos(-camera.rot) * dy); + --- + -- Moves the camera. + -- @param dt (number) Time since the last update in seconds. + -- @param dx (number) The distance to move along the x-axis. + -- @param dy (number) The distance to move along the y-axis. + -- + function self:move( dt, dx, dy ) + dx = dx * dt * CAMERA_TRANSLATION_SPEED; + dy = dy * dt * CAMERA_TRANSLATION_SPEED; + targetX = targetX + ( math.cos( -camera.rot ) * dx - math.sin( -camera.rot ) * dy ); + targetY = targetY + ( math.sin( -camera.rot ) * dx + math.cos( -camera.rot ) * dy ); end --- -- Processes camera related controls and updates the camera. - -- @param dt + -- @param dt (number) Time since the last update in seconds. -- - function self:update(dt) - local tzoom = calculateAutoZoom(camera.rot); - zoom = lerp(zoom, tzoom, dt * 2); + function self:update( dt ) + local targetZoom = calculateAutoZoom( camera.rot ); + zoom = Utility.lerp( zoom, targetZoom, dt * 2 ); - camera:zoomTo(clamp(CAMERA_MAX_ZOOM, zoom + manualZoom, CAMERA_MIN_ZOOM)); + camera:zoomTo( Utility.clamp( CAMERA_MAX_ZOOM, zoom + manualZoom, CAMERA_MIN_ZOOM )); -- Gradually move the camera to the target position. - cx = lerp(cx, gx + ox, dt * CAMERA_TRACKING_SPEED); - cy = lerp(cy, gy + oy, dt * CAMERA_TRACKING_SPEED); - camera:lookAt(cx, cy); - end - - --- - -- @param func - -- - function self:draw(func) - camera:draw(func); + currentX = Utility.lerp( currentX, graphCenterX + targetX, dt * CAMERA_TRACKING_SPEED ); + currentY = Utility.lerp( currentY, graphCenterY + targetY, dt * CAMERA_TRACKING_SPEED ); + camera:lookAt( currentX, currentY ); end --- - -- Receives events from an observable. - -- @param event - -- @param ... + -- Applies the camera transformation to the scene via a callback function. + -- @param func (function) The function to apply the camera transformations to. -- - function self:receive(event, ...) - if event == 'GRAPH_UPDATE_CENTER' then - updateCenter(...); - elseif event == 'GRAPH_UPDATE_DIMENSIONS' then - updateGraphDimensions(...); - end + function self:draw( func ) + camera:draw( func ); end --- -- Returns the camera's rotation. + -- @return (number) The camera's rotation. -- function self:getRotation() return camera.rot; end + --- + -- Returns the camera's zoom factor. + -- @return (number) The camera's zoom factor. + -- function self:getScale() return camera.scale; end + --- + -- Transforms the camera and sets the position. + -- @param nx (number) The new coordinate along the x-axis. + -- @param ny (number) The new coordinate along the y-axis. + -- + function self:setPosition( nx, ny ) + camera:lookAt( nx, ny ); + currentX, currentY = nx, ny; + end + + --- + -- Removes the camera's subsriptions from the Messenger. + -- + function self:reset() + for _, v in ipairs( subscriptions ) do + Messenger.remove( v ); + end + end + + -- ------------------------------------------------ + -- Observed Events + -- ------------------------------------------------ + + subscriptions[#subscriptions + 1] = Messenger.observe( EVENT.GRAPH_UPDATE_CENTER, function( ... ) + updateCenter( ... ); + end) + + subscriptions[#subscriptions + 1] = Messenger.observe( EVENT.GRAPH_UPDATE_DIMENSIONS, function( ... ) + updateGraphDimensions( ... ); + end) + return self; end diff --git a/src/ui/FilePanel.lua b/src/ui/FilePanel.lua new file mode 100644 index 0000000..915e500 --- /dev/null +++ b/src/ui/FilePanel.lua @@ -0,0 +1,181 @@ +local Utility = require( 'src.Utility' ); + +-- ------------------------------------------------ +-- Module +-- ------------------------------------------------ + +local FilePanel = {}; + +-- ------------------------------------------------ +-- Constants +-- ------------------------------------------------ + +local MAX_VELOCITY = 8; +local SCROLL_SPEED = 2; +local DAMPING = 8; + +local FIRST_ROW_X = 10; +local SECOND_ROW_X = 50; +local VERTICAL_OFFSET = 10; +local LINE_HEIGHT = 20; + +-- ------------------------------------------------ +-- Constructor +-- ------------------------------------------------ + +--- +-- Creates a new Timeline object. +-- @param visible (boolean) Wether to show the timeline. +-- @param x (number) The position of the filepanel along the x-axis. +-- @param y (number) The position of the filepanel along the y-axis. +-- @param w (number) The width of the filepanel. +-- @param h (number) The height of the filepanel. +-- @return (FilePanel) A new FilePanel object. +-- +function FilePanel.new( visible, x, y, w, h ) + local self = {}; + + local scrollVelocity = 0; + local contentOffset = 0; + + local minY, maxY; + + local totalFiles; + local sortedList; + + -- ------------------------------------------------ + -- Private Functions + -- ------------------------------------------------ + + --- + -- Draws the panel's contents. + -- @param cx (number) The position of the filepanel's content along the x-axis. + -- @param cy (number) The position of the filepanel's content along the y-axis. + -- + local function drawPanel( cx, cy ) + love.graphics.print( totalFiles, cx + FIRST_ROW_X, cy + VERTICAL_OFFSET ); + love.graphics.print( 'Files', cx + SECOND_ROW_X, cy + VERTICAL_OFFSET ); + + for i, tbl in ipairs( sortedList ) do + love.graphics.setColor( tbl.color.r, tbl.color.g, tbl.color.b ); + love.graphics.print( tbl.amount, cx + FIRST_ROW_X, cy + VERTICAL_OFFSET + i * LINE_HEIGHT ); + love.graphics.print( tbl.extension, cx + SECOND_ROW_X, cy + VERTICAL_OFFSET + i * LINE_HEIGHT ); + love.graphics.setColor( 255, 255, 255 ); + end + end + + --- + -- Calculates the panel's height. + -- @return (number) The panels height in pixels. + -- + local function calculatePanelHeight() + return VERTICAL_OFFSET + ( #sortedList + 1 ) * LINE_HEIGHT; + end + + -- ------------------------------------------------ + -- Public Functions + -- ------------------------------------------------ + + --- + -- Draws the panel. + -- + function self:draw() + if not visible then + return; + end + + love.graphics.setScissor( x, y, w, h ); + if totalFiles and sortedList then + drawPanel( x, y + contentOffset ); + end + love.graphics.setScissor(); + end + + --- + -- Updates the file panel. + -- @param dt (number) Time since the last update in seconds. + -- + function self:update( dt ) + if not visible then + return; + end + + -- Reduce the scrolling velocity over time. + if scrollVelocity < -0.5 then + scrollVelocity = scrollVelocity + dt * DAMPING; + elseif scrollVelocity > 0.5 then + scrollVelocity = scrollVelocity - dt * DAMPING; + else + scrollVelocity = 0; + end + + -- Clamp velocity to prevent too fast scrolling. + scrollVelocity = Utility.clamp( -MAX_VELOCITY, scrollVelocity, MAX_VELOCITY ); + + -- Update the position of the scrolled content. + contentOffset = contentOffset + scrollVelocity; + + minY, maxY = 0, calculatePanelHeight(); + + if maxY < h then + contentOffset = minY; + elseif y + contentOffset + maxY < y + h then + contentOffset = ( y + h ) - ( y + maxY ); + elseif contentOffset > minY then + contentOffset = minY; + end + end + + --- + -- Scrolls the contents of the file panel. + -- @param _ (number) The scroll speed in x-direction (unused). + -- @param dy (number) The scroll speed in y-direction. + -- + function self:scroll( _, dy ) + if dy < 0 then + scrollVelocity = scrollVelocity > 0 and 0 or scrollVelocity; + scrollVelocity = scrollVelocity - SCROLL_SPEED; + elseif dy > 0 then + scrollVelocity = scrollVelocity < 0 and 0 or scrollVelocity; + scrollVelocity = scrollVelocity + SCROLL_SPEED; + end + end + + --- + -- Checks if the coordinates intersect with the file panel's area. + -- @param cx (number) The position to check for along the x-axis. + -- @param cy (number) The position to check for along the y-axis. + -- @return (boolean) True if the specified coordinates intersect + -- the panel's area. + -- + function self:intersects( cx, cy ) + return x < cx and x + w > cx and y < cy and y + h > cy; + end + + --- + -- Toggles the file panel. + -- + function self:toggle() + visible = not visible; + end + + --- + -- Sets the sorted file list to use for drawing. + -- @param nsortedList (table) The sorted list of files in the graph. + -- + function self:setSortedList( nsortedList ) + sortedList = nsortedList; + end + + --- + -- Sets the total amount of files in the graph. + -- @param ntotalFiles (number) The total amount of files in the graph. + -- + function self:setTotalFiles( ntotalFiles ) + totalFiles = ntotalFiles; + end + + return self; +end + +return FilePanel; diff --git a/src/ui/Timeline.lua b/src/ui/Timeline.lua index 52f6bd5..ebd27dc 100644 --- a/src/ui/Timeline.lua +++ b/src/ui/Timeline.lua @@ -1,4 +1,5 @@ local Resources = require('src.Resources'); +local Utility = require( 'src.Utility' ); -- ------------------------------------------------ -- Module @@ -12,104 +13,167 @@ local Timeline = {}; local TEXT_FONT = Resources.loadFont('SourceCodePro-Medium.otf', 15); local DEFAULT_FONT = Resources.loadFont('default', 12); -local MARGIN_LEFT = 10; -local MARGIN_RIGHT = 10; -local MARGIN_LABEL = 35; -local HEIGHT = 30; -local TOTAL_STEPS = 128; -local DEFAULT_STEP_SCALE = 0.4; -local HIGHLIGHT_STEP_SCALE = 0.7; -local CURRENT_STEP_SCALE = 0.6; +local MARGIN = 5; +local HEIGHT = 10; +local MOUSE_HOVERING_BOUNDS = 30; + +local FADED_ALPHA = 0; +local VISIBLE_ALPHA = 150; + +local HAND_CURSOR = love.mouse.getSystemCursor( 'hand' ); -- ------------------------------------------------ -- Constructor -- ------------------------------------------------ -function Timeline.new(visible, totalCommits, date) +--- +-- Creates a new Timeline object. +-- @param visible (boolean) Wether to show the timeline. +-- @param totalCommits (number) The total number of commits to display. +-- @param date (string) The starting date. +-- @return (Timeline) A new timeline object. +-- +function Timeline.new( visible, totalCommits, date ) local self = {}; - local steps = totalCommits < TOTAL_STEPS and totalCommits or TOTAL_STEPS; - local stepWidth = (love.graphics.getWidth() - MARGIN_LEFT - MARGIN_RIGHT) / steps; - local currentStep = 0; - local highlighted = -1; + local sw, sh = love.graphics.getDimensions(); + local currentCommit = 0; - local stepSprite = Resources.loadImage('step.png'); - local spritebatch = love.graphics.newSpriteBatch(stepSprite, TOTAL_STEPS, 'dynamic'); - spritebatch:setColor(100, 100, 100, 255); + local alpha = FADED_ALPHA; + local datePosition = sh - HEIGHT - MARGIN - MARGIN; - -- Create the timeline. - for i = 1, steps do - spritebatch:add(MARGIN_LEFT + (i - 1) * stepWidth, love.graphics.getHeight() - (stepSprite:getHeight() * DEFAULT_STEP_SCALE), 0, DEFAULT_STEP_SCALE, DEFAULT_STEP_SCALE); - end + -- ------------------------------------------------ + -- Private Functions + -- ------------------------------------------------ --- - -- Calculates which timestep the user has clicked on and returns the - -- index of the commit which has been mapped to that location. - -- @param x - The clicked x-position + -- Takes a pixel coordinate and tries to map it to a commit at this position + -- on the timeline or close by. + -- @param x (number) The position in pixels. + -- @return (number) The commit at this position on the timeline. -- - local function calculateCommitIndex(x) - return math.floor(totalCommits / (steps / math.floor((x / stepWidth)))); + local function transformPixelsToCommits( x ) + return math.floor(( x - MARGIN ) / (( sw - MARGIN - MARGIN ) / totalCommits )); end --- - -- Maps a certain commit to a timestep. + -- Checks wether the mouse is hovering over the timeline. + -- @return (boolean) True if the mouse is hovering, false otherwise. -- - local function calculateTimelineIndex(cindex) - return math.floor((cindex / totalCommits) * (steps - 1) + 1); + local function mouseOver() + return love.mouse.getY() > sh - MOUSE_HOVERING_BOUNDS; end - function self:draw() - if not visible then return end - love.graphics.draw(spritebatch); + --- + -- Changes the mouse cursor to a hand symbol if the mouse is hovering over + -- the timeline and changes it back if it isn't. + -- + local function updateMouseCursor() + if mouseOver() then + love.mouse.setCursor( HAND_CURSOR ); + else + love.mouse.setCursor(); + end + end - local sw, sh = love.graphics.getDimensions(); - love.graphics.setColor(120, 120, 120, 255); - love.graphics.draw(stepSprite, MARGIN_LEFT + (currentStep - 1) * stepWidth, sh - (stepSprite:getHeight() * CURRENT_STEP_SCALE), 0, CURRENT_STEP_SCALE, CURRENT_STEP_SCALE); + -- ------------------------------------------------ + -- Public Functions + -- ------------------------------------------------ - love.graphics.setColor(255, 0, 0); - love.graphics.draw(stepSprite, MARGIN_LEFT + (highlighted - 1) * stepWidth, sh - (stepSprite:getHeight() * HIGHLIGHT_STEP_SCALE), 0, HIGHLIGHT_STEP_SCALE, HIGHLIGHT_STEP_SCALE); + --- + -- Draws the timeline. + -- + function self:draw() + if not visible then + return; + end - love.graphics.setColor(100, 100, 100); - love.graphics.setFont(TEXT_FONT); - love.graphics.print(date, sw * 0.5 - TEXT_FONT:getWidth(date) * 0.5, sh - MARGIN_LABEL); - love.graphics.setFont(DEFAULT_FONT) - love.graphics.setColor(255, 255, 255); + -- Draw the date label. + local labelX = sw * 0.5 - TEXT_FONT:getWidth( date ) * 0.5; + love.graphics.setColor( 0, 0, 0, 210 ); + love.graphics.rectangle( 'fill', labelX - 2, datePosition - 2, TEXT_FONT:getWidth( date ) + 4, TEXT_FONT:getHeight( date ) + 4 ); + love.graphics.setColor( 215, 215, 215, 255 ); + love.graphics.setFont( TEXT_FONT ); + love.graphics.print( date, labelX, datePosition ); + love.graphics.setFont( DEFAULT_FONT ) + love.graphics.setColor( 255, 255, 255, 255 ); + + -- Draw the timeline. + love.graphics.setColor( 215, 215, 215, alpha ); + love.graphics.rectangle( 'line', MARGIN, sh - HEIGHT - MARGIN, sw - ( 2 * MARGIN ), HEIGHT ); + love.graphics.setColor( 200, 200, 200, alpha ); + love.graphics.rectangle( 'fill', MARGIN, sh - HEIGHT - MARGIN, ( sw - ( 2 * MARGIN )) * ( currentCommit / totalCommits ), HEIGHT ); + love.graphics.setColor( 255, 255, 255, 255 ); end - function self:update(dt) - if love.mouse.getY() > love.graphics.getHeight() - HEIGHT then - highlighted = math.floor(love.mouse.getX() / stepWidth); - else - highlighted = -1; + --- + -- Updates the timeline. + -- @param dt (number) Time since the last update in seconds. + -- + function self:update( dt ) + if not visible then + return; end - end - function self:setCurrentCommit(commit) - currentStep = calculateTimelineIndex(commit); - end + -- Update the alpha channel of the timeline and the position of the + -- date label based on wether the mouse is hovering over the timeline + -- or not. + local hover = mouseOver(); + alpha = Utility.lerp( alpha, hover and VISIBLE_ALPHA or FADED_ALPHA, dt * 4 ); + datePosition = Utility.lerp( datePosition, hover and ( sh - TEXT_FONT:getHeight( date ) - HEIGHT - MARGIN - MARGIN ) or ( sh - HEIGHT - MARGIN - MARGIN ), dt * 4 ); - function self:setCurrentDate(ndate) - date = ndate; + updateMouseCursor(); end + --- + -- Toggles the timeline. + -- function self:toggle() visible = not visible; end - function self:getCommitAt(x, y) - if y > love.graphics.getHeight() - HEIGHT then - return calculateCommitIndex(x); - end + --- + -- Updates the screen's dimensions when it has been resized. + -- @param nx (number) The new screen width in pixels. + -- @param ny (number) The new screen width in pixels. + -- + function self:resize( nx, ny ) + sw, sh = nx, ny; + end + + -- ------------------------------------------------ + -- Setters + -- ------------------------------------------------ + + --- + -- Updates the current commit counter. + -- @param commit (number) The index of the currently displayed commit. + -- + function self:setCurrentCommit( commit ) + currentCommit = commit; end - function self:resize(nx, ny) - stepWidth = (nx - MARGIN_LEFT - MARGIN_RIGHT) / steps; + --- + -- Updates the current date. + -- @param ndate (string) The date of the currently displayed commit. + -- + function self:setCurrentDate( ndate ) + date = ndate; + end + + -- ------------------------------------------------ + -- Getters + -- ------------------------------------------------ - -- Recreate the spritebatch when the window is resized. - spritebatch:clear(); - for i = 1, steps do - spritebatch:add(MARGIN_LEFT + (i - 1) * stepWidth, ny - (stepSprite:getHeight() * DEFAULT_STEP_SCALE), 0, DEFAULT_STEP_SCALE, DEFAULT_STEP_SCALE); + --- + -- Returns a commit at a certain position on the timeline or close by. + -- @param x (number) The horizontal screen position in pixels. + -- @return (number) The commit at this position on the timeline. + -- + function self:getCommitAt( x ) + if mouseOver() then + return transformPixelsToCommits( x ); end end diff --git a/src/ui/commands/OpenFolderCommand.lua b/src/ui/commands/OpenFolderCommand.lua deleted file mode 100644 index 01cdbcd..0000000 --- a/src/ui/commands/OpenFolderCommand.lua +++ /dev/null @@ -1,13 +0,0 @@ -local OpenFolderCommand = {}; - -function OpenFolderCommand.new() - local self = {}; - - function self:execute() - love.system.openURL('file://' .. love.filesystem.getSaveDirectory()); - end - - return self; -end - -return OpenFolderCommand; diff --git a/src/ui/commands/RefreshLogCommand.lua b/src/ui/commands/RefreshLogCommand.lua deleted file mode 100644 index 4116386..0000000 --- a/src/ui/commands/RefreshLogCommand.lua +++ /dev/null @@ -1,13 +0,0 @@ -local LoadLogCommand = {}; - -function LoadLogCommand.new(receiver) - local self = {}; - - function self:execute() - receiver:refreshLog(); - end - - return self; -end - -return LoadLogCommand; diff --git a/src/ui/commands/SelectItemCommand.lua b/src/ui/commands/SelectItemCommand.lua deleted file mode 100644 index 6c8e35e..0000000 --- a/src/ui/commands/SelectItemCommand.lua +++ /dev/null @@ -1,13 +0,0 @@ -local SelectLogCommand = {}; - -function SelectLogCommand.new(receiver, var) - local self = {}; - - function self:execute() - receiver:selectLog(var); - end - - return self; -end - -return SelectLogCommand; diff --git a/src/ui/commands/WatchCommand.lua b/src/ui/commands/WatchCommand.lua deleted file mode 100644 index ca63a3e..0000000 --- a/src/ui/commands/WatchCommand.lua +++ /dev/null @@ -1,13 +0,0 @@ -local WatchCommand = {}; - -function WatchCommand.new(receiver) - local self = {}; - - function self:execute() - receiver:watchLog(); - end - - return self; -end - -return WatchCommand; diff --git a/src/ui/components/BaseComponent.lua b/src/ui/components/BaseComponent.lua deleted file mode 100644 index e9e6ff0..0000000 --- a/src/ui/components/BaseComponent.lua +++ /dev/null @@ -1,53 +0,0 @@ -local BaseComponent = {}; - -local function new(t, x, y, w, h) - local self = {}; - - function self:draw() - return; - end - - function self:update(dt) - return; - end - - function self:intersects(cx, cy) - return x < cx and x + w > cx and y < cy and y + h > cy; - end - - function self:mousemoved(mx, my, dx, dy) - return; - end - - function self:mousepressed(mx, my, b) - return; - end - - function self:mousereleased(mx, my, b) - return; - end - - function self:wheelmoved(x, y) - return; - end - - function self:setPosition(nx, ny) - x, y = nx, ny; - end - - function self:setDimensions(nw, nh) - w, h = nw, nh - end - - function self:getPosition() - return x, y; - end - - function self:getDimensions() - return w, h; - end - - return self; -end - -return setmetatable(BaseComponent, { __call = new }); diff --git a/src/ui/components/Button.lua b/src/ui/components/Button.lua deleted file mode 100644 index 93f247c..0000000 --- a/src/ui/components/Button.lua +++ /dev/null @@ -1,46 +0,0 @@ -local BaseComponent = require('src.ui.components.BaseComponent'); -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); -local BoxDecorator = require('src.ui.decorators.BoxDecorator'); -local MouseOverDecorator = require('src.ui.decorators.MouseOverDecorator'); -local Clickable = require('src.ui.decorators.Clickable'); -local TextLabel = require('src.ui.decorators.TextLabel'); -local Resources = require('src.Resources'); - --- ------------------------------------------------ --- Module --- ------------------------------------------------ - -local Button = {}; - --- ------------------------------------------------ --- Constants --- ------------------------------------------------ - -local LABEL_FONT = Resources.loadFont('SourceCodePro-Medium.otf', 20); - --- ------------------------------------------------ --- Constructor --- ------------------------------------------------ - -function Button.new(command, text, x, y, w, h) - local self = BaseDecorator(x, y, w, h); - - local bodyCol = { 60, 60, 60, 255 }; - local outlineCol = { 100, 100, 100, 255 }; - local hlCol = { 255, 255, 255, 100 }; - local textCol = { 200, 200, 200, 255 }; - - local textX = w * 0.5 - LABEL_FONT:getWidth(text) * 0.5; - local textY = h * 0.5 - LABEL_FONT:getHeight() * 0.5; - - self:attach(Clickable(command, 0, 0, 0, 0)); - self:attach(MouseOverDecorator(hlCol, 0, 0, 0, 0)); - self:attach(TextLabel(text, textCol, LABEL_FONT, textX, textY)); - self:attach(BoxDecorator('line', outlineCol, 0, 0, 0, 0)); - self:attach(BoxDecorator('fill', bodyCol, 0, 0, 0, 0)); - self:attach(BaseComponent(x, y, w, h)); - - return self; -end - -return Button; diff --git a/src/ui/components/FilePanel.lua b/src/ui/components/FilePanel.lua deleted file mode 100644 index f382a4a..0000000 --- a/src/ui/components/FilePanel.lua +++ /dev/null @@ -1,31 +0,0 @@ -local BaseComponent = require('src.ui.components.BaseComponent'); -local BoxDecorator = require('src.ui.decorators.BoxDecorator'); -local Resizable = require('src.ui.decorators.Resizable'); -local Draggable = require('src.ui.decorators.Draggable'); -local Scrollable = require('src.ui.decorators.Scrollable'); -local RenderArea = require('src.ui.decorators.RenderArea'); -local Toggleable = require('src.ui.decorators.Toggleable'); - --- ------------------------------------------------ --- Module --- ------------------------------------------------ - -local FilePanel = {}; - --- ------------------------------------------------ --- Constructor --- ------------------------------------------------ - -function FilePanel.new(render, update, x, y, w, h) - local bodyBaseCol = { 0, 0, 0, 0 }; - - local self = Toggleable(); - self:attach(Scrollable(0, 0, 0, 0)); - self:attach(RenderArea(render, update, 2, 2, -2, -2)); - self:attach(BoxDecorator('fill', bodyBaseCol, 0, 0, 0, 0)); - self:attach(BaseComponent(x, y, w, h)); - - return self; -end - -return FilePanel; diff --git a/src/ui/components/Header.lua b/src/ui/components/Header.lua deleted file mode 100644 index b04db55..0000000 --- a/src/ui/components/Header.lua +++ /dev/null @@ -1,32 +0,0 @@ -local BaseComponent = require('src.ui.components.BaseComponent'); -local TextLabel = require('src.ui.decorators.TextLabel'); -local Resources = require('src.Resources'); - --- ------------------------------------------------ --- Module --- ------------------------------------------------ - -local Header = {}; - --- ------------------------------------------------ --- Constants --- ------------------------------------------------ - -local HEADER_FONT = Resources.loadFont('SourceCodePro-Bold.otf', 35); - --- ------------------------------------------------ --- Constructor --- ------------------------------------------------ - -function Header.new(text, x, y, w, h) - local shadowCol = { 0, 0, 0, 100 }; - local textCol = { 255, 100, 100, 255 }; - - local self = TextLabel(text, textCol, HEADER_FONT, 0, 0); - self:attach(TextLabel(text, shadowCol, HEADER_FONT, 5, 5)); - self:attach(BaseComponent(x, y, w, h)); - - return self; -end - -return Header; diff --git a/src/ui/components/StaticPanel.lua b/src/ui/components/StaticPanel.lua deleted file mode 100644 index 882a678..0000000 --- a/src/ui/components/StaticPanel.lua +++ /dev/null @@ -1,28 +0,0 @@ -local BaseComponent = require('src.ui.components.BaseComponent'); -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); -local BoxDecorator = require('src.ui.decorators.BoxDecorator'); - --- ------------------------------------------------ --- Module --- ------------------------------------------------ - -local StaticPanel = {}; - --- ------------------------------------------------ --- Constructor --- ------------------------------------------------ - -function StaticPanel.new(x, y, w, h) - local self = BaseDecorator(x, y, w, h); - - local bodyCol = { 40, 40, 40, 255 }; - local outlineCol = { 80, 80, 80, 255 }; - - self:attach(BoxDecorator('line', outlineCol, 0, 0, 0, 0)); - self:attach(BoxDecorator('fill', bodyCol, 0, 0, 0, 0)); - self:attach(BaseComponent(x, y, w, h)); - - return self; -end - -return StaticPanel; diff --git a/src/ui/decorators/BaseDecorator.lua b/src/ui/decorators/BaseDecorator.lua deleted file mode 100644 index 578a557..0000000 --- a/src/ui/decorators/BaseDecorator.lua +++ /dev/null @@ -1,67 +0,0 @@ -local BaseDecorator = {}; - -local function new() - local self = { - child = nil; - }; - - function self:draw() - self.child:draw(); - end - - function self:update(dt) - self.child:update(dt); - end - - function self:intersects(cx, cy) - return self.child:intersects(cx, cy); - end - - function self:mousemoved(mx, my, dx, dy) - self.child:mousemoved(mx, my, dx, dy); - end - - function self:mousepressed(mx, my, b) - self.child:mousepressed(mx, my, b); - end - - function self:mousereleased(mx, my, b) - self.child:mousereleased(mx, my, b); - end - - function self:attach(nchild) - if not self.child then - self.child = nchild; - else - self.child:attach(nchild); - end - end - - function self:setPosition(nx, ny) - self.child:setPosition(nx, ny); - end - - function self:setDimensions(nw, nh) - self.child:setDimensions(nw, nh) - end - - function self:getPosition() - return self.child:getPosition(); - end - - function self:getDimensions() - return self.child:getDimensions(); - end - - local meta = {}; - - function meta.__index(table, key) - if key ~= 'child' and self.child then - return self.child[key]; - end - end - - return setmetatable(self, meta); -end - -return setmetatable(BaseDecorator, { __call = new }); diff --git a/src/ui/decorators/BoxDecorator.lua b/src/ui/decorators/BoxDecorator.lua deleted file mode 100644 index ad8adb2..0000000 --- a/src/ui/decorators/BoxDecorator.lua +++ /dev/null @@ -1,42 +0,0 @@ -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); - -local BoxDecorator = {}; - ---- --- @param t - The class table. --- @param mode - The draw mode with which to render the box ('line' or 'fill'). --- @param rgba - The color to use when rendering the box. --- @param x - The position of the decorator on the x-axis relative to its parent. --- @param y - The position of the decorator on the y-axis relative to its parent. --- @param w - The width of the decorator relative to its parent. --- @param h - The height of the decorator relative to its parent. --- @param fixedW - Determines wether to lock the width of the decorator or not. --- @param fixedH - Determines wether to lock the height of the decorator or not. --- @param fixedPosX - Determines wether to lock the position of the decorator or not. --- @param fixedPosY - Determines wether to lock the position of the decorator or not. --- -local function new(t, mode, rgba, x, y, w, h, fixedW, fixedH, fixedPosX, fixedPosY) - local self = BaseDecorator(); - - function self:draw() - self.child:draw(); - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - love.graphics.setColor(rgba); - love.graphics.rectangle(mode, px + x, py + y, pw + w, ph + h) - love.graphics.setColor(255, 255, 255, 255); - end - - function self:setDimensions(nw, nh) - local pw, ph = self:getDimensions(); - if fixedW then w = w + (pw - nw) end - if fixedH then h = h + (ph - nh) end - if fixedPosX then x = x - (pw - nw) end - if fixedPosY then y = y - (ph - nh) end - self.child:setDimensions(nw, nh); - end - - return self; -end - -return setmetatable(BoxDecorator, { __call = new }); diff --git a/src/ui/decorators/Clickable.lua b/src/ui/decorators/Clickable.lua deleted file mode 100644 index 186bd06..0000000 --- a/src/ui/decorators/Clickable.lua +++ /dev/null @@ -1,42 +0,0 @@ -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); - -local Clickable = {}; - ---- --- @param t - The class table. --- @param command - The command to call when a click is registered (Expects a command:execute() function). --- @param x - The position of the decorator on the x-axis relative to its parent. --- @param y - The position of the decorator on the y-axis relative to its parent. --- @param w - The width of the decorator relative to its parent. --- @param h - The height of the decorator relative to its parent. --- @param fixedW - Determines wether to lock the width of the decorator or not. --- @param fixedH - Determines wether to lock the height of the decorator or not. --- @param fixedPosX - Determines wether to lock the position of the decorator or not. --- @param fixedPosY - Determines wether to lock the position of the decorator or not. --- -local function new(t, command, x, y, w, h, fixedW, fixedH, fixedPosX, fixedPosY) - local self = BaseDecorator(); - - function self:mousepressed(mx, my, b) - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - if px + x < mx and px + x + pw + w > mx and py + y < my and py + y + ph + h > my then - command:execute(); - else - self.child:mousepressed(mx, my, b); - end - end - - function self:setDimensions(nw, nh) - local pw, ph = self:getDimensions(); - if fixedW then w = w + (pw - nw) end - if fixedH then h = h + (ph - nh) end - if fixedPosX then x = x - (pw - nw) end - if fixedPosY then y = y - (ph - nh) end - self.child:setDimensions(nw, nh); - end - - return self; -end - -return setmetatable(Clickable, { __call = new }); diff --git a/src/ui/decorators/Draggable.lua b/src/ui/decorators/Draggable.lua deleted file mode 100644 index 7de2c55..0000000 --- a/src/ui/decorators/Draggable.lua +++ /dev/null @@ -1,58 +0,0 @@ -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); - -local Draggable = {}; - ---- --- @param t - The class table. --- @param x - The position of the decorator on the x-axis relative to its parent. --- @param y - The position of the decorator on the y-axis relative to its parent. --- @param w - The width of the decorator relative to its parent. --- @param h - The height of the decorator relative to its parent. --- @param fixedW - Determines wether to lock the width of the decorator or not. --- @param fixedH - Determines wether to lock the height of the decorator or not. --- @param fixedPosX - Determines wether to lock the position of the decorator or not. --- @param fixedPosY - Determines wether to lock the position of the decorator or not. --- -local function new(t, x, y, w, h, fixedW, fixedH, fixedPosX, fixedPosY) - local self = BaseDecorator(); - - local drag = false; - - function self:mousepressed(mx, my, b) - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - if b == 1 and px + x < mx and px + x + pw + w > mx and py + y < my and py + y + ph + h > my then - drag = true; - return; - end - self.child:mousepressed(mx, my, b); - end - - function self:mousereleased(mx, my, b) - drag = false; - self.child:mousereleased(mx, my, b); - end - - function self:mousemoved(mx, my, dx, dy) - local px, py = self:getPosition(); - - if drag then - self:setPosition(px + x + dx, py + y + dy); - else - self.child:mousemoved(mx, my, dx, dy) - end - end - - function self:setDimensions(nw, nh) - local pw, ph = self:getDimensions(); - if fixedW then w = w + (pw - nw) end - if fixedH then h = h + (ph - nh) end - if fixedPosX then x = x - (pw - nw) end - if fixedPosY then y = y - (ph - nh) end - self.child:setDimensions(nw, nh); - end - - return self; -end - -return setmetatable(Draggable, { __call = new }); diff --git a/src/ui/decorators/MouseOverDecorator.lua b/src/ui/decorators/MouseOverDecorator.lua deleted file mode 100644 index 9834de7..0000000 --- a/src/ui/decorators/MouseOverDecorator.lua +++ /dev/null @@ -1,62 +0,0 @@ -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); - -local MouseOverDecorator = {}; - ---- --- @param t - The class table. --- @param highlightCol - The color to use when the mouse is over the decorator. --- @param x - The position of the decorator on the x-axis relative to its parent. --- @param y - The position of the decorator on the y-axis relative to its parent. --- @param w - The width of the decorator relative to its parent. --- @param h - The height of the decorator relative to its parent. --- @param fixedW - Determines wether to lock the width of the decorator or not. --- @param fixedH - Determines wether to lock the height of the decorator or not. --- @param fixedPosX - Determines wether to lock the position of the decorator or not. --- @param fixedPosY - Determines wether to lock the position of the decorator or not. --- -local function new(t, highlightCol, x, y, w, h, fixedW, fixedH, fixedPosX, fixedPosY) - local self = BaseDecorator(); - - local mouseOver = true; - - function self:draw() - self.child:draw(); - if mouseOver then - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - love.graphics.setColor(highlightCol); - love.graphics.rectangle('fill', px + x, py + y, pw + w, ph + h); - love.graphics.setColor(255, 255, 255, 255); - end - end - - function self:update(dt) - self:intersects(love.mouse.getPosition()); - self.child:update(dt); - end - - function self:intersects(cx, cy) - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - if px + x < cx and px + x + pw + w > cx and py + y < cy and py + y + ph + h > cy then - mouseOver = true; - return true; - else - mouseOver = false; - return self.child:intersects(cx, cy); - end - end - - function self:setDimensions(nw, nh) - local pw, ph = self:getDimensions(); - if fixedW then w = w + (pw - nw) end - if fixedH then h = h + (ph - nh) end - if fixedPosX then x = x - (pw - nw) end - if fixedPosY then y = y - (ph - nh) end - self.child:setDimensions(nw, nh); - end - - return self; -end - -return setmetatable(MouseOverDecorator, { __call = new }); diff --git a/src/ui/decorators/RenderArea.lua b/src/ui/decorators/RenderArea.lua deleted file mode 100644 index e8141ef..0000000 --- a/src/ui/decorators/RenderArea.lua +++ /dev/null @@ -1,79 +0,0 @@ -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); - -local RenderArea = {}; - ---- --- @param t - The class table. --- @param render - A function which receives the position from the scroll panel when it is called. --- @param x - The position of the decorator on the x-axis relative to its parent. --- @param y - The position of the decorator on the y-axis relative to its parent. --- @param w - The width of the decorator. Determines the wrap limit for the rendered text. --- @param h - The height of the decorator. Determines the scissor area of the decorator. --- @param fixedW - Determines wether to lock the width of the decorator or not. --- @param fixedH - Determines wether to lock the height of the decorator or not. --- @param fixedPosX - Determines wether to lock the position of the decorator or not. --- @param fixedPosY - Determines wether to lock the position of the decorator or not. --- -local function new(t, render, update, x, y, w, h, fixedW, fixedH, fixedPosX, fixedPosY) - local self = BaseDecorator(); - - local ox, oy = 0, 0; - local minX, maxX, minY, maxY; - - function self:draw() - self.child:draw(); - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - love.graphics.setScissor(px + x, py + y, pw + w, ph + h); - render(px + x + ox, py + y + oy); - love.graphics.setScissor(); - end - - function self:update(dt) - self.child:update(dt); - minX, maxX, minY, maxY = update(dt); - end - - function self:getContentOffset() - return ox, oy; - end - - function self:setContentOffset(nox, noy) - ox, oy = nox, noy; - - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - - -- Deactivate scrolling if the text fits into the panel. - if minX and maxX and minY and maxY then - if maxX < pw + w then - ox = minX; - elseif px + x + ox + maxX < px + x + pw + w then - ox = (px + x + pw + w) - (px + x + maxX); - elseif ox > minX then - ox = minX; - end - - if maxY < ph + h then - oy = minY; - elseif py + y + oy + maxY < py + y + ph + h then - oy = (py + y + ph + h) - (py + y + maxY); - elseif oy > minY then - oy = minY; - end - end - end - - function self:setDimensions(nw, nh) - local pw, ph = self:getDimensions(); - if fixedW then w = w + (pw - nw) end - if fixedH then h = h + (ph - nh) end - if fixedPosX then x = x - (pw - nw) end - if fixedPosY then y = y - (ph - nh) end - self.child:setDimensions(nw, nh); - end - - return self; -end - -return setmetatable(RenderArea, { __call = new }); diff --git a/src/ui/decorators/Resizable.lua b/src/ui/decorators/Resizable.lua deleted file mode 100644 index d43e565..0000000 --- a/src/ui/decorators/Resizable.lua +++ /dev/null @@ -1,61 +0,0 @@ -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); - -local Resizable = {}; - ---- --- @param t - The class table. --- @param x - The position of the decorator on the x-axis relative to its parent. --- @param y - The position of the decorator on the y-axis relative to its parent. --- @param w - The width of the decorator relative to its parent. --- @param h - The height of the decorator relative to its parent. --- @param fixedW - Determines wether to lock the width of the decorator or not. --- @param fixedH - Determines wether to lock the height of the decorator or not. --- @param fixedPosX - Determines wether to lock the position of the decorator or not. --- @param fixedPosY - Determines wether to lock the position of the decorator or not. --- -local function new(t, x, y, w, h, fixedW, fixedH, fixedPosX, fixedPosY) - local self = BaseDecorator(); - - local resize = false; - - function self:mousepressed(mx, my, b) - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - if b == 1 and px + x < mx and px + x + pw + w > mx and py + y < my and py + y + ph + h > my then - resize = true; - return; - end - self.child:mousepressed(mx, my, b); - end - - function self:mousereleased(mx, my, b) - resize = false; - self.child:mousereleased(mx, my, b); - end - - function self:mousemoved(mx, my, dx, dy) - local px, py = self:getPosition(); - - if resize then - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - local w, h = mx - px, my - py; - self:setDimensions(w, h); - else - self.child:mousemoved(mx, my, dx, dy) - end - end - - function self:setDimensions(nw, nh) - local pw, ph = self:getDimensions(); - if fixedW then w = w + (pw - nw) end - if fixedH then h = h + (ph - nh) end - if fixedPosX then x = x - (pw - nw) end - if fixedPosY then y = y - (ph - nh) end - self.child:setDimensions(nw, nh); - end - - return self; -end - -return setmetatable(Resizable, { __call = new }); diff --git a/src/ui/decorators/Scrollable.lua b/src/ui/decorators/Scrollable.lua deleted file mode 100644 index 02bd646..0000000 --- a/src/ui/decorators/Scrollable.lua +++ /dev/null @@ -1,76 +0,0 @@ -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); - -local Scrollable = {}; - -local MAX_VELOCITY = 8; -local SCROLL_SPEED = 2; -local DAMPING = 8; - ---- --- @param t - The class table. --- @param x - The position of the decorator on the x-axis relative to its parent. --- @param y - The position of the decorator on the y-axis relative to its parent. --- @param w - The width of the decorator relative to its parent. --- @param h - The height of the decorator relative to its parent. --- @param fixedW - Determines wether to lock the width of the decorator or not. --- @param fixedH - Determines wether to lock the height of the decorator or not. --- @param fixedPosX - Determines wether to lock the position of the decorator or not. --- @param fixedPosY - Determines wether to lock the position of the decorator or not. --- -local function new(t, x, y, w, h, fixedW, fixedH, fixedPosX, fixedPosY) - local self = BaseDecorator(); - - local scrollVelocity = 0; - - function self:update(dt) - -- Reduce the scrolling velocity over time. - if scrollVelocity < -0.5 then - scrollVelocity = scrollVelocity + dt * DAMPING; - elseif scrollVelocity > 0.5 then - scrollVelocity = scrollVelocity - dt * DAMPING; - else - scrollVelocity = 0; - end - - -- Clamp velocity to prevent too fast scrolling. - scrollVelocity = math.max(-MAX_VELOCITY, math.min(scrollVelocity, MAX_VELOCITY)); - - -- Update the position of the scrolled content. - local ox, oy = self.child:getContentOffset(); - self.child:setContentOffset(ox, oy + scrollVelocity); - - self.child:update(dt); - end - - function self:wheelmoved(x, y) - local mx, my = love.mouse.getPosition(); - local px, py = self:getPosition(); - local pw, ph = self:getDimensions(); - - -- Check if the mousepointer is over the scroll panel before applying scroll. - if px + x < mx and px + x + pw + w > mx and py + y < my and py + y + ph + h > my then - if y < 0 then - scrollVelocity = scrollVelocity > 0 and 0 or scrollVelocity; - scrollVelocity = scrollVelocity - SCROLL_SPEED; - elseif y > 0 then - scrollVelocity = scrollVelocity < 0 and 0 or scrollVelocity; - scrollVelocity = scrollVelocity + SCROLL_SPEED; - end - end - - self.child:wheelmoved(mx, my, b); - end - - function self:setDimensions(nw, nh) - local pw, ph = self:getDimensions(); - if fixedW then w = w + (pw - nw) end - if fixedH then h = h + (ph - nh) end - if fixedPosX then x = x - (pw - nw) end - if fixedPosY then y = y - (ph - nh) end - self.child:setDimensions(nw, nh); - end - - return self; -end - -return setmetatable(Scrollable, { __call = new }); diff --git a/src/ui/decorators/TextLabel.lua b/src/ui/decorators/TextLabel.lua deleted file mode 100644 index 11d28f9..0000000 --- a/src/ui/decorators/TextLabel.lua +++ /dev/null @@ -1,39 +0,0 @@ -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); - -local TextLabel = {}; - ---- --- @param t - The class table. --- @param text - The text to display. --- @param rgba - The color to use when rendering text. --- @param font - The font to use when rendering text. --- @param x - The position of the decorator on the x-axis relative to its parent. --- @param y - The position of the decorator on the y-axis relative to its parent. --- @param fixedW - Determines wether to lock the width of the decorator or not. --- @param fixedH - Determines wether to lock the height of the decorator or not. --- @param fixedPosX - Determines wether to lock the position of the decorator or not. --- @param fixedPosY - Determines wether to lock the position of the decorator or not. --- -local function new(t, text, rgba, font, x, y, fixedPosX, fixedPosY) - local self = BaseDecorator(); - - function self:draw() - self.child:draw(); - local px, py = self:getPosition(); - love.graphics.setFont(font); - love.graphics.setColor(rgba); - love.graphics.print(text, px + x, py + y); - love.graphics.setColor(255, 255, 255, 255); - end - - function self:setDimensions(nw, nh) - local pw, ph = self:getDimensions(); - if fixedPosX then x = x - (pw - nw) end - if fixedPosY then y = y - (ph - nh) end - self.child:setDimensions(nw, nh); - end - - return self; -end - -return setmetatable(TextLabel, { __call = new }); diff --git a/src/ui/decorators/Toggleable.lua b/src/ui/decorators/Toggleable.lua deleted file mode 100644 index f0d59ba..0000000 --- a/src/ui/decorators/Toggleable.lua +++ /dev/null @@ -1,78 +0,0 @@ -local BaseDecorator = require('src.ui.decorators.BaseDecorator'); - -local BoxDecorator = {}; - ---- --- Allows to deactivate all public functions inherited from the base decorator. --- -local function new() - local self = BaseDecorator(); - - local active = true; - - function self:draw() - if not active then return end - self.child:draw(); - end - - function self:update(dt) - if not active then return end - self.child:update(dt); - end - - function self:intersects(cx, cy) - if not active then return end - return self.child:intersects(cx, cy); - end - - function self:mousemoved(mx, my, dx, dy) - if not active then return end - self.child:mousemoved(mx, my, dx, dy); - end - - function self:mousepressed(mx, my, b) - if not active then return end - self.child:mousepressed(mx, my, b); - end - - function self:mousereleased(mx, my, b) - if not active then return end - self.child:mousereleased(mx, my, b); - end - - function self:setPosition(nx, ny) - if not active then return end - self.child:setPosition(nx, ny); - end - - function self:setDimensions(nw, nh) - if not active then return end - self.child:setDimensions(nw, nh) - end - - function self:getPosition() - if not active then return end - return self.child:getPosition(); - end - - function self:getDimensions() - if not active then return end - return self.child:getDimensions(); - end - - function self:toggle() - active = not active; - end - - function self:setActive(nactive) - active = nactive; - end - - function self:isActive() - return active; - end - - return self; -end - -return setmetatable(BoxDecorator, { __call = new }); diff --git a/version.lua b/version.lua new file mode 100644 index 0000000..965691b --- /dev/null +++ b/version.lua @@ -0,0 +1,8 @@ +local version = { + major = 1, + minor = 0, + patch = 0, + build = 572, +} + +return string.format( "%d.%d.%d.%d", version.major, version.minor, version.patch, version.build );