diff --git a/CHANGELOG.md b/CHANGELOG.md index c7aa6bb..828cf56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# Version 0xxx - 2015/xx/xx + +### 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 + +### Fixes +- Fixed [#51](https://github.com/rm-code/logivi/issues/51) - Fixed crash caused by faulty variable +- Fixed [#48](https://github.com/rm-code/logivi/issues/48) - Got rid of the timer for color fading +- Fixed [#35](https://github.com/rm-code/logivi/issues/35) - Made large graphs more stable +- Fixed minor issue with folder labels being drawn multiple times per frame + +### Other Changes +- LoGiVi now starts in windowed mode on first start +- Canged design of the file panel to be less intrusive + # Version 0404 - 2015/11/24 ### Additions @@ -67,7 +86,7 @@ - Added config file validation after it has been loaded (Closes [#26](https://github.com/rm-code/logivi/issues/26)) ### Fixes -- Fixed [#33](https://github.com/rm-code/logivi/issues/33) - Ignore lines in config file which aren't formatted correctly +- Fixed [#33](https://github.com/rm-code/logivi/issues/33) - Ignore lines in config file which aren't formatted correctly - Fixed [#32](https://github.com/rm-code/logivi/issues/32) - Resize Timeline when MainScreen is resized - Fixed [#31](https://github.com/rm-code/logivi/issues/31) - Directly pass the repository's path to the git command - Fixed [#30](https://github.com/rm-code/logivi/issues/30) - Ignore files when no changes were applied @@ -95,7 +114,7 @@ - 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 toggling fullscreen mode -- Added a timeline +- 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 @@ -172,7 +191,7 @@ - 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 + - 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 @@ -192,7 +211,7 @@ ### 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 + - 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 @@ -204,4 +223,4 @@ - 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 \ No newline at end of file +- Added warning message if no log file can be found diff --git a/README.md b/README.md index 4a4eed8..bf3a8b7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # LoGiVi -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. +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. ![Example Visualization](https://github.com/rm-code/logivi/wiki/media/logivi_0312.gif) @@ -46,9 +48,6 @@ This will create the file _log.txt_ in the folder you are currently in. Take thi /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. - -# LÖVE Version -Version 0351 and all prior versions of LoGiVi are written for Version 0.9.2 of the LÖVE framework. ___All future versions will be based on LÖVE 0.10.0 (currently unreleased).___ # License diff --git a/conf.lua b/conf.lua index 6473fde..6dbdef4 100644 --- a/conf.lua +++ b/conf.lua @@ -1,6 +1,6 @@ local PROJECT_TITLE = "LoGiVi"; -local PROJECT_VERSION = "0404"; +local PROJECT_VERSION = "0432"; local PROJECT_IDENTITY = "rmcode_LoGiVi"; diff --git a/res/templates/settings.cfg b/res/templates/settings_template.cfg similarity index 97% rename from res/templates/settings.cfg rename to res/templates/settings_template.cfg index c14241c..1bd3565 100644 --- a/res/templates/settings.cfg +++ b/res/templates/settings_template.cfg @@ -18,7 +18,7 @@ backgroundColor = 0, 0, 0 removeTmpFiles = false screenWidth = 0 screenHeight = 0 -fullscreen = true +fullscreen = false fullscreenType = desktop vsync = true msaa = 0 diff --git a/src/Author.lua b/src/Author.lua index 7e45473..3b5739d 100644 --- a/src/Author.lua +++ b/src/Author.lua @@ -82,7 +82,7 @@ function Author.new(name, avatar, cx, cy) -- Public Functions -- ------------------------------------------------ - function self:draw(rotation) + function self:draw(rotation, scale) if active then love.graphics.setLineWidth(BEAM_WIDTH); for i = 1, #links do @@ -94,7 +94,7 @@ function Author.new(name, avatar, cx, cy) 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, 1, LABEL_FONT:getWidth(name) * 0.5, - AVATAR_SIZE); + 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 diff --git a/src/AuthorManager.lua b/src/AuthorManager.lua index fa07828..24c273e 100644 --- a/src/AuthorManager.lua +++ b/src/AuthorManager.lua @@ -95,10 +95,10 @@ end --- -- Draws a list of all authors working on the project. -- -function AuthorManager.drawLabels(rotation) +function AuthorManager.drawLabels(rotation, scale) if visible then for _, author in pairs(authors) do - author:draw(rotation); + author:draw(rotation, scale); end end end diff --git a/src/FileManager.lua b/src/FileManager.lua index 2ec3180..9b48461 100644 --- a/src/FileManager.lua +++ b/src/FileManager.lua @@ -20,14 +20,6 @@ local colors; -- Local Functions -- ------------------------------------------------ ---- --- Returns the extension of a file (or '.?' if it doesn't have one). --- @param fileName --- -local function splitExtension(fileName) - return fileName:match("(%.[^.]+)$") or '.?'; -end - --- -- Takes the extensions list and creates a list -- which is sorted by the amount of files per extension. @@ -73,9 +65,9 @@ end --- -- Adds a new file extension to the list. -- @param fileName +-- @param ext -- -function FileManager.add(fileName) - local ext = splitExtension(fileName); +function FileManager.add(fileName, ext) if not extensions[ext] then extensions[ext] = {}; extensions[ext].extension = ext; @@ -99,9 +91,10 @@ end -- same extension. If there are no more files -- of that extension, it will remove it from -- the list. +-- @param fileName +-- @param ext -- -function FileManager.remove(fileName) - local ext = splitExtension(fileName); +function FileManager.remove(fileName, ext) if not extensions[ext] then error('Tried to remove the non existing file extension "' .. ext .. '".'); end diff --git a/src/conf/ConfigReader.lua b/src/conf/ConfigReader.lua index 00184ca..350f758 100644 --- a/src/conf/ConfigReader.lua +++ b/src/conf/ConfigReader.lua @@ -5,7 +5,7 @@ local ConfigReader = {}; -- ------------------------------------------------ local FILE_NAME = 'settings.cfg'; -local TEMPLATE_PATH = 'res/templates/settings.cfg'; +local TEMPLATE_PATH = 'res/templates/settings_template.cfg'; local INVALID_CONFIG_HEADER = 'Invalid config file'; local MISSING_SECTION_WARNING = 'Seems like the loaded configuration file is missing the [%s] section. The default settings will be used instead.'; diff --git a/src/graph/File.lua b/src/graph/File.lua index d35552e..2c1cc15 100644 --- a/src/graph/File.lua +++ b/src/graph/File.lua @@ -4,51 +4,42 @@ local File = {}; -- Constants -- ------------------------------------------------ -local MOD_TIMER = 2; +local ANIM_TIMER = 3.5; +local FADE_TIMER = 3.0; +local MOD_TIMER = 1.5; + local MOD_COLOR = { - add = { r = 0, g = 255, b = 0 }, - del = { r = 255, g = 0, b = 0 }, - mod = { r = 254, g = 140, b = 0 }, + add = { r = 0, g = 255, b = 0, a = 255 }, + del = { r = 255, g = 0, b = 0, a = 255 }, + mod = { r = 254, g = 140, b = 0, a = 255 }, }; -- ------------------------------------------------ -- Constructor -- ------------------------------------------------ -function File.new(parent, name, color, extension, x, y) +function File.new(posX, posY, defaultColor, extension) local self = {}; - local posX, posY = x, y; + local state; + + -- The target and the current offset from the parent node's position. + -- This is used to arrange the files around a node. local targetOffsetX, targetOffsetY = 0, 0; local currentOffsetX, currentOffsetY = 0, 0; - local fileColor, extension = color, extension; - local currentColor = { r = 0, g = 0, b = 0, a = 255 }; - local modified = false; - local timer = MOD_TIMER; - local fade = false; - local dead = false; + + -- The actual color currently displayed on screen. + local currentColor = {}; -- ------------------------------------------------ -- Private Functions -- ------------------------------------------------ - local function lerp(a, b, t) - return a + (b - a) * t; - end - --- - -- Resets the color values and the timer, and sets - -- the modified flag to false. + -- Linear interpolation between a and b. -- - local function reset() - timer = MOD_TIMER; - modified = false; - fade = false; - dead = false; - currentColor.r = fileColor.r; - currentColor.g = fileColor.g; - currentColor.b = fileColor.b; - currentColor.a = 255; + local function lerp(a, b, t) + return a + (b - a) * t; end --- @@ -60,8 +51,8 @@ function File.new(parent, name, color, extension, x, y) -- @param tarY - The target offset on the y-axis. -- local function animate(dt, tarX, tarY) - currentOffsetX = lerp(currentOffsetX, tarX, dt * 3.5); - currentOffsetY = lerp(currentOffsetY, tarY, dt * 3.5); + currentOffsetX = lerp(currentOffsetX, tarX, dt * ANIM_TIMER); + currentOffsetY = lerp(currentOffsetY, tarY, dt * ANIM_TIMER); end -- ------------------------------------------------ @@ -76,42 +67,32 @@ function File.new(parent, name, color, extension, x, y) function self:update(dt) animate(dt, targetOffsetX, targetOffsetY); - if fade then - currentColor.a = math.min(255, math.max(currentColor.a - 3, 0)); - if currentColor.a <= 0 then - dead = true; - end - return; - end + -- 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); - if modified then - if timer > 0 then - timer = timer - dt; - currentColor.r = lerp(currentColor.r, fileColor.r, dt * 1.5); - currentColor.g = lerp(currentColor.g, fileColor.g, dt * 1.5); - currentColor.b = lerp(currentColor.b, fileColor.b, dt * 1.5); - else - reset(); + -- 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)); + if currentColor.a == 0 then + state = 'dead'; end end end --- - -- Marks the file as modified and changes the - -- current color to the modified color. - -- @param mod + -- 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. -- - function self:modify(mod) - reset(); + function self:setState(mod) + state = mod; - modified = true; currentColor.r = MOD_COLOR[mod].r; currentColor.g = MOD_COLOR[mod].g; currentColor.b = MOD_COLOR[mod].b; - - if mod == 'del' then - fade = true; - end + currentColor.a = MOD_COLOR[mod].a; end -- ------------------------------------------------ @@ -153,7 +134,7 @@ function File.new(parent, name, color, extension, x, y) -- Returns true if the file is marked as dead. -- function self:isDead() - return dead; + return state == 'dead'; end -- ------------------------------------------------ diff --git a/src/graph/Graph.lua b/src/graph/Graph.lua index 3acade2..def9e00 100644 --- a/src/graph/Graph.lua +++ b/src/graph/Graph.lua @@ -153,17 +153,17 @@ function Graph.new(ewidth, showLabels) -- @param path -- @param filename -- - local function applyGitModifier(modifier, path, filename, mode) + local function applyGitModifier(modifier, path, filename, extension, mode) local targetNode = getNode(path); local modifiedFile; if modifier == MOD_ADD then - modifiedFile = targetNode:addFile(filename); + modifiedFile = targetNode:addFile(filename, extension); elseif modifier == MOD_DELETE then if mode == 'normal' then modifiedFile = targetNode:markFileForDeletion(filename); else - modifiedFile = targetNode:removeFile(filename); + modifiedFile = targetNode:removeFile(filename, extension); end elseif modifier == MOD_MODIFY then modifiedFile = targetNode:modifyFile(filename); @@ -179,12 +179,12 @@ function Graph.new(ewidth, showLabels) -- Public Functions -- ------------------------------------------------ - function self:draw(camrot) + function self:draw(camrot, camscale) root:draw(ewidth); love.graphics.draw(spritebatch); if showLabels then - root:drawLabel(camrot); + root:drawLabel(camrot, camscale); end end diff --git a/src/graph/Node.lua b/src/graph/Node.lua index a1e4958..c2cf971 100644 --- a/src/graph/Node.lua +++ b/src/graph/Node.lua @@ -19,14 +19,16 @@ local SPRITE_SCALE_FACTOR = SPRITE_SIZE / 256; local SPRITE_OFFSET = 128; local MIN_ARC_SIZE = SPRITE_SIZE; -local FORCE_SPRING = -0.005; -local FORCE_CHARGE = 10000000; +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 -- ------------------------------------------------ @@ -201,7 +203,7 @@ function Node.new(parent, path, name, x, y, spritebatch) function self:draw(ewidth) for _, node in pairs(children) do - love.graphics.setColor(255, 255, 255, 55); + love.graphics.setColor(EDGE_COLOR); love.graphics.setLineWidth(ewidth); love.graphics.line(posX, posY, node:getX(), node:getY()); love.graphics.setLineWidth(1); @@ -210,24 +212,22 @@ function Node.new(parent, path, name, x, y, spritebatch) end end - function self:drawLabel(camrot) + function self:drawLabel(camrot, camscale) love.graphics.setFont(LABEL_FONT); - love.graphics.print(name, posX, posY, -camrot, 1, 1, -radius, -radius); - love.graphics.setFont(DEFAULT_FONT); + love.graphics.print(name, posX, posY, -camrot, 1 / camscale, 1 / camscale, -radius * camscale, -radius * camscale); for _, node in pairs(children) do - love.graphics.setFont(LABEL_FONT); - love.graphics.print(name, posX, posY, -camrot, 1, 1, -radius, -radius); - love.graphics.setFont(DEFAULT_FONT); - node:drawLabel(camrot); + node:drawLabel(camrot, camscale); end + + love.graphics.setFont(DEFAULT_FONT); end function self:update(dt) move(dt); for name, file in pairs(files) do if file:isDead() then - self:removeFile(name); + self:removeFile(name, file:getExtension()); end file:update(dt); file:setPosition(posX, posY); @@ -247,19 +247,20 @@ 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. - -- @name - The name of the file to add. + -- @param name - The name of the file to add. + -- @param extension - The extension of the file to add. -- - function self:addFile(name) + function self:addFile(name, extension) -- Exit early if the file already exists. if files[name] then - files[name]:modify('add'); + files[name]:setState('add'); return files[name]; end -- Get the file color and extension from the FileManager and create the actual file object. - local color, extension = FileManager.add(name); - files[name] = File.new(self, name, color, extension, posX, posY); - files[name]:modify('add'); + local color = FileManager.add(name, extension); + files[name] = File.new(posX, posY, color, extension); + files[name]:setState('add'); fileCount = fileCount + 1; -- Update layout of the files. @@ -279,7 +280,7 @@ function Node.new(parent, path, name, x, y, spritebatch) return; end - file:modify('del'); + file:setState('del'); return file; end @@ -290,8 +291,9 @@ function Node.new(parent, path, name, x, y, spritebatch) -- 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. -- - function self:removeFile(name) + function self:removeFile(name, extension) local file = files[name]; if not file then @@ -301,7 +303,7 @@ function Node.new(parent, path, name, x, y, spritebatch) -- Store a reference to the file which can be returned -- after the file has been removed from the table. - FileManager.remove(name); + FileManager.remove(name, extension); files[name] = nil; fileCount = fileCount - 1; @@ -320,7 +322,7 @@ function Node.new(parent, path, name, x, y, spritebatch) return; end - file:modify('mod'); + file:setState('mod'); return file; end diff --git a/src/logfactory/LogCreator.lua b/src/logfactory/LogCreator.lua index ed6ea63..0bdc24d 100644 --- a/src/logfactory/LogCreator.lua +++ b/src/logfactory/LogCreator.lua @@ -84,7 +84,6 @@ end function LogCreator.isGitAvailable() local handle = io.popen('git version'); local result = handle:read('*a'); - print(result); handle:close(); return result:find('git version'); end diff --git a/src/logfactory/LogLoader.lua b/src/logfactory/LogLoader.lua index 9df376b..d02e83d 100644 --- a/src/logfactory/LogLoader.lua +++ b/src/logfactory/LogLoader.lua @@ -122,7 +122,9 @@ local function splitCommits(log) if path ~= '' then path = '/' .. path; end - commits[index][#commits[index] + 1] = { modifier = line:sub(1, 1), path = ROOT_FOLDER .. path, file = file }; + 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 }; end end diff --git a/src/logfactory/LogReader.lua b/src/logfactory/LogReader.lua index 93ba98e..91a8e3f 100644 --- a/src/logfactory/LogReader.lua +++ b/src/logfactory/LogReader.lua @@ -60,7 +60,7 @@ local function applyNextCommit() for i = 1, #log[index] do local change = log[index][i]; - notify(EVENT_CHANGED_FILE, change.modifier, change.path, change.file, 'normal'); + notify(EVENT_CHANGED_FILE, change.modifier, change.path, change.file, change.extension, 'normal'); end end @@ -73,7 +73,7 @@ local function reverseCurCommit() for i = 1, #log[index] do local change = log[index][i]; - notify(EVENT_CHANGED_FILE, reverseGitStatus(change.modifier), change.path, change.file, 'normal'); + notify(EVENT_CHANGED_FILE, reverseGitStatus(change.modifier), change.path, change.file, change.extension, 'normal'); end index = index - 1; @@ -95,7 +95,7 @@ local function fastForward(to) 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, 'fast'); + notify(EVENT_CHANGED_FILE, change.modifier, change.path, change.file, change.extension, 'fast'); end end end @@ -122,7 +122,7 @@ local function fastBackward(to) 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, 'fast'); + notify(EVENT_CHANGED_FILE, reverseGitStatus(change.modifier), change.path, change.file, change.extension, 'fast'); end end end diff --git a/src/screens/MainScreen.lua b/src/screens/MainScreen.lua index 3bf03cb..c41623d 100644 --- a/src/screens/MainScreen.lua +++ b/src/screens/MainScreen.lua @@ -29,6 +29,15 @@ 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; + -- ------------------------------------------------ -- Module -- ------------------------------------------------ @@ -71,6 +80,38 @@ function MainScreen.new() toggleFullscreen = config.keyBindings.toggleFullscreen; 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; + 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); + end + if InputHandler.isDown(camera_rotateL) then + camera:rotate(dt, -1); + elseif InputHandler.isDown(camera_rotateR) 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); + end + if InputHandler.isDown(camera_n) then + camera:move(dt, 0, -1); + elseif InputHandler.isDown(camera_s) then + camera:move(dt, 0, 1); + end end -- ------------------------------------------------ @@ -91,7 +132,6 @@ function MainScreen.new() -- Create the camera. camera = Camera.new(); - camera:assignKeyBindings(config); -- Load custom colors. FileManager.setColorTable(info.colors); @@ -106,7 +146,7 @@ function MainScreen.new() LogReader.register(graph); -- Create panel. - filePanel = FilePanel.new(FileManager.draw, FileManager.update, 0, 0, 150, 400); + filePanel = FilePanel.new(FileManager.draw, FileManager.update, 0, 0, 150, love.graphics.getHeight() - 40); filePanel:setActive(config.options.showFileList); timeline = Timeline.new(config.options.showTimeline, LogReader.getTotalCommits(), LogReader.getCurrentDate()); @@ -117,8 +157,8 @@ function MainScreen.new() function self:draw() camera:draw(function() - graph:draw(camera:getRotation()); - AuthorManager.drawLabels(camera:getRotation()); + graph:draw(camera:getRotation(), camera:getScale()); + AuthorManager.drawLabels(camera:getRotation(), camera:getScale()); end); filePanel:draw(); @@ -136,7 +176,9 @@ function MainScreen.new() timeline:setCurrentCommit(LogReader.getCurrentIndex()); timeline:setCurrentDate(LogReader.getCurrentDate()); - camera:move(dt); + controlCamera(dt); + + camera:update(dt); end function self:close() @@ -174,24 +216,25 @@ function MainScreen.new() end function self:mousepressed(x, y, b) - filePanel:mousepressed(x, y, b); - local pos = timeline:getCommitAt(x, y); if pos then LogReader.setCurrentIndex(pos); end end - function self:mousereleased(x, y, b) - filePanel:mousereleased(x, y, b); - end - function self:mousemoved(x, y, dx, dy) - filePanel:mousemoved(x, y, 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) - filePanel:wheelmoved(x, y); + local mx, my = love.mouse.getPosition(); + if filePanel:intersects(mx, my) then + filePanel:wheelmoved(x, y); + else + camera:zoom(love.timer.getDelta(), y); + end end function self:resize(nx, ny) diff --git a/src/screens/SelectionScreen.lua b/src/screens/SelectionScreen.lua index 4417936..d080cd4 100644 --- a/src/screens/SelectionScreen.lua +++ b/src/screens/SelectionScreen.lua @@ -26,8 +26,16 @@ local SelectionScreen = {}; local TEXT_FONT = Resources.loadFont('SourceCodePro-Medium.otf', 15); local DEFAULT_FONT = Resources.loadFont('default', 12); -local WARNING_TITLE = 'Not a valid git repository'; -local WARNING_MESSAGE = 'The path "%s" does not point to a valid git repository. Make sure you have specified the full path in the settings file.'; +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. Make sure you have specified the full path in the settings file.'; local UI_ELEMENT_PADDING = 20; local UI_ELEMENT_MARGIN = 5; @@ -59,16 +67,24 @@ function SelectionScreen.new() -- @param options -- local function createGitLogs(config) - if LogCreator.isGitAvailable() then - 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, string.format(WARNING_MESSAGE, path), 'warning', false); - end + -- 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 end end diff --git a/src/ui/CamWrapper.lua b/src/ui/CamWrapper.lua index 4f02fde..86cffe2 100644 --- a/src/ui/CamWrapper.lua +++ b/src/ui/CamWrapper.lua @@ -1,4 +1,3 @@ -local InputHandler = require('src.InputHandler'); local Camera = require('lib.camera.Camera'); -- ------------------------------------------------ @@ -20,19 +19,6 @@ local CAMERA_MIN_ZOOM = 2; local GRAPH_PADDING = 100; --- ------------------------------------------------ --- Local variables --- ------------------------------------------------ - -local camera_zoomIn; -local camera_zoomOut; -local camera_rotateL; -local camera_rotateR; -local camera_n; -local camera_s; -local camera_e; -local camera_w; - -- ------------------------------------------------ -- Constructor -- ------------------------------------------------ @@ -52,6 +38,10 @@ function CamWrapper.new() -- 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 @@ -98,67 +88,34 @@ function CamWrapper.new() -- Public Functions -- ------------------------------------------------ - --- - -- Assign the keybindings to local variables for faster access. - -- @param config - -- - function self:assignKeyBindings(config) - 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; + 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); + 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); end --- -- Processes camera related controls and updates the camera. -- @param dt -- - function self:move(dt) + function self:update(dt) local tzoom = calculateAutoZoom(camera.rot); zoom = lerp(zoom, tzoom, dt * 2); - -- Handle manual zoom. This will be added to the automatic zoom factor. - if InputHandler.isDown(camera_zoomIn) then - manualZoom = manualZoom + CAMERA_ZOOM_SPEED * dt; - elseif InputHandler.isDown(camera_zoomOut) then - manualZoom = manualZoom - CAMERA_ZOOM_SPEED * dt; - end - - camera:zoomTo(math.max(CAMERA_MAX_ZOOM, math.min(zoom + manualZoom, CAMERA_MIN_ZOOM))); - - -- Rotation. - if InputHandler.isDown(camera_rotateL) then - camera:rotate(-CAMERA_ROTATION_SPEED * dt); - elseif InputHandler.isDown(camera_rotateR) then - camera:rotate(CAMERA_ROTATION_SPEED * dt); - end - - -- Horizontal Movement. - local dx = 0; - if InputHandler.isDown(camera_w) then - dx = dx - dt * CAMERA_TRANSLATION_SPEED; - elseif InputHandler.isDown(camera_e) then - dx = dx + dt * CAMERA_TRANSLATION_SPEED; - end - -- Vertical Movement. - local dy = 0; - if InputHandler.isDown(camera_n) then - dy = dy - dt * CAMERA_TRANSLATION_SPEED; - elseif InputHandler.isDown(camera_s) then - dy = dy + dt * CAMERA_TRANSLATION_SPEED; - end - - -- Take the camera rotation into account when calculating the new offset. - ox = ox + (math.cos(-camera.rot) * dx - math.sin(-camera.rot) * dy); - oy = oy + (math.sin(-camera.rot) * dx + math.cos(-camera.rot) * dy); + camera:zoomTo(clamp(CAMERA_MAX_ZOOM, zoom + manualZoom, CAMERA_MIN_ZOOM)); -- Gradually move the camera to the target position. - cx = cx - (cx - math.floor(gx + ox)) * dt * CAMERA_TRACKING_SPEED; - cy = cy - (cy - math.floor(gy + oy)) * dt * CAMERA_TRACKING_SPEED; + cx = lerp(cx, gx + ox, dt * CAMERA_TRACKING_SPEED); + cy = lerp(cy, gy + oy, dt * CAMERA_TRACKING_SPEED); camera:lookAt(cx, cy); end @@ -189,6 +146,10 @@ function CamWrapper.new() return camera.rot; end + function self:getScale() + return camera.scale; + end + return self; end diff --git a/src/ui/components/BaseComponent.lua b/src/ui/components/BaseComponent.lua index 0cd50a1..e9e6ff0 100644 --- a/src/ui/components/BaseComponent.lua +++ b/src/ui/components/BaseComponent.lua @@ -12,9 +12,7 @@ local function new(t, x, y, w, h) end function self:intersects(cx, cy) - if x < cx and x + w > cx and y < cy and y + h > cy then - return true; - end + return x < cx and x + w > cx and y < cy and y + h > cy; end function self:mousemoved(mx, my, dx, dy) diff --git a/src/ui/components/FilePanel.lua b/src/ui/components/FilePanel.lua index ee31168..f382a4a 100644 --- a/src/ui/components/FilePanel.lua +++ b/src/ui/components/FilePanel.lua @@ -17,12 +17,9 @@ local FilePanel = {}; -- ------------------------------------------------ function FilePanel.new(render, update, x, y, w, h) - local bodyBaseCol = { 80, 80, 80, 150 }; + local bodyBaseCol = { 0, 0, 0, 0 }; local self = Toggleable(); - self:attach(Resizable(w - 16, h - 16, -w + 16, -h + 16, true, true, true, true)); - self:attach(BoxDecorator('fill', bodyBaseCol, w - 16, h - 16, -w + 16, -h + 16, true, true, true, true)); - self:attach(Draggable (0, 0, 0, 0)); 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));