章 16. NSLookupService

系統服務 (System service) 是 GNUstep 中很重要的功能. 透過系統服務, 資料可以在程式間傳遞並修改. 有些程式提供別的程式特定的服務, 例如檢查錯字, 改變圖檔的亮度等. 其他程式可以在系統服務選單 (Service Menu) 中看到這些服務. 當某程式選取其字串或圖形後, 可以透過服務選單當資料送出給服務提供者. 服務提供者可自行顯示處理過的資料, 或是將處理好的資料送回原程式. 這樣的好處是每個程式可以專注在某個領域, 再利用系統服務來延伸其功能. GNUstep 提供幾個簡單的服務, 例如改變字串的大小寫. 可以使用 Ink 輸入字串, 選擇字串, 選擇 "Services->To upper", 字串即成為大寫的. 這項功能其實是 exampleService 提供的, 而非 Ink. Ink 只是將資料傳給 exampleService 處理後再傳回.

首先介紹使用系統服務的方法. 在這裡製做一個查尋網址 IP 的程式, 算是圖形版的 nslookup.

使用 Gorm 建立如下的介面:

圖形 16.1. NSLookupService 使用者介面

NSLookupService 使用者介面

繼承自 NSObject 產生 Controller 類別, 加入兩個 outlets: "hostname" 及 "address". 連結 outlets 至 NSTextField.

圖形 16.2. 連結 outlet

連結 outlet

增加一個 action, 稱為 action:, 並指定 NSTextField 的 action 為 "action:"

圖形 16.3. 連結 action.

連結 action.

設定 Controller 為 NSApp 的代理者. 接下來提供服務時會用到.

圖形 16.4. 設定 NSApp 代理者

設定 NSApp 代理者

產生 Controller 程式碼並修改如下:

Controller.m

- (void) action: (id)sender
{
  [address setStringValue: [[NSHost hostWithName: [hostname stringValue]] address]];
}

當使用者輸入網址後, 按下 ENTER 鍵, 便使用 NSHost 計算並顯示其 IP.

要使用系統服務的方法就是加入系統選單. 先產生一個選單, 再指定該選單為系統選單.

Controller.m

- (void) awakeFromNib
{
  NSMenu *mainMenu, *serviceMenu;

  mainMenu = [NSApp mainMenu];

  [mainMenu insertItemWithTitle: @"Services"
                         action: NULL
                  keyEquivalent: @""
                        atIndex: 0];

  serviceMenu = [[NSMenu alloc] initWithTitle: @"Services"];
  [mainMenu setSubmenu: serviceMenu
               forItem: [mainMenu itemWithTitle: @"Services"]];
  [NSApp setServicesMenu: serviceMenu];
  RELEASE(serviceMenu);
}

利用 -awakeFromNib, 當使用者介面被載入之後, 增加一個服務選單. 最重要的是指定該選單為系統選單. 現在在 NSTextField 中輸入字串, 便可以使用服務選單改變其大小寫了.

如果要成為服務提供者, 只要完成兩項事. 一是寫好服務介面, 二是實做服務內容. 服務介面在程式是屬性檔 (Property List) 中, 在本範例中, 程式名稱為 NSLookupService, 其屬性檔即為 NSLookupServiceInfo.plist.

NSLookupServiceInfo.plist

{
  NSServices = (
    {
      NSPortName = NSLookupService;
      NSMessage = getAddress;
      NSSendTypes = (NSStringPboardType);
      NSMenuItem = {
        default = "Get Address";
        English = "Get Address";
      };
      NSKeyEquivalent = {
        default = "G";
      };
    }
  );
}

NSPortName 為程式名稱. NSMessage 為提供服務的 method 名稱, 接來下會實作. NSSendTypes 是將資料傳入所使用的剪貼簿的型態, 換言之, 這個服務只接受字串 (NSStringPboardType). NSMenuItem 及 NSKeyEquivalent 為出現在服務選單上的字串及快速鍵. 因為這個服務只接受字串, 因此這個選單只有在程式中有字串被選取時才會出現.

NSMessage 指定 getAddress: 為提供服務的 method. 其介面是固定的:

