为JFace(TableViewer,TreeViewer...)创建可直接编辑的DialogCellEditor

时间:2023-01-18 12:42:24

最终的解决方式采用了额外的线程,鉴于笔者对于多线程的功底不深所以很有可能引起其他问题.待完善.但本博客肯定会帮助你深刻了解问题. 如果你有优雅的多线程处理,别犹豫联系我.

背景:

1.JFace的TableViewer,TreeViewer普遍应用,TextCellEditor,ComboCellEditor,DialogCellEditor等是JFace自带的单元格编辑器. 这些编辑器确实可以满足很多需求,而且使用方便,但随着对于UI交互要求的提高,用户追求更加简单的操所时.这些Editor的编辑操作确实还有一些优化的空间.    (毕竟是背景, 说的官方高大上些)

2.这里主要优化的是DialogCellEditor  原因是: 用户想要编辑需要点击两次.  一次点击单元格,一次点击右侧button.  于是有的用户不爽了.(你知道一百个用户第一天得多点击多少次吗 布拉布拉)  用户希望可以直接编辑.  即:  用户点击第一次时,就可以直接在单元格里输入编辑类似TextCellEditor. 如果输入比较复杂. 则再点击Button.在Dialog中编辑.  Dialog中可以干很多事.  输入辅助,输入类型提示等.

现状:

这种显而易见的优化,很多人都会想到.我也检索了一些.效果虽然很接近,但是实际上关键问题并没有解决. 迫于公司任务的压力,我也无奈直面了它.花了几天时间,终于实现了效果.虽然不够优雅.

我检索到已有的Demo没有同时实现以下几点:

1.鼠标单击时,有没有把已有的值带到Text的编辑区域里

2.text中的值快捷直接应用.Text的编辑区域 编辑修改后,点击其他空行.(注意:不是其他的有数据的Cell), 新的数值是否保存

3.text中的数据和Dialog时时同步 即:  Text的编辑区域 编辑修改后,再单击button,新的未保存的值是否到了Dialog中去,再次修改后,点击OK后,最新的值是否可以保存到Table中.

从实现角度讲,无非是综合TextCellEditor和DialogCellEditor. 如果二者的源代码看懂的差不多.融合应该不难. 最开始我也是这样想的. 但是后来发现jface底层并不那么友善.


为JFace(TableViewer,TreeViewer...)创建可直接编辑的DialogCellEditor

定制CellEditor,融合TextCellEditor和DialogCellEditor

实验一:

当我简单的在DialogCellEditor.createContents()中添加了Text控件后,并且在doSetFocus()中让text获取焦点,并为它赋值后,发现需求的第1,3条就满足了.代码如下:

@Override
	protected Control createContents(final Composite cell) {
		this.text = new Text(cell, SWT.NONE);
		this.text.addKeyListener(new KeyListener() {

			public void keyPressed(KeyEvent e) {
				// TODO Auto-generated method stub
				if (e.keyCode == 13) {
					doSetValue(text.getText());
				}
			}

			public void keyReleased(KeyEvent e) {
				// TODO Auto-generated method stub

			}

		});
		return this.text;
	}

	@Override
	protected void doSetFocus() {
		if (text != null) {
			text.setText(getValue().toString());
			text.selectAll();
			text.setFocus();
		}
	}

这段代码基本来自TextCellEditor.但由于2号需求不满足会引起很大的问题.

1.保存数值,只能通过打开Dialog点击OK,或者点击其他有数据Cell才能保存

2.如果这个表格只有一行数据.很难保存数据了.

显然无法使用.

我知道你想说什么.  模仿TextCellEditor单击其他空白Cell保存值得逻辑呀.and I did.

实验二:

应用TextCellEditor逻辑.

 text.addFocusListener(new FocusAdapter() {
            @Override
			public void focusLost(FocusEvent e) {
                TextCellEditor.this.focusLost();
            }
        });

实现很简单.增加失去焦点的相应.当点击其他空白区域时触发.focusLost(). focusLost()逻辑.

protected void focusLost() {
		if (isActivated()) {
			<em><strong>fireApplyEditorValue();</strong></em>
			deactivate();
		}
	}
