VisualUIEditor项目讲解之撤消反撤消详解

VisualUIEditor项目讲解之撤消反撤消详解

撤消反撤消在UI编辑器里面必备的功能,它可以帮助我们在编辑过程中,无需害怕误操作,助您更好的使用编辑器

在项目中使用

当您在项目中错误的移动位置,或者设置了错误的属性,这时候您可以用CTRL+Z来撤消刚才您进行的修改
当您在项目中撤消了刚才的操作,又想快速的回到刚才的状态,这时候您可以使用CTRL+R反撤消功能,来进行反撤消

源码讲解

源码路径

项目的实现源码,可参考renderUndo

源码详解

每个Scene实例化的时候都会创建一个UndoObj,UndoObj里面是一层UndoList的浅封装,差别是在Undo和Redo的时候通常编辑器有节点发生变更

"ui:scene_change"(event, message) {
    let runScene = this.$.scene.getRunScene();
    if(!runScene._undo)
        runScene._undo =  new UndoObj();
}

UndoList维持着场景变更的记录,UndoList的构造函数如下

class UndoList extends EventEmitter {
  constructor (type) {
    super()
    //是否变化时发送事件的变化
    this._silent = false
    //分为local和global类型, local类型事件变化只会通知本地, global则会通常整个编辑器
    this._type = type

    //当前的命令列表
    this._curGroup = new CommandGroup()
    //操作过程的命令列表
    this._groups = []
    //记录当前的位置信息
    this._position = -1
    //上一次保存时的位置信息
    this._savePosition = -1
  }
}

当有新的操作到达时, 比如节点移动位置发生变更, 这时候则会调用函数

add (cmd) {
    this._clearRedo()
    if (this._curGroup.isCanCombine(cmd)) {
        this._curGroup.combineCommand(cmd)
    } else {
        this.commit()
        this._curGroup.add(cmd)
    }
    this._changed('add-command')
}

因为整个撤消反撤消操作是单线的, 所以当有新的操作, 之前保存的redo操作变成无意义, 这时候会先清除redo列表, 即当前位置后命令列表
这时候我们会判断该命令是否能够合并, 如果能合并, 我们会优先尝试合并

  //CommandGroup的函数
  isCanCombine (other) {
    if (this._commands.length == 0) {
      return true
    }
    for ( let i = 0; i < this._commands.length; ++i) {
      if (this._commands[i].isCanCombine(other)) {
        return true
      }
    }
    if (this._time && Math.abs(this._time - other.info.time) < 1000) {
      return true
    }
    return false
  }

  //Command的函数
  isCanCombine (other) {
    if (!this.info || !other.info) {
      return false
    }

    if (this.info.op != other.info.op) {
      return false
    }

    if (this.info.uuid != other.info.uuid) {
      return false
    }

    if (this.info.op == 'prop' && (this.info.prop != other.info.prop)) {
      return false
    }

    if (Math.abs(this.info.time - other.info.time) >= 1000) {
      return false
    }
    return true
  }

在CommandGroup中, 我们会遍历所有的Command,看能否进行合并或者最后一条Command的时间距离现在的时间太短,我们则认为能够合并
在Command中,同一种操作类型,同一个节点,同一个属性操作,时间在一定的时间内方可认为能合并
如果判断能合并,则进行合并并更新属性值

为什么如此设计?

因为整个撤消系统中,都是假定对其它系统一无所知的,而其它系统除了有限的Add接口,并没有暴露其它的接口信息
下面列举两种情景

  • 当在编辑器中,拖动某个节点移动的时候,每隔350ms会触发一次mousemove事件,这时候节点属性会频繁更改,而我们又认为这是一次操作,则我们应当在移动完成之后按一次撤消应该回到之前的状态,而不应该是移动过程中的任何状态,这时候用到同属性操作,在一定时间内合并操作
  • 当在编辑器中,一次拖动或者更改多个节点的属性,这时候预期的撤消应该为这次修改的所有属性同时回到变更之前的状态,而不应该是一个个节点的属性变更,所以这时候会合并所有的操作,当成一个组别

当判断不能合并的时候,则会新起一条CommandGroup来记录新的Command,并更新位置信息

撤消Undo

  undo () {
    // check if we have un-commit group
    if (this._curGroup.canCommit()) {
      this._curGroup.undo()
      this._changed('undo-cache')
      this._groups.push(this._curGroup)
      this._curGroup = new CommandGroup()
      return true
    }

    // check if can undo
    if (this._position < 0) {
      return false
    }

    let group = this._groups[this._position]
    group.undo()
    this._position--
    this._changed('undo')
    return true
  }

撤消时,存在以下三种情况

  • 当前的Group可提交,即当前Group记录着一些操作信息,并且我们没有提交操作,这时直接对当前的Group执行Undo,并更新列表信息
  • 无可撤消的内容,即位置信息小于0
  • 可撤消,获取当前位置的Group进行撤消,并更新位置信息
  //Command undo
  undo () {
    let node = cocosGetItemByUUID(this.info.scene, this.info.uuid)
    if (this.info.op == 'prop') {
      if (!node) {
        return false
      }
      if (this.info.doPropChange) {
        this.info.doPropChange(node, this.info.prop, this.info.oldValue)
      } else {
        NodePropChange(node, this.info.prop, this.info.oldValue)
      }
      return true
    }
    console.warn('Please implement undo function in your command')
  }

每个命令列表,首先会先尝试获取节点,然后根据节点的属性变更,进行新旧值的设置

反撤消Redo

  redo () {
    // check if can redo
    if (this._position >= this._groups.length - 1) {
      return false
    }

    this._position++
    let group = this._groups[this._position]
    group.redo()

    this._changed('redo')
    return true
  }

存在以下两种情况

  • 当前位置处理列表的最后一位,即无可反撤消的内容,此时不执行任何操作
  • 获取该反撤消的Group,对其执行redo操作
  //Command redo
  redo () {
    let node = cocosGetItemByUUID(this.info.scene, this.info.uuid)
    if (this.info.op == 'prop') {
      if (!node) {
        return false
      }

      if (this.info.doPropChange) {
        this.info.doPropChange(node, this.info.prop, this.info.newValue)
      } else {
        NodePropChange(node, this.info.prop, this.info.newValue)
      }
      return true
    }
    console.warn('Please implement redo function in your command')
  }

每个命令列表,首先会先尝试获取节点,然后根据节点的属性变更,进行新旧值的设置

在编辑器中添加Command

function addNodeCommand (node, prop, oldValue, newValue, doPropChange) {
  let scene = getRootNode(node)
  if (!scene._undo) {
    return
  }

  tryAddCommand(scene._undo, newPropCommandChange(scene, node.uuid, prop, oldValue, newValue, doPropChange))
}

当属性发生变更的时候,我们会调用addNodeCommand值进行相应的设置,来添加撤消反撤消功能的支持,以下的参数说明

  • node即发生变化的节点
  • prop即发生变化的属性值,值为x,width这种
  • oldValue即当前node节点的值
  • newValue为node属性将要变更的值
  • doPropChange如果没有传该函数,则会调用NodePropChange进行修改,如果伴随属性变更,需调用相关函数,则可以传自定义函数,原型如下
function(node, prop, newValue) {}

其它未列出的信息,可参考源码的实现细节

其它信息

VisualUIEditor开发QQ群欢迎您的加入: 453224679

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容