在本文中,我們將學習一種通過編程定制 Eclipse 中的編輯器和視圖標簽的拖放行為的技術。我們使用了一個示例來展示這種技術,該示例將編輯器與轉移類型 org.eclipse.ui.part.EditorInputTransfer 進行關聯。可以通過實現對這種轉移類型的支持來支持編輯器的拖放行為。我們還為視圖標簽提供了一個類似的展示。本文假設您熟悉 SWT 的拖放技術。
定制意味著什麼?
Eclipse Workbench 中的編輯器和視圖標簽可以支持默認拖放行為,默認拖放行為支持:
視圖在 Workbench 內部的移動和停靠 在有標簽的記事本內部重新安排視圖或編輯器的順序 創建快速視圖 為了並排查看源代碼,在編輯器區域並排顯示幾個編輯器
盡管這是一個非常令人振奮的功能列表,但在我去年從事的一個項目中,仍然需要更多的功能。用戶需要能夠對編輯器和視圖標簽進行拖放,將它們放到一個特殊的視圖上。當用戶這樣做時,我們要做的是截獲所拖放編輯器的 editor-id 和 input,以及所拖放視圖的 view-id,然後在一個特殊的視圖中顯示相同的內容。下面的 圖 1 和 圖 2 將展示這種功能。
圖 1 給出了一個標題為 .project 的編輯器,該編輯器被拖放到一個標題為 Drop Window 的特殊視圖中。一旦拖放到如 圖 2 所示的位置處,這個特殊的視圖就會顯示所拖放編輯器的 editor-id 和 input。
圖 1. 正被拖放到特殊視圖中的編輯器標簽
圖 2. 被拖放到特殊視圖上的編輯器標簽
與我們在這個項目中的要求類似,還可能存在其他一些需要對編輯器或視圖標簽的默認拖放行為進行定制的情況。例如,有人可能會希望允許 Eclipse 用戶將編輯器標簽從工作台窗口中拖出,將它放到相同 Eclipse 實例的另外一個窗口中。本文中介紹的技術也可以在這種情況下使用。
下面讓我們來學習定制拖放行為所涉及的步驟,具體地說,這些步驟是針對編輯器標簽的,但它們同樣適用於視圖標簽。
定制編輯器標簽的拖放行為
當 Eclipse 用戶拖動一個編輯器標簽時,要實現如 圖 1 和 圖 2 所示的定制拖放功能,並內部執行以下兩個主要任務或操作:
操作 1 捕獲底層編輯器的 IEditorInput 和 editor-id,前者包含有關正在編輯的文件的信息;後者包含有關編輯正在使用的編輯器的類型信息。 操作 2 將 editor-input 和 editor-id 放到 EditorInputTransfer.EditorInputData 對象中,並將其設置為拖放過程中正在轉移的對象。這種操作可以讓 SWT 負責實現其余的拖放操作,例如將編輯器標簽放到使用 EditorInputTransfer 作為轉移類型的控件上。
由於編輯器標簽通常放在 CTabFolder 容器中,因此我們需要為存放編輯器的 CTabFolder 容器創建一個 DragSource,並在這個 DragSource 的 dragSetData() 方法中執行 操作 1 和 操作 2。假設我們可以捕獲存放編輯器的 CTabFolder 容器,那麼創建所需拖放源的任務就非常簡單,如下所示:
清單 1. 為 Tab folder 創建拖放源
CTabFolder tabFolder = <"CTabFolder" composite that hosts editor-parts>;
int operations = DND.DROP_COPY | DND.DROP_DEFAULT;
DragSource dragSource = new DragSource(tabFolder, operations);
Transfer[] transferTypes = new Transfer[] {EditorInputTransfer.getInstance()};
dragSource.setTransfer(transferTypes);
dragSource.addDragListener(new DragSourceListener()
{
public void dragStart(DragSourceEvent dsEvent) { }
public void dragSetData(DragSourceEvent dsEvent)
{
//code to perform operation-1 and operation-2
}
public void dragFinished(DragSourceEvent dsEvent) { }
});
此處另外一個非常重要的假設是:CTabFolder 容器並沒有已經創建好的拖放源。否則,代碼 DragSource dragSource = new DragSource(tabFolder, operations); 就會觸發一個 SWTError 錯誤,這是因為我們不能為同一個控件創建多個拖放源。
要查看這種假設是否有效(這也是 清單 1 的一個可能用途),讓我們來看一下與這個編輯器標簽有關的默認拖放行為。默認的行為提示說存放這個編輯器標簽的 CTabFolder 容器可能早已為其創建了一個拖放源。然而,快速查看 org.eclipse.swt.custom.CTabFolder.java 的代碼可以看到,CTabFolder 的默認拖放操作都不是通過創建一個拖放源來實現的,而是通過為 SWT.DragDetect、SWT.MouseMove 和 SWT.MouseUp 類型的事件添加監聽程序來實現的。CTabFolder 容器還沒有創建拖放源的假設仍然有效,正如我們可以看到的一樣,這是基於查看非 API 內部類獲得的信息而作出的假設。因此,如果這種假設在將來的版本中無效,也不用感到驚奇;不過我認為這種可能性非常小。
下面讓我們來看一下如何捕獲存放編輯器的 CTabFolder 容器。通過對編輯器平鋪行為的觀察,我們注意到不管在何時平鋪顯示編輯器,都會創建一個新的 Tab 文件夾。另外,當一個標簽組中的所有編輯器全部關閉或被移動到一個不同的 Tab 文件夾上時,原來的 Tab 文件夾就會被銷毀。這意味著 CTabFolder 容器的創建和銷毀都是動態的,因此 CTabFolder 容器的拖放源的創建也應該是動態完成的。
要實現這種功能,需要能夠對 CTabFolder 容器的創建進行控制。但是 Eclipse 並沒有提供任何可以在創建 CTabFolder 容器時進行回調的功能。另外一種方法是對 CTabFolders 進行特殊化 (specialize) 處理(繼承),繼承這些 specialized CTabFolders(繼承類,而不是基類 CTabFolder),並在這些 specialized CTabFolders 的 constructors 中創建拖放源。然而,在 Eclipse 中實例化這些 specialized CTabFolders 是一項非常繁雜的任務,因此我們需要尋找一種新的解決方案。
一種創建拖放源的新方法
下面讓我們為 Display 添加一個拖放檢測監聽器(用來監聽 SWT.DragDetect 類型的事件),如下所示:
清單 2. 為 Display 添加拖放監聽器
PlatformUI.getWorkbench().getDisplay().addFilter(SWT.DragDetect, new Listener()
{
public void handleEvent(Event event)
{
}
});
不論何時發生拖放操作時,都會調用這個監聽器的 handleEvent() 方法,其中 event.widget 指向產生這個事件的控件。在拖動編輯器標簽時,event.widget 指向存放這個拖放編輯器標簽的 CTabFolder 容器。這樣我們現在就可以為這個 CTabFolder 容器創建一個拖放源,如下所示:
清單 3. 捕獲存放編輯器的 Tab 文件夾
PlatformUI.getWorkbench().getDisplay().addFilter(SWT.DragDetect, new Listener()
{
public void handleEvent(Event event)
{
//ignore drag of widgets other than tab-folders (which host editor and view tabs)
if(!(event.widget instanceof CTabFolder))
return;
CTabFolder draggedFolder = (CTabFolder)event.widget;
int operations = DND.DROP_COPY | DND.DROP_DEFAULT;
final DragSource dragSource = new DragSource(draggedFolder, operations);
Transfer[] transferTypes = new Transfer[] {EditorInputTransfer.getInstance()};
dragSource.setTransfer(transferTypes);
dragSource.addDragListener(new DragSourceListener()
{
public void dragStart(DragSourceEvent dsEvent) { }
public void dragSetData(DragSourceEvent dsEvent)
{
//code to perform operation-1 and operation-2
}
public void dragFinished(DragSourceEvent dsEvent)
{
dragSource.dispose();
}
});
}
});
現在我們主要關心的是在開始拖放操作之後創建了一個新的拖放源。在開始拖放操作之後創建一個拖放源,這樣做是否能夠確保這個新的拖放源可以接收現在發生的拖放操作的通知?
為了尋找答案,首先讓我們來了解一下 Eclipse 的事件分發行為。
當控件上產生某種類型的事件時,首先將是那些為相同事件類型注冊的 Display 的所有過濾器 收到發生該事件的通知(使用 Display.addFilter() 方法添加的監聽器),接收順序是過濾器在 Display 上的注冊順序。然後發生該事件的通知會發送給控件中為該事件類型注冊的所有監聽器,發送順序是這些監聽器在控件上的注冊順序。
例如,假設 Listener 1 是在某個控件上為 t1 類型的事件注冊的第一個監聽器,而 Listener 2 是在這個控件上為相同的 t1 類型的事件注冊的第二個監聽器。另外假設 Filter 1 是在 Display 上為相同的 t1 類型的事件注冊的過濾器。現在,當這個控件上產生一個 t1 類型的事件時,第一個接收到發生該事件的通知的是 Filter 1,然後是 Listener 1,最後是 Listener 2。
在這種新方法中,我們給 Display 為 SWT.DragDetect 類型的事件添加了一個 filter。默認拖放行為與編輯器標簽有關,這意味著要對編輯器重新進行排列和平鋪,說明 CTabFolder 存在多個拖放檢測監聽器。因此,我們有一個 filter 和多個與 CTabFolder 有關的 default-listeners,它們都要監聽 SWT.DragDetect 事件。
當我們拖動一個編輯器標簽時,filter 會第一個接收到這種拖動操作。在 filter 的 handleEvent() 方法中,我們正在創建一個 drag source,並為這個 drag-source 添加了一個 DragSourceListener。然後,它會向父 CTabFolder 容器注冊了另外一個拖放檢測監聽器,我們稱之為 drag-listener-x。因此,當程序控制返回 filter 的 handleEvent() 方法時,就會有一組 default-listeners 和 drag-listener-x 在等待接收拖動事件的通知。在通知 default set of listeners 之後,新注冊的 drag-listener-x 也會接收到拖放事件的通知,這樣就可以實現我們的目的了。
下圖以圖形方式顯示了這些操作:
圖 3. 拖放編輯器標簽的序列圖
注意,在 dragFinished() 方法中(參見上面的 清單 3),我們對在 CTabFolder 容器上創建的 drag-source 進行了處理。實際上這是因為為每次拖放操作都創建了一個 drag-source,我們並沒有對原來的 drag-source 進行處理,這會使問題變得更加復雜。對 drag-source 的處理還從 CTabFolder 的事件監聽器表中刪除了 drag-listener-x(它是在創建 drag-source 時添加的),如上面的 圖 3 所示。
如何捕獲所拖放編輯器的 IEditorInput 和 Editor ID?
下面讓我們來看一下怎樣在 dragSetData() 方法中實現 操作 1 和 操作 2。
由於編輯器與包含它的 CTabFolder 之間的映射並沒有公開,因此可以依靠 IWorkbenchPage.getActivePart() 來獲得正在拖放的工作台部分,從中可以很容易地提取出所需的信息,如下面的代碼所示:
清單 4. 捕獲所拖放編輯器的 editor-input 和 editor-id
public void dragSetData(DragSourceEvent dsEvent)
{
IWorkbenchWindow workbenchWindow =
PlatformUI.getWorkbench().getActiveWorkbenchWindow();
IWorkbenchPart workbenchPartBeingDragged =
workbenchWindow.getActivePage().getActivePart();
if(workbenchPartBeingDragged instanceof IEditorPart)
{
String editorId = workbenchPartBeingDragged.getSite().getId();
IEditorInput editorInput =
((IEditorPart)workbenchPartBeingDragged).getEditorInput();
EditorInputTransfer.EditorInputData data =
EditorInputTransfer.createEditorInputData(editorId, editorInput);
dsEvent.data = new EditorInputTransfer.EditorInputData[] { data };
}
}
限制 tab 文件夾的默認拖放監聽器的行為
正如上面介紹的一樣,編輯器標簽有一種默認的拖放行為,它將進行重新排列和平鋪操作,這可以使用拖放檢測、鼠標移動 和鼠標釋放 類型的事件的監聽器實現。這些鼠標移動和鼠標釋放的事件監聽器的行為可能會與我們正在對編輯器實現的拖放行為沖突。例如,在將編輯器標簽拖放到 Drop Window 上之後,如 圖 2 所示,編輯器標簽的重新排列和平鋪會重新出現,這會導致在執行定制行為中產生意料不到的操作。(我們可以認為這是另外一次操作。)因此,限制默認監聽器的行為是非常有必要的。
我們的想法是取消或忽略所發生的其他拖放操作。Eclipse 用戶可以通過使用 Esc 鍵或右鍵點擊鼠標來輕松實現這種功能。通過編程可以很容易實現這種功能:使用 event.button 值而不是 1 來執行觸發鼠標釋放事件,如下所示:
清單 5. 取消其他拖放操作
public void dragFinished(DragSourceEvent dsEvent)
{
dragSource.dispose();
// inhibit the action of CTabFolder's default drag-drop-listeners
draggedFolder.notifyListeners(SWT.MouseUp, null);
}
對視圖標簽的拖放行為進行定制
由於視圖通常都存放在 CTabFolder 容器中,因此上面用來定制編輯器的拖放行為的方法也可以用來定制視圖的拖放行為。要像上面的 圖 1 和 圖 2 中所顯示的那樣對視圖的拖放行為進行定制,則需要執行以下操作:當用戶拖動一個視圖標簽時,捕獲底層視圖的 view-id,並將其設置為拖放過程中正在轉移的對象。下面黑色字體表示的代碼是應該在上面 清單 3 和 清單 4 的基礎上添加的代碼。
清單 6. 定制編輯器和視圖標簽的拖放行為
PlatformUI.getWorkbench().getDisplay().addFilter(SWT.DragDetect, new Listener()
{
public void handleEvent(Event event)
{
//ignore drag of widgets other than tab-folders (which host
//editor and view tabs)
if(!(event.widget instanceof CTabFolder))
return;
final CTabFolder draggedFolder = (CTabFolder)event.widget;
//Handle special case where no editors are open but editor area
//(and hence containing tab-folder) are still visible. Now try
//dragging the tab-folder. This drag should be ignored.
if( draggedFolder.getItemCount() < 1 )
return;
int operations = DND.DROP_COPY | DND.DROP_DEFAULT;
final DragSource dragSource = new DragSource(draggedFolder, operations);
//get a reference to the workbench-part that is being dragged
IWorkbenchWindow workbenchWindow =
PlatformUI.getWorkbench().getActiveWorkbenchWindow();
final IWorkbenchPart workbenchPartBeingDragged =
workbenchWindow.getActivePage().getActivePart();
Transfer[] transferTypes = null;
if(workbenchPartBeingDragged instanceof IEditorPart)
transferTypes = new Transfer[] {EditorInputTransfer.getInstance()};
else
transferTypes = new Transfer[] {TextTransfer.getInstance()};
dragSource.setTransfer(transferTypes);
dragSource.addDragListener(new DragSourceListener()
{
public void dragStart(DragSourceEvent dsEvent) { }
public void dragSetData(DragSourceEvent dsEvent)
{
if(workbenchPartBeingDragged instanceof IEditorPart)
{
String editorId = workbenchPartBeingDragged.getSite().getId();
IEditorInput editorInput =
((IEditorPart)workbenchPartBeingDragged).getEditorInput();
EditorInputTransfer.EditorInputData data =
EditorInputTransfer.createEditorInputData(editorId, editorInput);
dsEvent.data = new EditorInputTransfer.EditorInputData[] { data };
}
else if(workbenchPartBeingDragged instanceof IViewPart)
{
String viewId = workbenchPartBeingDragged.getSite().getId();
dsEvent.data = viewId;
}
}
public void dragFinished(DragSourceEvent dsEvent)
{
dragSource.dispose();
// inhibit the action of CTabFolder's default drag-detect-listeners
draggedFolder.notifyListeners(SWT.MouseUp, null);
}
});
}
});
運行這個示例
這個 DragDropWorkbenchParts 插件在 Window 菜單中增加了一個菜單項 Enable Drag-n-Drop of Editor/View Parts。它在工具條中添加了一個相應的觸發按鈕。當用戶選擇這個菜單項或觸發按鈕時,就會有一個如 清單 6 所示的過濾器 被添加到 Display 中,從而啟用編輯器和視圖標簽的拖放操作。當沒有選擇這個操作時,就會從 Display 中刪除這個過濾器,恢復編輯器和視圖標簽的默認拖放行為(這意味著又可以進行重新排列和平鋪操作了)。
該插件還定義了一個標題為 Drop Window 的視圖,它有一個支持 EditorInputTransfer 和 TextTransfer 的拖放目標,允許將編輯器和視圖標簽拖放到此窗口中。