我着重的那行,很清楚. fire监听器应用当前编辑好的值.(TableViewer的是ColumnViewerEditor的匿名内部类.PS:很多Viewer统一实现ICellEditorListener才实现了CellEditor的通用)

以为万事大吉了,想的美,测试的时候,你会发现.2条满足了. 但是第3条却不能满足.先show代码:

this.text.addFocusListener(new FocusListener() {

			public void focusGained(FocusEvent e) {
				// TODO Auto-generated method stub

			}

			public void focusLost(FocusEvent e) {
				doSetValue(text.getText());
				CustomDirectEditDialogCellEditor.this.focusLost();
			}

		});
虽然3条不满足,即:如果用户先修改了Text的值,当点击Button后Text的当前值就会作为最终值,所以Dialog中怎么修改也是无用的.

这就是所谓的鱼与熊掌不可兼得. 我本想着修改一两行代码就可以二者兼得了,下一个任务了.但浪费了很多时间后,我意识到这个问题并不是那么容易解决.最后我就踏实的跟源码.究其原因.

关键问题说明:CellEditor只允许赋值一次 即: fireApplyEditorValue();只能被调用一次.

原因:源码写着呢.

/**
	 * Applies the current value and deactivates the currently active cell
	 * editor.
	 */
	void applyEditorValue() {
		<span style="color:#FF0000;">// avoid re-entering</span>  <span style="color:#FF0000;"> 为啥 给个原因呀</span>
		if (!inEditorDeactivation) {
			try {
				inEditorDeactivation = true;
				CellEditor c = this.cellEditor;
				if (c != null && this.cell != null) {
					ColumnViewerEditorDeactivationEvent tmp = new ColumnViewerEditorDeactivationEvent(
							cell);
					tmp.eventType = ColumnViewerEditorDeactivationEvent.EDITOR_SAVED;
					if (editorActivationListener != null
							&& !editorActivationListener.isEmpty()) {
						Object[] ls = editorActivationListener.getListeners();
						for (int i = 0; i < ls.length; i++) {

							((ColumnViewerEditorActivationListener) ls[i])
									.beforeEditorDeactivated(tmp);
						}
					}

					Item t = (Item) this.cell.getItem();

					// don't null out table item -- same item is still selected
					if (t != null && !t.isDisposed() && c.isValueValid()) {
						<span style="background-color: rgb(102, 51, 255);">saveEditorValue(c);</span>
					}
					<span style="color:#FF0000;">if (!viewer.getControl().isDisposed()) {
						setEditor(null, null, 0);
					}</span>

					<span style="color:#FF0000;">c.removeListener(cellEditorListener);  // 
					Control control = c.getControl();
					if (control != null && !control.isDisposed()) {
						if (mouseListener != null) {
							control.removeMouseListener(mouseListener);
							// Clear the instance not needed any more
							mouseListener = null;
						}
						if (focusListener != null) {
							control.removeFocusListener(focusListener);
						}

						if (tabeditingListener != null) {
							control.removeTraverseListener(tabeditingListener);
						}
					}</span>
					c.deactivate(tmp);

					if (editorActivationListener != null
							&& !editorActivationListener.isEmpty()) {
						Object[] ls = editorActivationListener.getListeners();
						for (int i = 0; i < ls.length; i++) {
							((ColumnViewerEditorActivationListener) ls[i])
									.afterEditorDeactivated(tmp);
						}
					}
					
					if( ! this.cell.getItem().isDisposed() ) {
						this.cell.getItem().removeDisposeListener(disposeListener);
					}
				}

				<span style="color:#FF0000;">this.cellEditor = null;
				this.cell = null;</span>
			} finally {
				inEditorDeactivation = false;
			}
		}
	}

源码我没有详细的读,但是蓝色是赋值重点逻辑.  然后就挥泪斩马谡.红色的代码把底层支持赋值的数据全部移除置空.  我曾试过备份这些数据.再第二次赋值时还原.但太难了 太深入底层了.而且太不符合逻辑.放弃了.

解释上面的两个实验的现象:

实验一:  

text和dialog值时时同步. 原因二者都是通过setValue()和getValue()获取和设置值的.

