idea插件开发--组件--编程久坐提醒
gitee地址:https://gitee.com/jyq_18792721831/studyplugin.git
idea插件開發入門
idea插件開發–配置
idea插件開發–服務-翻譯插件
idea插件開發–組件–編程久坐提醒
idea插件開發--組件--編程久坐提醒
- 介紹
- 組件
- 應用程序啟動
- 項目打開
- 模塊打開
- 應用程序/項目關閉
- 監聽程序
- 代碼中注冊監聽器
- 聲明注冊監聽器
- 項目級的監聽器
- 聲明注冊的其他配置
- 自定義監聽器接口
- 消息系統
- 設計
- 主題
- 消息總線
- 連接
- 廣播
- 嵌套消息
- 組件定義
- 應用程序級別
- 項目級別
- 監聽器定義
- Java 計時器
- 實例
- 需求
- 分解
- 項目創建
- 配置界面
- 存儲服務
- 配置和存儲
- 計時器
- 應用程序打開關閉監聽器
- 提醒對話框
- 額外的技術點
- 效果
- 總結
介紹
插件組件是一項遺留功能,支持與為舊版本的 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() {@Overridepublic void after(@NotNull List<? extends VFileEvent> events) {// handle the events} });聲明注冊監聽器
在實際開發的時候,當實現了一個監聽器接口,我們還需要去擴展點列表中找到對應關系,然后在把主題和監聽器進行注冊,這樣就比較麻煩。
所以在plugin.xml中注冊監聽器,允許我們指定監聽器接口,用監聽器接口代替訂閱的主題。
這樣就少了一個環節,避免在這個環節出錯。
<applicationListeners><listener class="myPlugin.MyVfsListener"topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/> </applicationListeners>監聽器的實現
public class MyVfsListener implements BulkFileListener {@Overridepublic void after(@NotNull List<? extends VFileEvent> events) {// handle the 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;}@Overridepublic void stateChanged(@NotNull ToolWindowManager toolWindowManager) {// handle the state change} }聲明注冊的其他配置
在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() {@Overridepublic void beforeAction(Context context) {// Process 'before action' event.}@Overridepublic void afterAction(Context context) {// Process 'after action' event.}}); }當然,我們應該盡可能使用聲明注冊監聽器
觸發
觸發代碼如下
public void doChange(Context context) {ChangeActionNotifier publisher = myBus.syncPublisher(ActionTopics.CHANGE_ACTION_TOPIC);publisher.beforeAction(context);try {// Do action// ...} finally {publisher.afterAction(context)} }- MessageBus實例可通過ComponentManager.getMessageBus()獲得 許多標準接口都實現了消息總線,例如Application和Project。
- IntelliJ平臺使用許多公共主題,例如AppTopics,ProjectTopics等。 ``因此,可以訂閱它們以接收有關處理的信息。
消息系統
在實際開發中,發布訂閱模式是一個非常棒的模式。
在idea中,消息的傳遞系統就是一個發布訂閱模式。并且在發布訂閱的基礎上,擴展了層級結構的廣播和特殊嵌套事件的傳遞。
設計
消息傳遞的終點是主題,每一個消息最終都會傳遞到主題停止,當然可能不止一個主題。客戶端可以訂閱消息總線中的主題,并且支持客戶端向消息總線中發布消息。
主題
主題有兩個核心的屬性,一個是可讀性的名字,用于區分不同的主題,這里的可讀是人類可讀;另一個屬性是廣播方向。前面說了,消息傳遞不僅僅是發布訂閱,還有層級結構的廣播,比如向下廣播,向上廣播,兄弟廣播之類的。理解主題的層級結構為樹形,我覺得更容易理解一點。
主題有兩種類型,分別為應用程序級別,和項目級別。
使用Topic的內部枚舉來區分AppLevel,ProjectLevel
消息總線
消息總線主要實現兩個功能:客戶端發布消息,監聽器訂閱主題。
可以認為所有的消息都要通過消息總線,在消息總線中通過的時候,就會分發給訂閱者。
連接
消息總線與客戶端建立關系的鏈接,它是實現訂閱的核心,更準確的說,它一方面關聯了消息總線,另一方面關聯了監聽器。
當有消息投遞的時候,消息總線就會首先把消息傳遞給連接,然后連接調用監聽器處理。
廣播
消息總線可以組織到層級結構中
如果topic1將廣播方向定義為*TO_CHILDREN,*我們會得到以下內容:
廣播方式:子廣播(默認),不廣播,父廣播。也是通過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 {@Overridepublic void initComponent() {Messages.showMessageDialog("initComponent", "applicationComponent", Messages.getInformationIcon());}@Overridepublic void disposeComponent() {Messages.showMessageDialog("disposeComponent", "applicationComponent", Messages.getInformationIcon());}@Overridepublic @NotNullString 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 {@Overridepublic void projectOpened() {Messages.showMessageDialog("projectOpen", "projectComponent", Messages.getInformationIcon());System.out.println("projectOpened");}@Overridepublic void projectClosed() {Messages.showMessageDialog("projectClosed", "projectComponent", Messages.getInformationIcon());System.out.println("projectClosed");}@Overridepublic void initComponent() {}@Overridepublic void disposeComponent() {}@Overridepublic @NotNullString 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 {@Overridepublic void appStarted() {NotificationUtil.error("appStarted");}@Overridepublic 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,跳過提醒。
分解的需求如下:
項目創建
首先創建一個項目,名字就是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);@Overridepublic @NotNull@NonNlsString getId() {return PLUGIN_CONFIG_ID;}@Overridepublic @NlsContexts.ConfigurableName String getDisplayName() {return PLUGIN_CONFIG_NAME;}@Overridepublic @NullableJComponent 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();}@Overridepublic 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();}@Overridepublic 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);@Overridepublic 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);@Overridepublic 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));}}@Overridepublic 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@Overrideprotected 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);// 每過1秒,進度條更新一次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);}@Overrideprotected @NullableJComponent createCenterPanel() {// 設置進度條最大值,也就是休息時間int restTime = configService.get(PLUGIN_REST_TIME, DEFAULT_REST_TIME) * 60;progressBar.setMaximum(restTime);timeLabel.setText(String.valueOf(restTime));// 設置進度條開始progressBar.setMinimum(0);// 進度條初始化為0progressBar.setValue(0);// 計時器啟動timer.start();return rootJPanel;}@Overrideprotected JComponent createSouthPanel() {// 隱藏 ok 取消按鈕return null;}@SneakyThrows@Overridepublic 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的界面。
其實時間的存儲中,開發的時候也遇到了一定的困難,比如時間和時間戳的相互轉化,時區的獲取。
也逐漸讓我明白了,打印日志是多么的重要,特別是這種多線程的開發的時候,不打印日志,即使有斷點調試,梳理多個線程之間的互相調用,也是比較難的。好的日志可以讓問題一目了然。
總的來說,收獲良多。
總結
以上是生活随笔為你收集整理的idea插件开发--组件--编程久坐提醒的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [NLP]——BPE、WordPiece
- 下一篇: Leaflet地图初始化地图(谷歌+天地