From 96f9f5442a9c49f0fd054a43567d6c62e645f7da Mon Sep 17 00:00:00 2001 From: Isaiah Odhner Date: Thu, 18 Jun 2015 21:11:40 -0400 Subject: [PATCH] Keyboard interaction with dialogues --- README.md | 2 +- TODO.md | 8 +--- classic.css | 13 +++++- layout.css | 19 ++++++++- src/$Window.js | 107 +++++++++++++++++++++++++++++++++++++++++++---- src/functions.js | 43 +++++++++---------- 6 files changed, 151 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 7c661f6..3a7a418 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ You can also install it as a Chrome app. * The Magnifier's viewport preview * Shape styles on most of the shape tools * The polygon tool needs some work -* Keyboard support in the menus and dialogues +* Keyboard support in the menus * [This entire document full of things to do](TODO.md) Clipboard support is somewhat limited. diff --git a/TODO.md b/TODO.md index 7688bb9..eaff6a6 100644 --- a/TODO.md +++ b/TODO.md @@ -20,6 +20,7 @@ * Issues + * Image > Flip/Rotate, Rotate by angle, ` ` Degrees sort of nullifies the image * Crashes when saving large images * If you open an image it resets the zoom but if you're on the magnification tool it doesn't update the options * If you zoom in with the magnifier without previously changing the magnification on the toolbar, @@ -56,12 +57,6 @@ * Make the component ghost account for the window's titlebar -* Keyboard interaction with dialogues - * Close dialogues with Escape - * Navigating form windows - * Left/Right, Enter/Space - - * Handle windows going off the screen @@ -296,7 +291,6 @@ Prankily wait for next user input before fullscreening and bluescreening * Everything is in random files! "functions.js", REALLY? * $Window has a $Button facility; $FormWindow overrides it with essentially a better one * Image inversion code is duplicated in ChooserCanvas from tool-options.js but should go in image-manipulation.js - * `$w.$form.addClass("jspaint-horizontal").css({display: "flex"});` * Comment everything and then try to make the code as obvious as the comments * Improve code quality: https://codeclimate.com/github/1j01/jspaint diff --git a/classic.css b/classic.css index 6338c71..466b2af 100644 --- a/classic.css +++ b/classic.css @@ -181,6 +181,7 @@ border-bottom: 1px solid #7b7b7b; } .jspaint-window-content .jspaint-dialogue-button:hover:active, +.jspaint-window-content .jspaint-dialogue-button.pressed, .jspaint-tool:hover:active, .jspaint-tool.selected { padding-bottom: 2px; @@ -194,6 +195,7 @@ border-bottom: 1px solid white; } .jspaint-window-content .jspaint-dialogue-button:hover:active:before, +.jspaint-window-content .jspaint-dialogue-button.pressed:before, .jspaint-tool:hover:active:before, .jspaint-tool.selected:before { top: 0px; @@ -396,6 +398,7 @@ border-bottom: 1px solid #808080; } .jspaint-button:hover:active, +.jspaint-button.pressed, .jspaint-button.selected { border-top: 1px solid #000; border-left: 1px solid #000; @@ -403,6 +406,7 @@ border-bottom: 1px solid #fff; } .jspaint-button:hover:active:after, +.jspaint-button.pressed:after, .jspaint-button.selected:after { border-top: 1px solid #808080; border-left: 1px solid #808080; @@ -413,7 +417,12 @@ right: 0px; top: -3px; } -.jspaint-button:hover:active:before { +.jspaint-button:hover:active:before, +.jspaint-button.pressed:before { right: -1px; top: -2px; -} \ No newline at end of file +} +.jspaint-button:focus { + outline: 1px dotted black; + outline-offset: -4px; +} diff --git a/layout.css b/layout.css index b22cb28..d30e563 100644 --- a/layout.css +++ b/layout.css @@ -205,12 +205,27 @@ html, body, .jspaint { } .jspaint-window-content .jspaint-button-group { - width: 85px; + display: flex; + flex: 0 0 auto; + flex-flow: column; } .jspaint-window-content .jspaint-button-group > button { - width: 95%; + width: 80px; padding: 3px 5px; } +.jspaint-window-content > form { + display: flex; + flex-flow: row; +} +.jspaint-dialogue-window .jspaint-window-content > form { + flex-flow: column; +} +.jspaint-dialogue-window .jspaint-window-content > form > .jspaint-button-group { + flex-flow: row; +} +.jspaint-dialogue-window .jspaint-window-content > form > div:first-child { + padding: 5px; +} ::before, ::after { pointer-events: none; diff --git a/src/$Window.js b/src/$Window.js index f62ff4d..8a38697 100644 --- a/src/$Window.js +++ b/src/$Window.js @@ -1,6 +1,13 @@ $Window.Z_INDEX = 5; +// $.fn.isBefore = function(elem){ +// if(!(elem instanceof $)){ +// elem = $(elem); +// } +// return this.add(elem).index(elem) > 0; +// }; + function $Window($component){ var $w = $(E("div")).addClass("jspaint-window").appendTo("body"); $w.$titlebar = $(E("div")).addClass("jspaint-window-titlebar").appendTo($w); @@ -30,6 +37,74 @@ function $Window($component){ }); }); + $w.on("keydown", function(e){ + if(e.ctrlKey || e.altKey || e.shiftKey){ + return; + } + var $buttons = $w.$content.find("button.jspaint-button"); + var $focused = $(document.activeElement); + var focused_index = $buttons.index($focused); + // if(focused_index === -1){ + // if($focused.isBefore($buttons.first())){ + // focused_index = 0; + // }else{ + // focused_index = $buttons.length - 1; + // } + // } + // console.log(e.keyCode); + switch(e.keyCode){ + case 40: // down + case 39: // right + if($focused.is("button")){ + if(focused_index < $buttons.length - 1){ + $buttons.get(focused_index + 1).focus(); + e.preventDefault(); + } + } + break; + case 38: // up + case 37: // left + if($focused.is("button")){ + if(focused_index > 0){ + $buttons.get(focused_index - 1).focus(); + e.preventDefault(); + } + } + break; + case 32: // space + case 13: // enter (doesn't actually work (in chrome), the button gets clicked immediately) + if($focused.is("button")){ + $focused.addClass("pressed"); + var release = function(){ + $focused.removeClass("pressed"); + $focused.off("focusout", release); + $(window).off("keyup", keyup); + }; + var keyup = function(e){ + if(e.keyCode === 32 || e.keyCode === 13){ + release(); + } + }; + $focused.on("focusout", release); + $(window).on("keyup", keyup); + } + break; + case 9: // tab + // wrap around when tabbing through controls in a window + var $controls = $w.$content.find("input, textarea, select, button, a"); + var focused_control_index = $controls.index($focused); + if(focused_control_index === $controls.length - 1){ + e.preventDefault(); + $controls[0].focus(); + } + break; + case 27: // escape + $w.close(); + break; + } + }); + // @TODO: restore last focused controls when clicking/mousing down on the window + $w.applyBounds = function(){ $w.css({ left: Math.max(0, Math.min(innerWidth - $w.width(), $w[0].getBoundingClientRect().left)), @@ -69,9 +144,19 @@ function $Window($component){ } }); + // $w.updateTabIndexes = function(){ + // var ti = 1; + // $w.find("button").each(function(){ + // this.tabIndex = ti++; + // }); + // $w.find("input, select").each(function(){ + // this.tabIndex = ti++; + // }); + // }; + $w.$Button = function(text, handler){ - $w.$content.append( - $(E("button")) + var $b = $(E("button")) + .appendTo($w.$content) .addClass("jspaint-dialogue-button") .text(text) .on("click", function(){ @@ -79,8 +164,9 @@ function $Window($component){ handler(); } $w.close(); - }) - ); + }); + // $w.updateTabIndexes(); + return $b; }; $w.title = function(title){ if(title){ @@ -116,12 +202,11 @@ function $FormWindow(title){ $w.title(title); $w.$form = $form = $(E("form")).appendTo($w.$content); - $w.$form_left = $(E("div")).appendTo($w.$form); - $w.$form_right = $(E("div")).appendTo($w.$form).addClass("jspaint-button-group"); - $w.$form.addClass("jspaint-horizontal").css({display: "flex"}); + $w.$main = $(E("div")).appendTo($w.$form); + $w.$buttons = $(E("div")).appendTo($w.$form).addClass("jspaint-button-group"); $w.$Button = function(label, action){ - var $b = $(E("button")).appendTo($w.$form_right).text(label); + var $b = $(E("button")).appendTo($w.$buttons).text(label); $b.on("click", function(e){ // prevent the form from submitting // @TODO: instead, prevent the form's submit event @@ -133,6 +218,12 @@ function $FormWindow(title){ // this should really not be needed @TODO $b.addClass("jspaint-button jspaint-dialogue-button"); + // $w.updateTabIndexes(); + + $b.on("mousedown", function(){ + $b.focus(); + }); + return $b; }; diff --git a/src/functions.js b/src/functions.js index 4a4bc6b..f4bebe1 100644 --- a/src/functions.js +++ b/src/functions.js @@ -194,14 +194,14 @@ function are_you_sure(action){ if(saved){ action(); }else{ - var $w = new $Window(); + var $w = new $FormWindow().addClass("jspaint-dialogue-window"); $w.title("Paint"); - $w.$content.text("Save changes to "+file_name+"?"); + $w.$main.text("Save changes to "+file_name+"?"); $w.$Button("Save", function(){ $w.close(); file_save(); action(); - }); + }).focus(); $w.$Button("Discard", function(){ $w.close(); action(); @@ -259,7 +259,7 @@ function paste(img){ paste_img(); $canvas_area.trigger("resize"); }); - }); + }).focus(); $w.$Button("Crop", function(){ paste_img(); }); @@ -380,14 +380,14 @@ function render_history_as_gif(){ function undoable(callback, action){ saved = false; if(redos.length > 5){ - var $w = new $Window(); + var $w = new $FormWindow().addClass("jspaint-dialogue-window"); $w.title("Paint"); - $w.$content.html("Discard "+redos.length+" possible redo-able actions?
(Ctrl+Y or Ctrl+Shift+Z to redo)
"); + $w.$main.html("Discard "+redos.length+" possible redo-able actions?
(Ctrl+Y or Ctrl+Shift+Z to redo)
"); $w.$Button(action ? "Discard and Apply" : "Discard", function(){ $w.close(); redos = []; action && action(); - }); + }).focus(); $w.$Button("Keep", function(){ $w.close(); }); @@ -527,8 +527,8 @@ function image_attributes(){ } var $w = image_attributes.$window = new $FormWindow("Attributes"); - var $form_left = $w.$form_left; - var $form_right = $w.$form_right; + var $main = $w.$main; + var $buttons = $w.$buttons; // Information @@ -537,7 +537,7 @@ function image_attributes(){ "Size on disk": "Not available", // @TODO "Resolution": "72 x 72 dots per inch", }; - var $table = $(E("table")).appendTo($form_left); + var $table = $(E("table")).appendTo($main); for(var k in table){ var $tr = $(E("tr")).appendTo($table); var $key = $(E("td")).appendTo($tr).text(k + ":"); @@ -551,12 +551,12 @@ function image_attributes(){ var width_in_px = canvas.width; var height_in_px = canvas.height; - var $width_label = $(E("label")).appendTo($form_left).text("Width:"); - var $height_label = $(E("label")).appendTo($form_left).text("Height:"); + var $width_label = $(E("label")).appendTo($main).text("Width:"); + var $height_label = $(E("label")).appendTo($main).text("Height:"); var $width = $(E("input")).appendTo($width_label); var $height = $(E("input")).appendTo($height_label); - $form_left.find("input") + $main.find("input") .css({width: "40px"}) .on("change keyup keydown keypress mousedown mousemove paste drop", function(){ if($(this).is($width)){ @@ -569,7 +569,7 @@ function image_attributes(){ // Fieldsets - var $units = $(E("fieldset")).appendTo($form_left).append('Transparency'); + var $units = $(E("fieldset")).appendTo($main).append('Transparency'); $units.append(''); $units.append(''); $units.append(''); @@ -581,7 +581,7 @@ function image_attributes(){ current_unit = new_unit; }).triggerHandler("change"); - var $transparency = $(E("fieldset")).appendTo($form_left).append('Transparency'); + var $transparency = $(E("fieldset")).appendTo($main).append('Transparency'); $transparency.append(''); $transparency.append(''); $transparency.find("[value=" + (transparency ? "transparent" : "opaque") + "]").attr({checked: true}); @@ -601,7 +601,7 @@ function image_attributes(){ $canvas.trigger("user-resized", [0, 0, ~~width, ~~height]); image_attributes.$window.close(); - }); + }).focus(); $w.$Button("Cancel", function(){ image_attributes.$window.close(); @@ -622,7 +622,7 @@ function image_attributes(){ function image_flip_and_rotate(){ var $w = new $FormWindow("Flip and Rotate"); - var $fieldset = $(E("fieldset")).appendTo($w.$form_left); + var $fieldset = $(E("fieldset")).appendTo($w.$main); $fieldset.append("Flip or rotate"); $fieldset.append(""); $fieldset.append(""); @@ -679,7 +679,7 @@ function image_flip_and_rotate(){ } $w.close(); - }); + }).focus(); $w.$Button("Cancel", function(){ $w.close(); }); @@ -690,9 +690,9 @@ function image_flip_and_rotate(){ function image_stretch_and_skew(){ var $w = new $FormWindow("Stretch and Skew"); - var $fieldset_stretch = $(E("fieldset")).appendTo($w.$form_left); + var $fieldset_stretch = $(E("fieldset")).appendTo($w.$main); $fieldset_stretch.append("Stretch
"); - var $fieldset_skew = $(E("fieldset")).appendTo($w.$form_left); + var $fieldset_skew = $(E("fieldset")).appendTo($w.$main); $fieldset_skew.append("Skew
"); var $RowInput = function($table, img_src, label_text, default_value, label_unit){ @@ -727,7 +727,8 @@ function image_stretch_and_skew(){ var vskew = parseFloat(skew_y.val())/360*TAU; stretch_and_skew(xscale, yscale, hskew, vskew); $w.close(); - }); + }).focus(); + $w.$Button("Cancel", function(){ $w.close(); });