Remember the State

本文介绍了如何在Eclipse中实现应用状态的保存与恢复,包括窗口与视角的状态、视图选择状态、对话框大小位置及组合框历史记录等。通过使用IMemento和IDialogSettings等API,可以有效地提升用户体验。

As a rule of thumb, your application should try to remember the state across sessions. So when a user hits the close button and then opens it after few mins/days, it should present exactly in the same way where he left. In this tip, I'm going to explain few things of how this could be achieved.

The first and foremost thing is the number of windows opened and current perspective and its state in each window. If you are writing a plugin for the IDE, you need not worry about this. Eclipse does that for you. If you are an RCP app, you have to do it yourself. You have to do it in the WorkbenchAdvisor.initialize() method:

01. public class ApplicationWorkbenchAdvisor extends WorkbenchAdvisor {
02.    
03.   @Override
04.   public void initialize(IWorkbenchConfigurer configurer) {
05.  
06.    super .initialize(configurer);
07.    configurer.setSaveAndRestore( true );
08.   }
09.  
10.   // other methods ...
11. }


Now this will take care of lot of things - the number of windows that are opened; location & size of those windows; perspective of each window; view & editors in each perspective; their locations,... All by setting that one boolean variable!

Now that our views and editors are restored by Eclipse, we need to ensure the state of them. Most of the views will be tree/table based. In these cases, the current selection is an important one to restore.

To store these items and anyother stuff you want to, Eclipse provides IMemento. An instance of this IMemento is passed to the view when its initialized and when workbench is closed. The information can be stored hierarchically with string keys and it will be persisted as an XML. If you are wondering why can't it be as simple as passing a Serializable Java object and the workbench persisting it, the answer is simple. The same class may not be there when Eclipse starts next time or even if the same class is available, it might have changed. IMemento avoids this problem by persisting the stae as an XML string.

So how do we use it? In the view, you have to override the init() and saveState() methods:

01. @Override
02. public void init(IViewSite site, IMemento memento) throws PartInitException {
03.   this .memento = memento;
04.   super .init(site, memento);
05. }
06.  
07. public void createPartControl(Composite parent) {
08.          // create the viewer
09.   restoreSelection();
10. }
11.  
12. private void restoreSelection() {
13.   if (memento != null ) {
14.    IMemento storedSelection = memento.getChild( "StoredSelection" );
15.    if (storedSelection != null ) {
16.     // create a structured selection from it
17.     viewer.setSelection(selection);
18.    }
19.   }
20. }
21.  
22. @Override
23. public void saveState(IMemento memento) {
24.   IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
25.   if (!selection.isEmpty()) {
26.    IMemento storedSelection = memento.createChild( "StoredSelection" );
27.    // store the selection under storedSelection
28.   }
29. }


Not just the selection, we can store any other information (which of the tree nodes are expanded, sorter & filter settings etc) which will be useful to restore the state.

Great. Now moving on from the views, the next item would be dialogs. Similar to the workbench windows, you can store the size, location and other elements of a dialog as well. The functionality for size and location is available by default, but you need to enable it by overriding the Dialog.getDialogBoundsSettings() method. The Dialog class stores & retrieves the size and location from IDialogSettings returned from that method. The original implementation returns null, so nothing is saved. We need to create an instanceof IDialogSettings and return it. Your plugin's Activator simplifies that. When required, it creates a dialog_settings.xml under your plugin's data area and store the dialog settings of all the dialogs. You have to create a separate section for each dialog.

01. private static final String MY_DIALOG_SETTINGS = "MY_DIALOG_SETTINGS" ;
02.  
03. @Override
04. protected IDialogSettings getDialogBoundsSettings() {
05.   IDialogSettings settings = Activator.getDefault().getDialogSettings();
06.   IDialogSettings section = settings.getSection(MY_DIALOG_SETTINGS);
07.   if (section == null ) {
08.    section = settings.addNewSection(MY_DIALOG_SETTINGS);
09.   }
10.   return section;
11. }


In case you want to store only the location or size, you can specify it by overriding the getDialogBoundsStrategy() method.

Much like the IMemento, the IDialogSettings basically organizes the key-value strings in an hierarchical way. So along with the size & location, you can store any other information in this IDialogSettings. Its a good practice to store the values of the widgets (which radion button is selecte, checked state of a check box, etc) in the dialog, so its faster for an user who frequently repeats an operation.

Talking about the widgets, the trickier one is Combo. When the list of options are predefined and the user can't enter a new value, then its easy. But in places where the user can enter the values, (like File/Directory selection, search boxes), remembering them is not straight forward.