- (void) getAddress: (NSPasteboard *) pboard
           userData: (NSString *) userData
              error: (NSString **) error

getAddress: 即為 NSMessage 所指定的, pboard 是存放傳入資料的剪貼簿. userData 是一特定字串. 如果在屬性檔中有指定 NSUserData, 則會在這裡出現. 一般是攜帶一些額外資料. error 是當服務出錯時傳回的字串.

現在在 Controller.m 中實作 getAddress: 這個 method.

Controller.m

- (void) getAddress: (NSPasteboard *) pboard
           userData: (NSString *) userData
              error: (NSString **) error
{
  NSArray *allTypes;
  NSString *name;

檢查剪貼簿是否支援 NSString 類型. 若無, 傳回錯誤.

  allTypes = [pboard types];

  if ( ![allTypes containsObject: NSStringPboardType] )
    {
      *error = @"No string type supplied by pasteboard";
      return;
    }

從剪貼簿中取出字串. 若無, 傳回錯誤.

  name = [pboard stringForType: NSStringPboardType]; 

  if (name == nil)
    {
      *error = @"No string value supplied by pasteboard";
      return;
    }

將字串放到 hostname, 檢查是否為合法的網址. 若否, 傳回錯誤. 若是, 使用現成的 -action: 顯示其 IP

  [hostname setStringValue: name];
  if ([[NSHost hostWithName: name] address] == nil)
    {
      *error = @"Host name is not valid";
      return;
    }
  [self action: self];
}

最後, 要讓 NSApp 知道那一個物件提供 getAddress: 這個服務.

- (void) applicationDidFinishLaunching: (NSNotification *) not
{
  [NSApp setServicesProvider: self];
}

程式必需安裝, 服務才會先效. 安裝完後必需執行 make_services 程式, 系統服務才會更新. make_services 是 GNUstep 所提供的, 會在 GNUstep.sh 或 GNUstep.csh 中被呼叫. 因此每次登入時都會更新一次系統服務. 如果不想重新登入, 那就要使用 make_services 來使服務生效.

執行 make_services 後, 在 Ink 中輸入並選擇一些字串. 這時服務選單應該會出現 NSLookupService 所提供的服務. 選該務服會啟動 NSLookupService, 並查出其 IP, 或是傳回錯誤訊息.

如果想將服務提供者處理過的資料傳回, 首先要在屬性檔中指定要傳回的資料型態:

NSLookupServiceInfo.plist

{
  NSServices = (
    {
      NSPortName = NSLookupService;
      NSMessage = getAddress;
      NSSendTypes = (NSStringPboardType);
      NSReturnTypes = (NSStringPboardType);
      NSMenuItem = {
        default = "Get Address";
        English = "Get Address";
      };
      NSKeyEquivalent = {
        default = "G";
      };
    }
  );
}

在這裡, 傳回值正好也是字串. 接下來只要在 -getAddress: 中, 將傳回值放回剪貼簿中即可:

Controller.m

- (void) getAddress: (NSPasteboard *) pboard
           userData: (NSString *) userData
              error: (NSString **) error
{
  NSArray *allTypes;
  NSString *name;

  allTypes = [pboard types];

  if ( ![allTypes containsObject: NSStringPboardType] )
    {
      *error = @"No string type supplied by pasteboard";
      return;
    }

  name = [pboard stringForType: NSStringPboardType];

  if (name == nil)
    {
      *error = @"No string value supplied by pasteboard";
      return;
    }

  [hostname setStringValue: name];
  if ([[NSHost hostWithName: name] address] == nil)
    {
      *error = @"Host name is not valid";
      return;
    }
  [self action: self];

  /* For return value */
  allTypes = [NSArray arrayWithObject: NSStringPboardType];
  [pboard declareTypes: allTypes owner: nil];
  [pboard setString: [NSString stringWithFormat: @"%@ (%@)", name, [address stringValue]]
            forType: NSStringPboardType];

}

在這裡, 傳回值是原網址加上其 IP. 現在可以再用 Ink 來測試其效果.

程式碼在此: NSLookupService-src.tar.gz