diff --git a/viewer/gui/GpsPage.jsx b/viewer/gui/GpsPage.jsx index bf4b39e6d..4f2186c91 100644 --- a/viewer/gui/GpsPage.jsx +++ b/viewer/gui/GpsPage.jsx @@ -23,6 +23,7 @@ import FullScreen from '../components/Fullscreen'; import remotechannel, {COMMANDS} from "../util/remotechannel"; import RemoteChannelDialog from "../components/RemoteChannelDialog"; import {DynamicTitleIcons} from "../components/TitleIcons"; +import layouthandler from "../util/layouthandler.js"; const PANEL_LIST=['left','m1','m2','m3','right']; //from https://stackoverflow.com/questions/16056591/font-scaling-based-on-width-of-container @@ -220,6 +221,7 @@ class GpsPage extends React.Component{ PANEL_LIST, [LayoutHandler.OPTIONS.ANCHOR]), LayoutFinishedDialog.getButtonDef(), + LayoutHandler.revertButtonDef(), FullScreen.fullScreenDefinition, Dimmer.buttonDef(), { diff --git a/viewer/gui/NavPage.jsx b/viewer/gui/NavPage.jsx index 5dc98b951..0c0c2ea5a 100644 --- a/viewer/gui/NavPage.jsx +++ b/viewer/gui/NavPage.jsx @@ -667,6 +667,7 @@ class NavPage extends React.Component{ onClick: ()=>OverlayDialog.dialog((props)=>) }, LayoutFinishedDialog.getButtonDef(), + LayoutHandler.revertButtonDef(), RemoteChannelDialog({overflow:true}), FullScreen.fullScreenDefinition, Dimmer.buttonDef(), diff --git a/viewer/images/icons-new/ic_undo.svg b/viewer/images/icons-new/ic_undo.svg new file mode 100644 index 000000000..07671c437 --- /dev/null +++ b/viewer/images/icons-new/ic_undo.svg @@ -0,0 +1 @@ + diff --git a/viewer/style/buttons.less b/viewer/style/buttons.less index b952f66a9..4fe0a4117 100644 --- a/viewer/style/buttons.less +++ b/viewer/style/buttons.less @@ -447,6 +447,9 @@ button{ border-color: @editingColor; } } + &.RevertLayout{ + .iconButton('ic_undo.svg'); + } &.EditPage{ .iconButton('tune.svg'); } diff --git a/viewer/util/keys.jsx b/viewer/util/keys.jsx index b2d7cf559..58f9581a7 100644 --- a/viewer/util/keys.jsx +++ b/viewer/util/keys.jsx @@ -242,6 +242,7 @@ let keys={ windowDimensions: K, layoutEditing:K, layoutSequence: K, //updated on layout load + layoutReverts: K, reloadSequence: K, //will be changed if new data from server toastText:K, toastTimeout:K, diff --git a/viewer/util/layouthandler.js b/viewer/util/layouthandler.js index 16720c048..d21cba86d 100644 --- a/viewer/util/layouthandler.js +++ b/viewer/util/layouthandler.js @@ -7,8 +7,44 @@ import assign from 'object-assign'; import LocalStorage, {STORAGE_NAMES} from './localStorageManager'; import defaultLayout from '../layout/default.json'; -import {SortModes} from "../hoc/Sortable"; const DEFAULT_NAME="system.default"; + +const ACTION_MOVE=1; +const ACTION_ADD=2; +const ACTION_REPLACE=3; +const ACTION_DELETE=4; +class LayoutAction{ + constructor(action,page,panel,options){ + this.action=action; + this.page=page; + this.panel=panel; + this.options=options; + } + run(handler){ + if (this.action === ACTION_MOVE){ + return handler.noAction(()=> + handler.moveItem(this.page,this.panel,this.options.oldIndex,this.options.newIndex,this.options.newPanel) + ) + } + if (this.action === ACTION_REPLACE){ + return handler.noAction(()=> + handler.replaceItem(this.page,this.panel,this.options.index,this.options.item) + ); + } + if (this.action === ACTION_DELETE){ + return handler.noAction(()=> + handler.replaceItem(this.page,this.panel,this.options.index) + ); + } + if (this.action === ACTION_ADD){ + return handler.noAction(()=> + handler.replaceItem(this.page,this.panel,this.options.index,this.options.item,this.options.addMode) + ); + } + return false; + } +} + class LayoutHandler{ constructor(){ this.layout=undefined; @@ -22,6 +58,8 @@ class LayoutHandler{ this.hiddenPanels={}; //panels we removed during editing this.temporaryOptions={}; //options being set during edit globalStore.register(this,keys.gui.capabilities.uploadLayout); + this.actions=[]; + this.lockActions=false; } dataChanged(skeys){ this.storeLocally=!globalStore.getData(keys.gui.capabilities.uploadLayout,false); @@ -123,6 +161,7 @@ class LayoutHandler{ } _setEditing(on){ this.editing=on; + this.resetActions(); globalStore.storeData(keys.gui.global.layoutEditing,on); } @@ -486,12 +525,18 @@ class LayoutHandler{ if (opt_add == this.ADD_MODES.beginning) { //insert at the beginning panelData.splice(0, 0, layoutItem); + this._addAction(new LayoutAction(ACTION_DELETE,page,panel,{ + index:0 + })); this.incrementSequence(); return true; } if (opt_add == this.ADD_MODES.end) { //append panelData.push(layoutItem); + this._addAction(new LayoutAction(ACTION_DELETE,page,panel,{ + index: panelData.length-1 + })); this.incrementSequence(); return true; } @@ -503,25 +548,43 @@ class LayoutHandler{ if (opt_add == this.ADD_MODES.afterIndex){ if (index == (panelData.length-1)){ panelData.push(layoutItem); + this._addAction(new LayoutAction(ACTION_DELETE,page,panel,{ + index: panelData.length-1 + })); } else { panelData.splice(index + 1, 0, layoutItem) + this._addAction(new LayoutAction(ACTION_DELETE,page,panel,{ + index: index+1 + })); } this.incrementSequence(); return true; } if (opt_add == this.ADD_MODES.beforeIndex){ panelData.splice(index,0,layoutItem); + this._addAction(new LayoutAction(ACTION_DELETE,page,panel,{ + index: index + })); this.incrementSequence(); return true; } return false; //invalid add mode } + const old=panelData[index]; if (item) { panelData.splice(index, 1, layoutItem); + this._addAction(new LayoutAction(ACTION_REPLACE,page,panel,{ + index: index, + item: old + })); } else{ panelData.splice(index, 1); + this._addAction(new LayoutAction(ACTION_ADD,page,panel,{ + index: index, + item: old + })); } this.incrementSequence(); return true; @@ -539,6 +602,11 @@ class LayoutHandler{ newPanelData=this.getDirectPanelData(page,opt_newPanel); if (! newPanelData) return false; } + this._addAction(new LayoutAction(ACTION_MOVE,page,opt_newPanel||panel,{ + oldIndex: newIndex, + newIndex: oldIndex, + newPanel: panel + })); let item=panelData[oldIndex]; panelData.splice(oldIndex,1); newPanelData.splice(newIndex,0,item); @@ -655,6 +723,51 @@ class LayoutHandler{ return rt; } + resetActions(){ + this.actions=[]; + globalStore.storeData(keys.gui.global.layoutReverts,this.actions.length); + } + hasRevertableActions(){ + return this.isEditing() && this.actions.length > 0; + } + revertAction(){ + if (! this.hasRevertableActions()) return false; + const action=this.actions.pop(); + globalStore.storeData(keys.gui.global.layoutReverts,this.actions.length); + return action.run(this); + } + _addAction(action){ + if (! this.lockActions) { + this.actions.push(action); + globalStore.storeData(keys.gui.global.layoutReverts,this.actions.length); + } + } + noAction(callback){ + this.lockActions=true; + try{ + return callback(); + } + finally { + this.lockActions=false; + } + } + revertButtonDef(){ + return{ + name: 'RevertLayout', + onClick: ()=>this.revertAction(), + storeKeys:{ + reverts: keys.gui.global.layoutReverts, + editing: keys.gui.global.layoutEditing + }, + updateFunction:(state)=>{ + return { + visible: state.editing, + disabled: state.reverts < 1 + } + } + } + } + } LayoutHandler.prototype.OPTIONS={