We shouldn't be storing every single value the user enters, but only the recently used ones. Probably with a limit of 5 or 10 items only. This can be done with the help of LinkedHashSet. It guarantees the order of the element, so whenever the user enters a new values, put the current value first in the set then add the rest of the elements (even if the first element is repeated, it won't change the position). Then take the first N elements and store it.

01. private static final String COMBO_STATE= "COMBO_STATE" ;
02.  
03. private static final int HISTORY_SIZE = 5 ;
04.  
05. private String []comboState;
06.  
07. private void restoreComboState(IDialogSettings settings) {
08.   comboState = settings.getArray(COMBO_STATE);
09.   if (comboState == null )
10.    comboState = new String[ 0 ];
11.   for (String value : comboState) {
12.    myCombo.add(value);
13.   }
14. }
15.  
16. private void saveComboState(IDialogSettings settings) {
17.  
18.   // use LinkedHashSet to have the recently entered ones
19.   LinkedHashSet<String> newState = new LinkedHashSet<String>();
20.   newState.add(myCombo.getText());
21.   newState.addAll(Arrays.asList(comboState));
22.  
23.   // now convert it to the array of required size
24.   int size = Math.min(HISTORY_SIZE, newState.size());
25.   String[] newStateArray = new String[size];
26.   newState.toArray(newStateArray);
27.  
28.   // store
29.   settings.put(COMBO_STATE, newStateArray);
30. }


One last piece. Look at this dialog:



Sometimes only for the first time you want to ask the question to the user. Thereafter if the user prefers, you can use the same answer. To simplify this, you can use the MessageDialogWithToggle. You need to pass the preference store and the key for the preference value. When the user selects the check box, the value will be stored. From the next time onwards, you can check the value from the preference and use it.

1. MessageDialogWithToggle.openYesNoQuestion(shell, "Remember me" , "Is this tip useful?" ,
2.    "Don't bug me with this question again" , true ,
3.    Activator.getDefault().getPreferenceStore(), "BUG_USER_KEY" );


There you go:


:-)

 

 

 

 

 

 

 

 

Keyboard accessibility thru Command Framework

Keyboard shortcuts is usually much speedier than reaching out your mouse, moving it, pointing it to something and clicking. But there are some things which cannot be done that easily by keyboard shortcuts. For me, one of them is finding out a closed project and open it. Unfortunately, I've quite a large set of projects in my workspace and try to keep most them closed, when not used. In addition to that in the Package Explorer, I've the 'Closed Projects' filter on. So if I need to open a project, I've use the pull down menu, uncheck 'Closed Projects' navigate thru the working sets to find the right project and double click it. To enable keyboard access to this regular task, I decided to make use of Commands Framework .

The solution is to add a parameterized command , and in the values, I compute the projects which are closed. So when I press the awesome shortcut (Ctrl+3) it would display me the list of closed projects. With few keys, I can navigate to the project I want and open it. Lets see how to do it. First step is the command with the parameter:

01. < extension point = "org.eclipse.ui.commands" >
02.     < command
03.              defaultHandler = "com.eclipse_tips.handlers.OpenProjectHandler"
04.              id = "com.eclipse-tips.openProject.command"
05.              name = "Open Project" >
06.        < commandParameter
07.                 id = "com.eclipse-tips.openProject.projectNameParameter"
08.                 name = "Name"
09.                 optional = "false"
10.                 values = "com.eclipse_tips.handlers.ProjectNameParameterValues" >
11.        </ commandParameter >
12.     </ command >
13. </ extension >


And the handler:

01. public Object execute(ExecutionEvent event) throws ExecutionException {
02.   String projectName = event.getParameter( "com.eclipse-tips.openProject.projectNameParameter" );
03.   IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
04.   IProject project = root.getProject(projectName);
05.   try {
06.    project.open( null );
07.   } catch (CoreException e) {
08.    throw new ExecutionException( "Error occured while open project" , e);
09.   }
10.   return null ;
11. }


For the parameter values, I look for closed projects and return them:

01. public Map<String, String> getParameterValues() {
02.  
03.   IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
04.   IProject[] projects = root.getProjects();
05.   Map<String, String> paramValues = new HashMap<String, String>();
06.   for (IProject project : projects) {
07.    if (project.exists() && !project.isOpen()) {
08.     paramValues.put(project.getName(), project.getName());
09.    }
10.  
11.   }
12.   return paramValues;
13. }



So finally, When I press Ctrl+3 and type OPN, I get the list:

 


This idea can be extended to provide keyboard accessibility to many functionalities. Say in an RCP mail application, you can add a command like 'Go To Mail' with parameter as the Subject/Sender:


