Zhihao's Studio.

mouseSync后续功能完善心得

Word count: 2,052 / Reading time: 7 min
2017/09/29 Share

背景

上篇博客中,我介绍了mouseSync这款软件的开发初衷、使用场景和开发的过程,感谢一些朋友试用该软件并给我留言,提出了一些新的需求和issue。同时也感谢开发者头条的推荐至头版的精选板块,让那篇开发笔记被2万开发者阅读过了,收获了github的star若干、公众号的关注若干、赞赏约50元。真的非常感谢大家的关注、star鼓励和金钱赞赏。

我整理了一下大家的需求,大体上可归纳为3类。

  1. 没有logo
  2. 在外部设备(鼠标/Trackpad真正属于的那台Mac)上容易引起误操作
  3. 一个关键的功能,左键拖拽功能,没有实现

在这篇博客中,我就来写一下对上面提到的三个需求的解决方案。完成上述三个需求后的软件在网盘可下载到,最新代码依然开源在github上。

需求

其实这件事挺尴尬的,我不是设计师,因此平时logo的主要来源是flaticon,这次没忍心打扰正在熟睡的非著名设计师Joseph,因此还是在flaticon上找的icon,进行了简单的颜色变换与组合。

icon考虑到了mouseSync具有鼠标和蓝牙的特性,因此还是比较写实的。考虑到蓝牙客户端有外部设备和数据中心(两台Mac)之分,因此两个icon虽然很相似,但仍有细微的不同。不同点在于,数据中心的icon多了一个类似信号指示的标志,表示它是接收蓝牙信号的一端。

由于本人设计方面实在业余,还请大家轻喷。有空再找非著名设计师Joseph实现一枚漂亮的logo。



> icons

在外部设备端容易引起误操作

之前的demo视频中,为了让两台电脑看起来一致,我录的是同时打开文件,同时在图标上右击,在现实生活中,在同一时刻同时操作两台Mac的需求很少不存在。通常情况下,用户还是想对某一台Mac进行操作的。mouseSync的定位应该是:帮助用户用一个鼠标操作两台电脑,且给用户决定什么时候操作哪台,省去两个鼠标之间换来换去的麻烦。

之前的项目中,我对鼠标事件添加的均是全局监控事件,因此只要mouseSync生命周期没有结束,它的操作将实时被发送到另一台mac上。我想过两个解决方案:

  1. 跟其他共享键盘的软件一样,使用全局快捷键对蓝牙通知服务进行开启/关闭
  2. 将全局监控事件变成局部监控事件,蓝牙通知服务仅在应用视图为Keywindow时有效

为什么选择Plan B

关于方案1,我觉得作为一款软件,快捷键应当由用户自己决定,虽然cocoapod上已经有非常好的项目KeyHolder进行支持,但是考虑到两个因素还是决定使用Plan B。

先看看Plan B,Plan B是这样的,首先让当前的window全屏,但是里面没有内容,因此无论在当前试图下如何操作,都不会有可能造成误操作。回到Plan A,快捷键的录入放到哪里呢?如果放到当前视图,将会带来一定的误操作风险;如果放到MenuBar,那么用户将不得不退出全屏模式,到菜单栏进行设置,这样的解决方案破坏了用户体验,综合考虑,最终还是选了Plan B。

实现过程中遇到的问题

让当前窗口一打开就全屏的方法是在ViewDidLoad方法体中进行控制的:

1
2
3
4
override func viewDidLoad() {
super.viewDidLoad()
self.view.window?.zoom(self)
}

但是我很快就发现了问题:尽管已经将应用全屏了,但是用户依然会有一定几率对dock栏和顶部菜单栏进行误操作。用户如果在操作另一台电脑的时候,还得注意这台电脑上是不是误操作了,这不符合我们软件开发的初衷。考虑到有些用户将dock放在底部,有些用户将dock放在左边,因此,我加了这样的限制,当用户将鼠标移动到距离屏幕边缘50px以内的时候,程序帮助他将鼠标移动至屏幕中央。参照下图,灰色地带就可以作为防止鼠标误操作的缓冲区。



> 防止对dock和菜单栏误操作解决方法示意图

