gitee地址:https://gitee.com/jyq_18792721831/studyplugin.git idea插件开发入门 idea插件开发–配置 idea插件开发–服务-翻译插件 idea插件开发–组件–编程久坐提醒
介绍
插件组件是一项遗留功能,支持与为旧版本的 IntelliJ 平台创建的插件兼容。使用组件的插件不支持动态加载(在不重新启动 IDE 的情况下安装、更新和卸载插件的功能)。
插件组件在plugin.xml 中配置,配置的标签有<application-components> ,<project-components> 和<module-components> 三种。
分别对应idea第一次打开,打开项目,打开模块。
不过组件目前不支持使用。
官方建议使用服务,订阅状态替换组件的使用,并很有可能在未来废弃活删除组件。
服务
如果是借助组件进行初始化一些对象,或者准备一些数据,或者服务之类的,而且基本上是所有IDE都相同的,那么可以使用服务来替换。
存储
如果是在组件中存储一些信息,不管是应用程序级别的或者是项目级别的,建议使用持久化来替换。
订阅事件
如果需要在应用程序第一次打开触发,或者项目第一次打开触发,或者模块第一次打开触发,那么建议订阅事件来替换组件。
组件
说是组件,可能不好理解,我自己的理解是,组件实际上是触发的事件。
比如<application-components> 标签下定义的组件,实际上就是订阅了应用程序打开的事件,当应用程序打开时,会触发这些订阅了应用程序打开事件的监听,从而执行一些逻辑。
应用程序启动
官方不建议在应用程序启动的时候执行代码,因为这会减慢启动速度。插件应该在打开项目活用户调用插件的时候执行,如果必须在应用程序启动的时候执行,那么现在可以有以下几种方式实现。
组件
application-components 组件,这些组件,会在应用程序启动的时候执行。但是不建议使用,有组件废弃的可能。
订阅
订阅AppLifecycleListener 监听器的主题,以便在应用程序打开时触发。
执行一次
如果只是想代码执行一次,那么可以使用RunOnceUtil 工具类实现。
数据准备
如果只是想在应用程序启动的时候,开始提前为插件的工作准备条件,那么可以在应用程序启动的时候,增加后台任务,比如预加载活动PreloadingActivity 接口
项目打开
官方比较建议的是在项目打开的时候,执行代码。
组件
project-components 组件,这里的组件会在项目打开的时候执行,也是不建议使用的,有组件废弃的可能。
扩展点
对于项目打开有两种扩展点:前台执行,后台执行。
com.intellij.postStartupActivity 是前台执行的扩展点,也是当项目打开的时候会立即执行。
com.intellij.backgroundPostStartupActivity 是后台执行的扩展点,当项目打开后,会延迟大约5秒执行(2019.3及以后的版本)。
执行一次
如果只是想代码执行一次,那么可以使用RunOnceUtil 工具类实现。
模块打开
随着微服务的兴起,我们一个项目中存在多个模块已经是不争的事实了,所以官方实际上是不建议在模块打开的时候执行代码,因为这意味着当一个项目被打开,那么可能有多个模块被打开。
组件
module-components 组件,这里的组件会在模块打开的时候执行,不建议使用。
除了因为组件可能被废弃,新的解决方案中并不支持在模块打开的时候执行代码。
应用程序/项目关闭
对于应用程序或者项目关闭时执行代码,实际上并没有做单独的处理,而是巧妙的借助服务实现的。
我们定义服务是可以指定作用域的,比如应用程序范围内,或者项目范围内。
而且服务是可以实现Dispose 接口的。
这样,当我们想要在项目关闭的时候执行代码,那么只需要定义一个项目范围内的服务,然后让服务实现Dispose 接口,然后把需要在项目关闭的时候执行的代码放在Dispose 接口中即可。
如果想要在应用程序关闭的时候执行代码,那么也是类似,定义一个应用程序范围内的服务,也是实现Dispose 接口,把需要在应用程序关闭的时候执行的代码放在Dispose 接口内。
监听程序
监听器允许插件以声明的方式订阅通过消息总线传递的事件,监听器必须是无状态的,并且不能实现生命周期,比如Disposeable 。
监听器有两种作用域:应用程序级别和项目级别。
监听器可以订阅的全部主题列表和应该实现的监听接口扩展点列表|IntelliJ Platform Plugin SDK (jetbrains.com)
监听器的声明性注册拥有比代码注册有更好的性能。因为声明注册的监听器实例是懒创建的,第一次事件触发时才会创建监听器实例,而不是在应用程序启动或者项目打开的期间。
从2019.3版本开始,支持在plugin.xml 中定义监听器。
应用程序级别的监听器
<idea-plugin>
<applicationListeners>
<listener class="myPlugin.MyListenerClass" topic="BaseListenerInterface"/>
</applicationListeners>
</idea-plugin>
这里的class就是监听器的具体实现,而TOPIC就是我们关注的主题,或者说订阅的主题。
除了扩展点列表中的主题,我们也可以自己通过Topic 类创建自定义的主题。
你也可以像扩展点列表中一样,要求监听器实现哪些操作,从而定义接口。
代码中注册监听器
在代码中声明监听器,我们首先需要将监听器和订阅的主题,注册到消息总线,然后处理触发后的操作
比如监听有关虚拟文件系统更改的事件
messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
@Override
public void after(@NotNull List<? extends VFileEvent> events) {
}
});
声明注册监听器
在实际开发的时候,当实现了一个监听器接口,我们还需要去扩展点列表中找到对应关系,然后在把主题和监听器进行注册,这样就比较麻烦。
所以在plugin.xml 中注册监听器,允许我们指定监听器接口,用监听器接口代替订阅的主题。
这样就少了一个环节,避免在这个环节出错。
<applicationListeners>
<listener class="myPlugin.MyVfsListener"
topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
</applicationListeners>
监听器的实现
public class MyVfsListener implements BulkFileListener {
@Override
public void after(@NotNull List<? extends VFileEvent> events) {
}
}
项目级的监听器
上面讲的都是应用程序级别的监听器,如果我们需要定义项目级别的监听器,就需要对项目做区分。
首先,在plugin.xml 中使用projectListeners 声明
<idea-plugin>
<projectListeners>
<listener class="MyToolwindowListener"
topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener" />
</projectListeners>
</idea-plugin>
然后在监听器实现中,传入多项目之间的区分,project 对象。
传入方式为构造器注入,就是写一个Project 参数的构造器,这样当创建监听器实例的时候,就会把Project 传入,注意,必须是Project 类型。
在idea插件中,构造器注入是一种常见的方式,但是需要注意,支持构造器注入的,一般也就是Project 对象,有一些还支持Module 对象,使用构造器注入应该小心。
public class MyToolwindowListener implements ToolWindowManagerListener {
private final Project project;
public MyToolwindowListener(Project project) {
this.project = project;
}
@Override
public void stateChanged(@NotNull ToolWindowManager toolWindowManager) {
}
}
声明注册的其他配置
在plugin.xml 中声明监听器,除了上面用到的属性,还有一些其他的属性:
- os:允许监听器只监听给定的操作系统,比如os=“windows”,这个属性需要在2020.1以及之后的版本中使用。
- activeInTextMode:测试环境中禁用或启用监听器
- activeInHeadlessMode:在另一种测试环境中禁用监听器
这些都比较少用。
自定义监听器接口
首先应该在接口中指定监听器订阅的主题,接着定义操作
public interface ChangeActionNotifier {
Topic<ChangeActionNotifier> CHANGE_ACTION_TOPIC = Topic.create("custom name", ChangeActionNotifier.class)
void beforeAction(Context context);
void afterAction(Context context);
}
订阅操作
代码注册如下
public void init(MessageBus bus) {
bus.connect().subscribe(ActionTopics.CHANGE_ACTION_TOPIC, new ChangeActionNotifier() {
@Override
public void beforeAction(Context context) {
}
@Override
public void afterAction(Context context) {
}
});
}
当然,我们应该尽可能使用声明注册监听器
触发
触发代码如下
public void doChange(Context context) {
ChangeActionNotifier publisher = myBus.syncPublisher(ActionTopics.CHANGE_ACTION_TOPIC);
publisher.beforeAction(context);
try {
} finally {
publisher.afterAction(context)
}
}
消息系统
在实际开发中,发布订阅模式是一个非常棒的模式。
在idea中,消息的传递系统就是一个发布订阅模式。并且在发布订阅的基础上,扩展了层级结构的广播和特殊嵌套事件的传递。
设计
消息传递的终点是主题,每一个消息最终都会传递到主题停止,当然可能不止一个主题。客户端可以订阅消息总线中的主题,并且支持客户端向消息总线中发布消息。
主题
主题有两个核心的属性,一个是可读性的名字,用于区分不同的主题,这里的可读是人类可读;另一个属性是广播方向。前面说了,消息传递不仅仅是发布订阅,还有层级结构的广播,比如向下广播,向上广播,兄弟广播之类的。理解主题的层级结构为树形,我觉得更容易理解一点。
主题有两种类型,分别为应用程序级别,和项目级别。
使用Topic 的内部枚举来区分AppLevel,ProjectLevel
消息总线
消息总线主要实现两个功能:客户端发布消息,监听器订阅主题。
可以认为所有的消息都要通过消息总线,在消息总线中通过的时候,就会分发给订阅者。
连接
消息总线与客户端建立关系的链接,它是实现订阅的核心,更准确的说,它一方面关联了消息总线,另一方面关联了监听器。
当有消息投递的时候,消息总线就会首先把消息传递给连接,然后连接调用监听器处理。
广播
消息总线可以组织到层级结构中
如果topic1将广播方向定义为*TO_CHILDREN,*我们会得到以下内容:
- 通过应用程序总线将消息发送到topic1;
- 处理程序 1收到有关消息的通知;
- 消息将传递到项目总线**(handler2和*handler3)*中同一主题的订阅者);
广播方式:子广播(默认),不广播,父广播。也是通过Topic 类中的内部枚举定义。
嵌套消息
消息系统保证发送到某个主题的所有消息的顺序都是一定的。
- 消息1已发送;
- handler1接收message1并将message2发送到同一主题;
- 处理程序 2接收消息 1;
- 处理程序 2接收消息 2;
- 处理程序 1接收消息 2;
组件定义
应用程序级别
在plugin.xml 中声明
<application-components>
<component>
<implementation-class>com.study.plugin.sedentaryreminder.components.MyApplicationComponent</implementation-class>
</component>
</application-components>
然后新增组件实现类,实现类实现ApplicationComponent 接口。
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.ui.Messages;
import org.jetbrains.annotations.NotNull;
public class MyApplicationComponent implements ApplicationComponent {
@Override
public void initComponent() {
Messages.showMessageDialog("initComponent", "applicationComponent", Messages.getInformationIcon());
}
@Override
public void disposeComponent() {
Messages.showMessageDialog("disposeComponent", "applicationComponent", Messages.getInformationIcon());
}
@Override
public @NotNull
String getComponentName() {
return "MyApplicationComponent";
}
}
效果
项目级别
项目级别的使用project-components
<project-components>
<component>
<implementation-class>com.study.plugin.sedentaryreminder.components.MyProjectComponent</implementation-class>
</component>
</project-components>
实现类实现接口ProjectComponent
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.ui.Messages;
import org.jetbrains.annotations.NotNull;
public class MyProjectComponent implements ProjectComponent {
@Override
public void projectOpened() {
Messages.showMessageDialog("projectOpen", "projectComponent", Messages.getInformationIcon());
System.out.println("projectOpened");
}
@Override
public void projectClosed() {
Messages.showMessageDialog("projectClosed", "projectComponent", Messages.getInformationIcon());
System.out.println("projectClosed");
}
@Override
public void initComponent() {
}
@Override
public void disposeComponent() {
}
@Override
public @NotNull
String getComponentName() {
return "MyProjectComponent";
}
}
效果
监听器定义
在plugin.xml 中声明定义
<applicationListeners>
<listener class="com.study.plugin.sedentaryreminder.listeners.MyApplicationOpenListener" topic="com.intellij.ide.AppLifecycleListener"/>
</applicationListeners>
这里标签加入后,会变红,检测不通过,是因为plugin.xml 中idea-version 配置的不支持监听器的版本,要使用监听器,那么idea的版本必须是2019.3及之后的版本,修改原来的173.0版本为193.0,就不会报红了
然后业务实现Topic的接口即可
import com.intellij.ide.AppLifecycleListener;
import com.study.plugin.sedentaryreminder.utils.NotificationUtil;
public class MyApplicationOpenListener implements AppLifecycleListener {
@Override
public void appStarted() {
NotificationUtil.error("appStarted");
}
@Override
public void appClosing() {
NotificationUtil.error("appClosing");
}
}
查看接口,发现区分的比组件更详细。
效果
Java 计时器
在Java中要实现定时执行某项任务就需要用到Timer类和TimerTask类。其中,Timer类可以实现在某一刻时间或某一段时间后安排某一个任务执行一次或定期重复执行,该功能需要与TimerTask类配合使用。TimerTask类表示由Timer类安排的一次或多次重复执行的那个任务。
方法 | 描述 |
---|
void cancel() | 终止此计时器,丢弃所有当前已安排的任务,对当前正在执行的任务没有影响 | int purge() | 从此计时器的任务队列中移除所有已取消的任务,一般用来释放内存空间 | void schedule(TimerTask task, Date time) | 安排在指定的时间执行指定的任务 | void schedule(TimerTask task, Date firstTime, long period) | 安排指定的任务在指定的时间开始进行重复的固定延迟执行 | void schedule(TimerTask task, long delay) | 安排在指定延迟后执行指定的任务 | void schedule(TimerTask task, long delay, long period) | 安排指定的任务从指定的延迟后开始进行重复的固定延迟执行 | void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) | 安排指定的任务在指定的时间开始进行重复的固定速率执行 | void scheduleAtFixedRate(TimerTask task, long delay, long period) | 安排指定的任务在指定的延迟后开始进行重复的固定速率执行 |
时间都是毫秒为单位
schedule()和scheduleAtFixedRate()方法的区别
schedule()方法的执行时间间隔永远的是固定的,如果之前出现了延迟情况,那么之后也会继续按照设定好的时间间隔来执行
scheduleAtFixedRate()方法在出现延迟情况时,则将快读连续地出现两次或更多的执行,从而使后续执行能够追赶上来。从长远来看,执行的频率将正好是指定的周期。
实例
我们接下来用一个小例子来应用所学。
开发一个编程久坐提醒。
需求
随着开发任务越来越重,经济下行,每个人在电脑前编程的时间越来越长,而久坐会导致许多疾病的发生,比如腹部肥胖,腰间盘突出等,所以在编程一段时间后,ide能提醒开发者,你应该休息一下,活动一下。
分解
首先需要有配置,每个人身体状况不同,所以可以自定义每隔多长时间提醒一次,然后每次休息多长时间。
有的人自制力好点,到了时间就休息,但是有的人却是工作狂,工作不完成,誓不休息;所以应该可以配置是否可豁免。
当然,有些时候是需要暂时关闭提醒功能的,所以可以配置,今日是否提醒。
从每天第一次打开ide开始计时,中间关闭ide时候停止计时,然后计算累计时间,防止有人不讲武德,每次快到时间了,重启ide,跳过提醒。
分解的需求如下:
- 配置界面配置提醒间隔时长,以及休息时间
- 提醒窗口是否是模式对话框
- 提醒窗口实现倒计时
- ide关闭暂停计时,ide打开开始计时,计时每日清零
项目创建
首先创建一个项目,名字就是sedentaryreminder ,然后创建目录结构
配置界面
配置界面长这个样子
别忘记增加一个监听器,如果输入的时间不在1小时内,给出提示
效果
存储服务
存储服务将配置存储,防止用户重新打开后配置的信息丢失。
存储服务非常简单,主要是巩固之前的轻量级服务idea插件开发–服务-翻译插件_a18792721831的博客-CSDN博客
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.components.Service;
@Service
public final class SedentaryReminderConfigService {
private final PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
public void save(String key, String value) {
propertiesComponent.setValue(key, value);
}
public void save(String key, Integer value) {
propertiesComponent.setValue(key, value, 0);
}
public void save(String key, Boolean value) {
propertiesComponent.setValue(key, value);
}
public void clear(String key) {
propertiesComponent.unsetValue(key);
}
public String get(String key, String defValue) {
return propertiesComponent.getValue(key, defValue);
}
public int get(String key, int defValue) {
return propertiesComponent.getInt(key, defValue);
}
public boolean get(String key, boolean defValue) {
return propertiesComponent.getBoolean(key, defValue);
}
}
配置和存储
配置界面也是非常的简单,实现基本要求即可idea插件开发–配置_a18792721831的博客-CSDN博客
配置setting中绘制界面的时候,需要先从存储服务中获取已存储的值,然后设置为配置界面的值,当发生修改的时候,存储起来即可。
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.util.NlsContexts;
import com.study.plugin.sedentaryreminder.service.SedentaryReminderConfigService;
import com.study.plugin.sedentaryreminder.ui.SedentaryReminderConfigUI;
import com.study.plugin.sedentaryreminder.utils.PluginAppKeys;
import java.util.Objects;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.JComponent;
public class SedentaryReminderConfig implements SearchableConfigurable, PluginAppKeys {
private SedentaryReminderConfigUI ui = new SedentaryReminderConfigUI();
private SedentaryReminderConfigService configService = ApplicationManager.getApplication().getService(SedentaryReminderConfigService.class);
@Override
public @NotNull
@NonNls
String getId() {
return PLUGIN_CONFIG_ID;
}
@Override
public @NlsContexts.ConfigurableName String getDisplayName() {
return PLUGIN_CONFIG_NAME;
}
@Override
public @Nullable
JComponent createComponent() {
ui.setIntervalTime(configService.get(PLUGIN_INTERVAL_TIME, DEFAULT_INTERVAL_TIME));
ui.setRestTime(configService.get(PLUGIN_REST_TIME, DEFAULT_REST_TIME));
ui.setCompulsionRest(configService.get(PLUGIN_COMPULSION_REST, DEFAULT_COMPULSION_REST));
ui.setTodaySkipReminder(configService.get(PLUGIN_TODAY_SKIP_REMINDER, DEFAULT_TODAY_SKIP_REMINDER));
return ui.getRootPanel();
}
@Override
public boolean isModified() {
return configService.get(PLUGIN_INTERVAL_TIME, DEFAULT_INTERVAL_TIME) != ui.getIntevalTime() ||
configService.get(PLUGIN_REST_TIME, DEFAULT_REST_TIME) != ui.getRestTime() ||
configService.get(PLUGIN_COMPULSION_REST, DEFAULT_COMPULSION_REST) != ui.getCompulsionRest() ||
configService.get(PLUGIN_TODAY_SKIP_REMINDER, DEFAULT_TODAY_SKIP_REMINDER) != ui.getTodaySkipReminder();
}
@Override
public void apply() throws ConfigurationException {
Integer intevalTime = ui.getIntevalTime();
if (Objects.nonNull(intevalTime)) {
configService.save(PLUGIN_INTERVAL_TIME, intevalTime);
}
Integer restTime = ui.getRestTime();
if (Objects.nonNull(restTime)) {
configService.save(PLUGIN_REST_TIME, restTime);
}
configService.save(PLUGIN_COMPULSION_REST, ui.getCompulsionRest());
configService.save(PLUGIN_TODAY_SKIP_REMINDER, ui.getTodaySkipReminder());
}
}
计时器
当计时器触发的时候,需要记录下本次提醒时间,以及清空已经编程时间,然后展示提醒对话框
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.study.plugin.sedentaryreminder.service.SedentaryReminderConfigService;
import com.study.plugin.sedentaryreminder.ui.ReminderDialog;
import com.study.plugin.sedentaryreminder.utils.PluginAppKeys;
import java.time.LocalDateTime;
import java.util.TimerTask;
public class ReminderTask extends TimerTask implements PluginAppKeys {
private SedentaryReminderConfigService configService = ApplicationManager.getApplication().getService(SedentaryReminderConfigService.class);
private static final Logger log = Logger.getInstance(ReminderTask.class);
@Override
public void run() {
log.info("reminder timer task is run");
configService.save(SEDENTARY_REMINDER_LAST_REMINDER_DATE, LocalDateTime.now());
configService.clear(SEDENTARY_REMINDER_LAST_USE_DATE);
log.info("last reminder date is save : " + configService.get(SEDENTARY_REMINDER_LAST_REMINDER_DATE, LocalDateTime.now()) +
", last use date is clear : " +
configService.get(SEDENTARY_REMINDER_LAST_USE_DATE, 0L));
new ReminderDialog().show();
log.info("reminder dialog is show");
}
}
应用程序打开关闭监听器
当应用程序打开的时候,需要读取上次提醒时间以及编程已用时间,然后获取当前时间,判断上次提醒时间是否是当天,如果是同一天,那么继续上次编程时间计时,如果不是同一天那么清空上次编程时间。
也就是每天需要独立计时。
接着需要判断是否今日跳过提醒,如果需要今日跳过提醒,那么结束,否则继续后续操作。
如果今日不可跳过,那么获取最大编程时间和休息时间,然后启动计时器。
如果是同一天,需要继续上次编程已用时间继续计时,否则从0开始计时
当应用程序关闭的时候,需要终止计时器,并放弃所有的任务,同时释放计时器内存。
如果今日可跳过,那么结束。
如果今日不可跳过,那么获取上次提醒时间,获取休息时间,获取允许的最大编程时间和当前时间,计算编程已用时间
编程已用时间 = 当前时间 - 上次提醒时间 - 休息时间
如果编程已用时间大于最大允许的编程时间,那么是原来今日跳过提醒修改为今日提醒,此时设置编程已用时间为0,然后记录编程已用时间。
别忘记在plugin.xml 中注册监听器。
代码如下
import com.intellij.ide.AppLifecycleListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.study.plugin.sedentaryreminder.service.SedentaryReminderConfigService;
import com.study.plugin.sedentaryreminder.task.ReminderTask;
import com.study.plugin.sedentaryreminder.utils.PluginAppKeys;
import java.time.LocalDateTime;
import java.util.Timer;
public class SedentaryReminderApplicationListener implements AppLifecycleListener, PluginAppKeys {
private SedentaryReminderConfigService configService = ApplicationManager.getApplication().getService(SedentaryReminderConfigService.class);
private Timer timer = new Timer();
private static final Logger log = Logger.getInstance(SedentaryReminderApplicationListener.class);
@Override
public void appStarted() {
LocalDateTime lastReminderDate = configService.get(SEDENTARY_REMINDER_LAST_REMINDER_DATE, LocalDateTime.now());
log.info("app start last reminder date : " + lastReminderDate);
long lastUseDateSeconds = configService.get(SEDENTARY_REMINDER_LAST_USE_DATE, 0L);
log.info("app start last use date : " + lastUseDateSeconds);
LocalDateTime now = LocalDateTime.now();
if (now.getDayOfMonth() != lastReminderDate.getDayOfMonth()) {
configService.clear(PLUGIN_TODAY_SKIP_REMINDER);
log.info("app start last reminder not today, clear today skip reminder");
}
boolean todaySkipReminder = configService.get(PLUGIN_TODAY_SKIP_REMINDER, false);
if (todaySkipReminder) {
log.info("app start todaySkipReminder is true");
return;
}
int intervalTime = configService.get(PLUGIN_INTERVAL_TIME, DEFAULT_INTERVAL_TIME);
int restTime = configService.get(PLUGIN_REST_TIME, DEFAULT_REST_TIME);
if (now.getDayOfMonth() != lastReminderDate.getDayOfMonth()) {
log.info("app start last reminder date not today");
configService.save(SEDENTARY_REMINDER_LAST_USE_DATE, 0L);
log.info("app start save last use date 0");
timer.schedule(new ReminderTask(), intervalTime * 60 * 1000, (intervalTime + restTime) * 60 * 1000);
log.info("app start first reminder in " + intervalTime + " min");
log.info("app start reminder interval is " + (intervalTime + restTime));
}
else {
log.info("app start last reminder date is today");
timer.schedule(new ReminderTask(), (intervalTime * 60 - lastUseDateSeconds) * 1000, (intervalTime + restTime) * 60 * 1000);
log.info("app start first reminder in " + (intervalTime * 60 - lastUseDateSeconds) + " sec");
log.info("app start reminder interval is " + (intervalTime + restTime));
}
}
@Override
public void appWillBeClosed(boolean isRestart) {
timer.cancel();
timer.purge();
log.info("app colsed timer is stop");
boolean todaySkipReminder = configService.get(PLUGIN_TODAY_SKIP_REMINDER, false);
if (todaySkipReminder) {
log.info("app closed todaySkipReminder is true");
return;
}
LocalDateTime lastReminderTime = configService.get(SEDENTARY_REMINDER_LAST_REMINDER_DATE, LocalDateTime.now());
int restTime = configService.get(PLUGIN_REST_TIME, DEFAULT_REST_TIME);
int intervalTime = configService.get(PLUGIN_INTERVAL_TIME, DEFAULT_INTERVAL_TIME);
LocalDateTime now = LocalDateTime.now();
long lastUseTime = now.toEpochSecond(currentZoneOffset) - lastReminderTime.toEpochSecond(currentZoneOffset) - restTime * 60;
lastUseTime = lastUseTime > intervalTime ? 0 : lastUseTime;
configService.save(SEDENTARY_REMINDER_LAST_USE_DATE,
lastUseTime);
log.info("app closed last use date is save : " + configService.get(SEDENTARY_REMINDER_LAST_USE_DATE, 0L));
}
}
提醒对话框
提醒对话框继承DialogWrapper 类,DiaWrapper 类是idea平台封装的对话框的基类。
提醒对话框首先需要一个JPanel用于存放其他控件,也就是rootJPanel。
然后使用方位布局,在中间放一个进度条,在上面放一个倒计时的JLabel,用于显示倒计时。
同时需要一个适配swing的计时器,用于更新进度条。
特别需要注意的是,swing的更新操作全部需要放在EDT线程中,详见Java多线程开发系列之番外篇:事件派发线程—EventDispatchThread - 王若伊_恩赐解脱 - 博客园 (cnblogs.com)
而DialogWrapper 类的很多操作都会检测线程是否是EDT线程,如果不是EDT线程,那么就会阻止用户更新界面,所以我们需要重写这些会检查线程的操作,如果当前线程不是EDT线程,需要提交事件到EDT事件队列中。
在初始化界面的时候,需要给计时器绑定更新操作,更新操作主要是更新进度条和倒计时。
然后给进度条增加监听,当进度条满的时候,使用EDT关闭对话框
更别忘记设置取消不可用。
在idea创建对话框面板的时候,需要根据配置设置进度条的初始值,最大值和最小值,并启动计时器。
然后重写对话框下面的按钮,隐藏确定,取消按钮
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.ui.DialogWrapper;
import com.study.plugin.sedentaryreminder.service.SedentaryReminderConfigService;
import com.study.plugin.sedentaryreminder.utils.PluginAppKeys;
import java.awt.BorderLayout;
import lombok.SneakyThrows;
import org.jetbrains.annotations.Nullable;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class ReminderDialog extends DialogWrapper implements PluginAppKeys {
private static final Logger log = Logger.getInstance(ReminderDialog.class);
private JPanel rootJPanel = new JPanel();
private JProgressBar progressBar = new JProgressBar();
private Timer timer;
private JLabel timeLabel;
private SedentaryReminderConfigService configService = ApplicationManager.getApplication().getService(SedentaryReminderConfigService.class);
public ReminderDialog() {
super(true);
setModal(configService.get(PLUGIN_COMPULSION_REST, DEFAULT_COMPULSION_REST));
setTitle("休息中~");
initJPanel();
init();
}
@SneakyThrows
@Override
protected void init() {
if (SwingUtilities.isEventDispatchThread()) {
super.init();
} else {
SwingUtilities.invokeAndWait(() -> super.init());
}
}
private void initJPanel() {
rootJPanel.setLayout(new BorderLayout());
timeLabel = new JLabel();
rootJPanel.add(timeLabel, BorderLayout.NORTH);
rootJPanel.add(progressBar, BorderLayout.CENTER);
progressBar.setBorderPainted(true);
timer = new Timer(1000, e -> {
progressBar.setValue(progressBar.getValue() + 1);
timeLabel.setText(String.valueOf(progressBar.getMaximum() - progressBar.getValue()));
});
progressBar.addChangeListener(e -> {
Object source = e.getSource();
if (source instanceof JProgressBar) {
JProgressBar bar = (JProgressBar) source;
if (bar.getValue() == bar.getMaximum()) {
timer.stop();
SwingUtilities.invokeLater(() -> close(CLOSE_EXIT_CODE));
log.info("reminder dialog will be closed");
}
}
});
getCancelAction().setEnabled(false);
}
@Override
protected @Nullable
JComponent createCenterPanel() {
int restTime = configService.get(PLUGIN_REST_TIME, DEFAULT_REST_TIME) * 60;
progressBar.setMaximum(restTime);
timeLabel.setText(String.valueOf(restTime));
progressBar.setMinimum(0);
progressBar.setValue(0);
timer.start();
return rootJPanel;
}
@Override
protected JComponent createSouthPanel() {
return null;
}
@SneakyThrows
@Override
public void show() {
if (SwingUtilities.isEventDispatchThread()) {
super.show();
} else {
SwingUtilities.invokeAndWait(() -> super.show());
}
}
}
额外的技术点
休息倒计时是使用swing适配的计时器完成,是一个可复用的计时器,基本原理和java计时器相同,相关的使用方式见Java Swing Timer:计时器组件 (biancheng.net)
进度条控件也是swing封装的一个组件,使用起来需要用户自己更新进度条的值,一般是配合swing适配的计时器使用,相关资料见Java Swing JProgressBar:进度条组件 (biancheng.net)
还有就是我们存储时间时候,存储的是时间戳,获取时间的时间戳,然后把时间戳作为字符串存储。
时间使用LocalDateTime ,而LocalDataTime 和时间戳的互转,
LocalDateTime -> 时间戳
使用LocalDateTime.toEpochSecond 方法,参数是时区。
时间戳 -> LocalDateTime
使用LocalDateTime.ofEpochSecond 方法,参数是时间戳的秒,纳秒我们设置为0,然后在传入时区即可。
操作系统的时区获取
使用OffsetDateTime.now().getOffset() 获取操作系统默认的时区。
日志
idea插件打印日志需要使用idea平台的日志类,创建日志对象。
com.intellij.openapi.diagnostic.Logger.getInstance(ReminderTask.class)
效果
强制休息时,会展示如下模式对话框,此时你是无法操作的,同时会自动将鼠标焦点聚焦到模式对话框上。
你点击叉叉是无法取消对话框的,而且你也无法操作其他的。
只能等待倒计时结束,自动关闭对话框。
而且当你重启后,还会接着上次编程已用时间继续倒计时。
默认是每编程25分钟,休息5分钟。
你可以自己配置编程时间,编程时间不能大于1小时。
你可以在未触发提醒对话框的时候配置今日跳过,并重启idea后生效。
当然你也可以配置非模式对话框,只是提醒,而不强制。
总结
这个小插件的灵感来源于运动手环,运动手环有久坐提醒,每当我们久坐1小时,手环就会震动,提醒我们活动一下,但是很多时候,我们并不会按照提醒进行休息。
开发编程久坐提醒一方面是强制休息,另一方面是提醒休息。
总的来说这个插件还是有一定挑战性的,开发过程中的一些技术点,是之前并不了解的,所以这个插件的开发难度一度出乎了我的预期,好在网上有许多大神的总结,一步一步的攻克,完成了这个插件。
通过这个插件,首先是了解了idea插件的组件,包括组件的定义,使用以及idea自己对组件的演变。
接着了解了组件的替代者,有监听器,有工具类等,idea提供了多种方式实现原本组件的功能。
同时也是进一步体会到了技术的发展对开发工具的影响,比如随着微服务的兴起,项目内模块的数量迅速增加,此前提供的模块级别的组件,此时就不太适合了,那么idea就抛弃了组件这种功能,转为其他方式实现。
然后是了解了idea中的消息系统,以及idea是如何实现的消息系统,idea中各个控件如何相互配合,多个线程之间的状态如何进行数据的传递,以及Idea对消息系统中发布订阅模型的客户化修改。
当然,还有最重要的监听器,可以说,监听器可以关注订阅idea中任何状态,事件和操作,都允许插件开发者对这些信息做自己关注的处理。
除此之外,对jdk中提供的计时器有了一定的了解,计时器的使用,原理和计算方式。
接着是如何使用swing中的进度条的控件,包括进度条的创建,使用和更新,以及进度条值得监控。
swing对计时器的适配,使得使用计时器更新进度条更加简便。
在后则是idea中提供的对话框的封装,以及如何使用重写机制,来修改父类中对话框的绘制,以及如何创建对话框,展示对话框和关闭对话框。
在对话框中了解到了swing中对于多个线程对相同数据的竞争是如何解决的,以及EDT线程是什么,如何避免EDT线程检测,如何正确的在EDT线程之外操作swing的界面。
其实时间的存储中,开发的时候也遇到了一定的困难,比如时间和时间戳的相互转化,时区的获取。
也逐渐让我明白了,打印日志是多么的重要,特别是这种多线程的开发的时候,不打印日志,即使有断点调试,梳理多个线程之间的互相调用,也是比较难的。好的日志可以让问题一目了然。
总的来说,收获良多。
|