Hmmm, if only the 'Built On Eclipse' mail app that I *have* to use, knows the existence of threads other than the UI thread :-(

 

 

 

 

 

 

 

Subtask in ProgressMonitors

Here is a trivial tip. When inspecting a bug, I found a interesting thing on Progress Monitors. We know monitor.worked() increments the progress bar, but how do we change the text to update the current subtask? The initial text is set by the beginTask() method and it should be called only once. I digged into the IProgressMonitor and found the subtask() method:

01. IRunnableWithProgress operation = new IRunnableWithProgress() {
02.  
03.   public void run(IProgressMonitor monitor) {
04.  
05.    monitor.beginTask( "Main task running ..." , 5 );
06.    for ( int i = 0 ; i < 5 ; i++) {
07.     monitor.subTask( "Subtask # " + i + " running." );
08.     runSubTask( new SubProgressMonitor(monitor, 1 ), i);
09.    }
10.   }
11.  
12. };

 


Now the question is what happens when the runSubTask() method sets another subTask on the SubProgressMonitor?

01. private void runSubTask(IProgressMonitor monitor, int subTaskId) {
02.  
03.   monitor.beginTask( "Sub task running" , 10 );
04.    for ( int i = 0 ; i < 10 ; i++) {
05.     monitor.subTask( "Inside subtask, " + i + " out of 10" );
06.     // do something here ...
07.     monitor.worked( 1 );
08.     if (monitor.isCanceled())
09.      throw new OperationCanceledException();
10.   }
11.    monitor.done();
12.   }
13.  
14. }


Basically the SubProgressMonitor's subTask() overwrites the parent's subTask(). Thats the default behaviour. You can customize it with the style bits provided in the SubProgressMonitor:

If you want to append the SubProgressMonitor's subTask info, use the style PREPEND_MAIN_LABEL_TO_SUBTASK:

1. new SubProgressMonitor(monitor, 1 , SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK), i);

 


Else if you want to ignore it altogher then use the SUPPRESS_SUBTASK_LABEL style:

 

 

1. new SubProgressMonitor(monitor, 1 , SubProgressMonitor.SUPPRESS_SUBTASK_LABEL), i);



In a previous tip, we saw how to make a Properties View to respond only to a particular View or Editor. In this tip, we are going to see how to exclude a particular View or Editor from the Properties View. This will be very helpful in RCP apps, where they don't want the generic view like Outline view or an editor that contributes to the Properties View. Last time we extended the Properties View to create our own view and added some code. But this time its simple. You just have to use the org.eclipse.ui.propertiesView extension point.

<extension
         point="org.eclipse.ui.propertiesView">
      <excludeSources
            id="org.eclipse.ui.views.ContentOutline">
      </excludeSources>
      <excludeSources
            id="com.eclipse_tips.editors.MyOwnEditor">
      </excludeSources>
</extension>

Yes, its that simple :-) Don't try with Galileo, you would need Helios (3.6) M2 to get this working.
// rotary encoder inputs #define CLK 2 #define DT 3 #define SW 4 int temp_counter = 128; int currentStateCLK; int lastStateCLK; String currentDir =""; unsigned long lastButtonPress = 0; int counterstep = 40; void setup() { // Set encoder pins as inputs pinMode(CLK,INPUT); pinMode(DT,INPUT); pinMode(SW, INPUT_PULLUP); // Setup Serial Monitor Serial.begin(9600); // Read the initial state of CLK lastStateCLK = digitalRead(CLK); } void loop() { // Read the current state of CLK currentStateCLK = digitalRead(CLK); int counter = constrain(temp_counter, 1, 255); // If last and current state of CLK are different, then pulse occurred // React to only 1 state change to avoid double count if (currentStateCLK != lastStateCLK && currentStateCLK == 1){ // If the DT state is different than the CLK state then // the encoder is rotating CCW so decrement if (digitalRead(DT) != currentStateCLK) { temp_counter -= counterstep; currentDir ="CCW"; } else { // Encoder is rotating CW so increment temp_counter += counterstep; currentDir ="CW"; } int counter = constrain(temp_counter, 1, 255); Serial.print("Direction: "); Serial.print(currentDir); Serial.print(" | Counter: "); Serial.println(counter); Serial.println(temp_counter); } // Remember last CLK state lastStateCLK = currentStateCLK; // Read the button state int btnState = digitalRead(SW); //If we detect LOW signal, button is pressed if (btnState == LOW) { //if 50ms have passed since last LOW pulse, it means that the //button has been pressed, released and pressed again if (millis() - lastButtonPress > 50) { Serial.println("Button pressed!"); } // Remember last button press event lastButtonPress = millis(); } // Put in a slight delay to help debounce the reading delay(1); } 将以上程序的1-255输出变成16进制1-255输出
05-29
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值