阅读过上一篇博客的朋友一定注意到了,我们的程序是记录上一个鼠标点的位置,并将上一个鼠标点的位置与当前位置相减,将位置变动作为特征值进行发送。因此,在鼠标被程序移动到屏幕中央的时候,需要将上一个位置更新为屏幕中央的坐标。

1
2
3
4
//屏幕宽高的获取方式
let SCREEN_WIDTH = NSScreen.main()!.frame.width
let SCREEN_HEIGHT = NSScreen.main()!.frame.height
}

左键拖拽功能

左键拖一个文件或窗口到一个新位置这个功能还是经常被使用到的,我使用的是Trackpad,通常是使用三支拖移。想要监听外围设备的这个动作并不难,但是想让计算机模拟这个操作相对比较困难。就像双击不是单纯的模拟单击事件两次一样,这次我又踩到坑了,drag and move事件并不是简单的mousedown-> mousemove -> mouseup这么容易的事情。

我在Stack Overflow上找到了正确的方法:有一个叫kCGEventLeftMouseDragged的事件,专门来处理这件事。

1
2
3
4
5
6
void mouse_left_drag_to(float x, float y) {
CGEventRef left_drag_event = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDragged, CGPointMake(x, y), 0);
CGEventPost(kCGHIDEventTap, left_drag_event);
CFRelease(left_drag_event);
}

和Scroll滚轮事件一样,这里我们采用的是Quartz模拟鼠标事件,因此依然需要C与swift进行混编。模拟总共可分为3步:第一步是创建一个Quartz鼠标事件;第二步是将这个事件放到事件流中,并指定位置;第三步是release这个事件。

模拟这个事件的时候,随着鼠标拖拽后的不停移动,会不停产生event,而且event中x和y的偏移量始终是相对于第一次鼠标drag按下时的坐标。因此,我们需要记录两件事,第一件事是第一次drag时候的坐标,第二件事是当前是不是开启了新一轮的drag还是仍然在上一轮的drag中。核心代码如下:

1
2
3
4
5
6
7
8
if dragFirst == true{
dragFirst = false
mouseLocBeforeDrag = NSEvent.mouseLocation() mouseLocBeforeDrag.y = NSHeight(NSScreen.screens()![0].frame) - mouseLocBeforeDrag.y;
mouse_left_drag_to(Float(mouseLocBeforeDrag.x)+Float(dx!),Float(mouseLocBeforeDrag.y)-Float(dy!)
}else{
mouse_left_drag_to(Float(mouseLocBeforeDrag.x)+Float(dx!),Float(mouseLocBeforeDrag.y)-Float(dy!))
}

上面的代码有两处需要注意:

  1. dragFirst初始值为true,在mouseUp之后重新置为true,表示一轮的drag已经完成了;
  2. y的坐标是-,x的坐标是+,这是由OS X的坐标系决定的。

最后,我用两台电脑同时在画板上写了一个牛B。mouseSync的开发工作算是告一段落了,希望对大家有所帮助!



> drag事件demo

Reference

  1. https://stackoverflow.com/questions/1817628/clicking-the-mouse-down-to-drag-objects-on-mac
  2. https://www.flaticon.com

结束

试用软件之后如果您对软件有任何的意见与建议,欢迎留言。如果您喜欢这款软件,也欢迎您将它推荐给您的朋友们。让我们一起将mouseSync变得更好!

我会在订阅号里不定期分享我个人的macOS/ios开发心得和开发笔记,也会在里面发表对于苹果产品/框架/趋势的拙见,希望爱好科技产品或者苹果生态圈的开发者关注。相信本公众号一定能给您带来收获和启发。

【欢迎扫码关注微信公众号】



> 扫码关注微信公众号 骨灰级果粉 获得最新文章更新

~~如果我的博客内容帮助到了您,您可以使用支付宝/微信扫码请我喝一听可乐;如果您对我文章的内容有疑惑,欢迎留言或发email至zhihaozhang@me.com与我进一步交流~~

CATALOG
  1. 1. 背景
  2. 2. 需求
    1. 2.1. 没有logo
    2. 2.2. 在外部设备端容易引起误操作
      1. 2.2.1. 为什么选择Plan B
      2. 2.2.2. 实现过程中遇到的问题
    3. 2.3. 左键拖拽功能
  3. 3. Reference
  4. 4. 结束