保存值:1.在Dialog点击OK时,应用fireApplyEditorValue();

  button.addSelectionListener(new SelectionAdapter() {

			@Override
			public void widgetSelected(SelectionEvent event) {
            	// Remove the button's focus listener since it's guaranteed
            	// to lose focus when the dialog opens
            	button.removeFocusListener(getButtonFocusListener());
                
            	Object newValue =<strong> openDialogBox(editor);</strong> 打开dialog的抽象方法
            	
            	// Re-add the listener once the dialog closes
            	button.addFocusListener(getButtonFocusListener());

            	if (newValue != null) {
                    boolean newValidState = isCorrect(newValue);
                    if (newValidState) {
                        markDirty();
                        doSetValue(newValue);
                    } else {
                        // try to insert the current value into the error message.
                        setErrorMessage(MessageFormat.format(getErrorMessage(),
                                new Object[] { newValue.toString() }));
                    }
                  <strong>  fireApplyEditorValue();</strong>
                }
            }
2.选择其他可编辑的Cell保存时.相应MouseDown的事件 会触发以下逻辑

private void handleMouseDown(MouseEvent e) {
		ViewerCell cell = getCell(new Point(e.x, e.y));

		if (cell != null) {
			triggerEditorActivationEvent(new ColumnViewerEditorActivationEvent(
					cell, e));
		}
	}
如果点击位置是个正常的Cell  则:getCell不为空.     反之,如果是空白区域则cell为空.  

而这决定triggerEditorActivationEvent()是否执行.而这里面有最后调用之前编辑的cell调用fireApplyEditorValue()的逻辑.

void handleEditorActivationEvent(ColumnViewerEditorActivationEvent event) {

		// Only activate if the event isn't tagged as canceled
		if (!event.cancel
				&& editorActivationStrategy.isEditorActivationEvent(event)) { <strong>//保护性检测
		if (cellEditor != null) {    //如果上一个CellEditor还存在(cellEditor=null,实在ApplyApplyEditorValue()中执行的,我
			applyEditorValue(); //展示过标记为红色. 所以如果cellEditor不为空则意味着上一个cell的值未应用,所以触发应用
		}</strong>

			this.cell = (ViewerCell) event.getSource(); //激活新的Cell的相关编辑

			// Only null if we are not in a deactivation process see bug 260892
			if( ! activateCellEditor(event) && ! inEditorDeactivation ) {
				this.cell = null;
				this.cellEditor = null;
			}
		}
	}

这也就是点击空白cell的不能保存,可编辑cell能保存的原因.

实验二:

点击空白可以保存.

原因:由于text先失去焦点,触发了fireApplyEditorValue()的逻辑新的值生效. 所以不需要通过上面保护性检测来触发保存

dialog中的新值赋值无效

原因: 点击button打开dialog时,text已经失去焦点之后的事了.. 所以dialog关闭后,想再次应用fireApplyEditorValue() 已经失去了条件.即: 第二次调用无效 所以注定值不能保存.

问题解决:

综合一下时在实验二的基础上解决容易些.  而且避免修改jface的深层逻辑.

解决思想:

所以我从避免两次调用fireApplyEditorValue()触发

如果text失去焦点后,但没有打开dialog就出发fireApplyEditorValue(),

如果text失去焦点后,但打开了dialog就不触发fireApplyEditorValue();

但text如何知道未来发生的事进行不同的处理呢.   让它等待一小会再检测

将focusLost()修改为:

@Override
			public void focusLost(FocusEvent e) {
				UIJob job = new UIJob("输入域保存值异常") {
					@Override
					public IStatus runInUIThread(IProgressMonitor monitor) {
						if (dialog == null) {
							NormalCellEditor.this.focusLost(); //这是我自定义的类名
						}
						return Status.OK_STATUS;
					}
				};
				job.schedule(200);  <strong>//延迟200ms 执行run()</strong>
			};

虽然这样会使点击空白区域保存慢一点,但还是可以接受的.     还有些小细节 :add和remove这个focusListener 我就不贴代码了.

思路分析清楚了,不同的环境可能用不同的应用线程的方式,这个很明显不是最好的解决方式,如果有更好的解决方法,希望及时和我探讨 一起提升.

最后吐槽下,jface为啥不允许赋值两次,为啥,为啥.