Calc(39)コンテクストメニューをカスタマイズする: その7

2017-11-05

旧ブログ

t f B! P L
デフォルトのコンテクストメニューについて、ActionTriggerのプロパティとディスパッチコマンドのラベルを取得します。

前の関連記事:Calc(38)ディスパッチコマンドのラベル一覧の取得


デフォルトのコンテクストメニューはすべてディスパッチコマンド


notifyContextMenuExecute()メソッドの引数で渡されるContextMenuExecuteEvent StructのActionTriggerContainerアトリビュートに入っているActionTriggerContainerにはデフォルトのコンテクストメニューのActionTriggerが入っています。

ところがこのActionTriggerのプロパティはTextプロパティもHelpURLプロパティもImageもプロパティ何も入っていません(正確にはTextとHelpURLは空文字、ImageはNoneが入っています)。

SubContainerプロパティを除けば、値が入っているのはCommandURLプロパティだけです。

デフォルトのコンテクストメニューの項目のCommandURLプロパティにはすべてディスパッチコマンドが入っていました。

ということでデフォルトのコンテクストメニューの項目をカスタマイズするにはCommandURLプロパティのディスパッチコマンドで判断するしかなさそうです。

デフォルトコンテクストメニューのディスパッチコマンドをラベルとともに取得する


ListContextMenuEntries.ods

このCalcドキュメントをマクロを有効にして開くとコンテクストメニューにMenuEntriesという項目が出現します。

Windows10でも動作確認できました。


MenuEntriesを実行するとそのコンテクストメニューにあるActionTriggerのプロパティをすべて出力します。

A1セルにはActionTriggerContainerアトリビュートのgetName()の戻り値を出力しています(Calc(37)コンテクストメニューをカスタマイズする: その6参照)。

A3セル以降に各メニューエントリのText, CommandURL, HelpURL, Image, SubContainerの各プロパティに値を出力しています。

これらに加えて最後にディスパッチコマンドのラベルも出力しています。

ディスパッチコマンドのラベルはActionTriggerのプロパティには含まれていないので、ConfigurationAccessで取得しています(Calc(38)ディスパッチコマンドのラベル一覧の取得参照)。

Calcのコンテクストメニューは次の4種類になります。

セルのコンテクストメニュー

https://docs.google.com/spreadsheets/d/e/2PACX-1vR-Pc0Cu7kgnK3sSBdGwJrTsyYZsvhFqH4zDRrr_VzssGamUrMCEOncivrsa8HPpv8_gL9573ESMFC4/pubhtml?gid=0&single=true

行ヘッダーのコンテクストメニュー

https://docs.google.com/spreadsheets/d/e/2PACX-1vR-Pc0Cu7kgnK3sSBdGwJrTsyYZsvhFqH4zDRrr_VzssGamUrMCEOncivrsa8HPpv8_gL9573ESMFC4/pubhtml?gid=1911919839&single=true

列ヘッダーのコンテクストメニュー

https://docs.google.com/spreadsheets/d/e/2PACX-1vR-Pc0Cu7kgnK3sSBdGwJrTsyYZsvhFqH4zDRrr_VzssGamUrMCEOncivrsa8HPpv8_gL9573ESMFC4/pubhtml?gid=1289547728&single=true

シートタブのコンテクストメニュー

https://docs.google.com/spreadsheets/d/e/2PACX-1vR-Pc0Cu7kgnK3sSBdGwJrTsyYZsvhFqH4zDRrr_VzssGamUrMCEOncivrsa8HPpv8_gL9573ESMFC4/pubhtml?gid=506841944&single=true

コンテクストメニューのディスパッチコマンドを一覧にするマクロ


上のCalcドキュメントに入っているマクロです。

ドキュメントを起動したときにmacro()を実行するようにしています。
#!/opt/libreoffice5.4/program/python
# -*- coding: utf-8 -*-
import unohelper  # オートメーションには必須(必須なのはuno)。
import os
from com.sun.star.uno import RuntimeException
from com.sun.star.beans import PropertyValue  # Struct
from com.sun.star.ui import XContextMenuInterceptor
from com.sun.star.ui import ActionTriggerSeparatorType  # 定数
from com.sun.star.ui.ContextMenuInterceptorAction import EXECUTE_MODIFIED  # enum
from com.sun.star.sheet import CellFlags as cf # 定数
def macro(documentevent=None):  
 doc = XSCRIPTCONTEXT.getDocument() if documentevent is None else documentevent.Source  # ドキュメントのモデルを取得。 
 controller = doc.getCurrentController()  # コントローラーを取得。
 contextmenuinterceptor = ContextMenuInterceptor(doc)
 controller.registerContextMenuInterceptor(contextmenuinterceptor)
class ContextMenuInterceptor(unohelper.Base, XContextMenuInterceptor):
 def __init__(self, doc):  
  ctx = XSCRIPTCONTEXT.getComponentContext()  # コンポーネントコンテクストの取得。
  smgr = ctx.getServiceManager()  # サービスマネージャーの取得。
  self.args = getBaseURL(ctx, smgr, doc), createDispatchCommandLabelReader(ctx, smgr)
 def notifyContextMenuExecute(self, contextmenuexecuteevent):  # 右クリックで呼ばれる関数。
  baseurl, getDiapatchCommandLabel = self.args
  contextmenu = contextmenuexecuteevent.ActionTriggerContainer  # コンテクストメニューコンテナの取得。
  controller = contextmenuexecuteevent.Selection  # ドキュメントのコントローラの取得。
  global enumerateMenuEntries  # ScriptingURLで呼び出す関数。オートメーションやAPSOでは不可。
  enumerateMenuEntries = createEnumerator(controller, contextmenu, getDiapatchCommandLabel)  # クロージャーでScriptingURLで呼び出す関数に変数を渡す。
  addMenuentry(contextmenu, "ActionTrigger", 0, {"Text": "MenuEntries", "CommandURL": baseurl.format(enumerateMenuEntries.__name__)})  # CommandURLで渡す関数にデコレーターは不可。
  addMenuentry(contextmenu, "ActionTriggerSeparator", 1, {"SeparatorType": ActionTriggerSeparatorType.LINE})  # 区切り線の挿入。
  return EXECUTE_MODIFIED # EXECUTE_MODIFIED, IGNORED, CANCELLED, CONTINUE_MODIFIED 
def createEnumerator(controller, contextmenu, getDiapatchCommandLabel):
 props = "Text", "CommandURL", "HelpURL", "Image", "SubContainer"  # ActionTriggerのプロパティ。
 separatortypes = {0:"LINE", 1:"SPACE", 2:"LINEBREAK"}  # 定数ActionTriggerSeparatorTypeを文字列に変換。  
 def enumerateMenuEntries():  # ScriptingURLで渡すので引数は受け取れない。
  sheet = controller.getActiveSheet()  # アクティブなシートを取得。
  def _enumarateEntries(container, k, c):  # 第2引数は出力先の開始行。第3引数は出力先の開始列。
   r = k - 1
   for menuentry in container:
    r += 1
    if menuentry.supportsService("com.sun.star.ui.ActionTrigger"):
     text, commandurl, helpurl, image, subcontainer = [menuentry.getPropertyValue(prop) for prop in props]  # getPropertyValues()は実装されていない。
     propvalues = [text, commandurl, helpurl]
     propvalues.append("icon" if image else str(image)) 
     propvalues.append("submenu" if subcontainer else str(subcontainer)) 
     if commandurl.startswith(".uno:"):
      label = getDiapatchCommandLabel(commandurl)
      if label:
       propvalues.append(label)
     sheet[r, c].setString(", ".join(propvalues))
     if subcontainer:
      r = _enumarateEntries(subcontainer, r, c+1)  # 再帰呼出し。
    elif menuentry.supportsService("com.sun.star.ui.ActionTriggerSeparator"):
     separatortype = menuentry.getPropertyValue("SeparatorType")
     sheet[r, c].setString(separatortypes[separatortype]) 
   return r  
  sheet.clearContents(cf.VALUE+cf.DATETIME+cf.STRING+cf.ANNOTATION+cf.FORMULA+cf.HARDATTR+cf.STYLES)  # セルの内容を削除。cf.HARDATTR+cf.STYLESでセル結合も解除。  
  datarows = (contextmenu.getName(),),\
    (", ".join(props),)
  sheet[:len(datarows), :len(datarows[0])].setDataArray(datarows)
  _enumarateEntries(contextmenu[2:], 2, 0)  # このマクロで追加した項目と線は出力しない。つまり項目インデックス2から出力。第2引数は出力先の開始行。第3引数は出力先の開始列。
  cellcursor = sheet.createCursor()  # シート全体のセルカーサーを取得。
  cellcursor.gotoEndOfUsedArea(True)  # 使用範囲の右下のセルまでにセルカーサーのセル範囲を変更する。
  cellcursor.getColumns().setPropertyValue("OptimalWidth", True)  # セルカーサーのセル範囲の列幅を最適化する。
 return enumerateMenuEntries
def createDispatchCommandLabelReader(ctx, smgr): 
 rootpaths = "/org.openoffice.Office.UI.CalcCommands/UserInterface/Commands/{}", \
    "/org.openoffice.Office.UI.CalcCommands/UserInterface/Popups/{}", \
    "/org.openoffice.Office.UI.GenericCommands/UserInterface/Commands/{}"  # ルートパスのCalc用のタプル。
 props = "PopupLabel", "Label"  # , "ContextLabel"  # 取得するプロパティのタプル。存在すれば使用したいラベル順に並べる。PopupLabelはコンテクストメニュー用、ContextLabelはツールバー用。
 configreader = createConfigReader(ctx, smgr)  # 読み込み専用の関数を取得。
 def getDiapatchCommandLabel(dispatchcommand):
  if dispatchcommand.startswith(".uno:"):
   for rootpath in rootpaths:
    rootpath = rootpath.format(dispatchcommand)
    try:
     root = configreader(rootpath)
     propvalues = root.getPropertyValues(props)  # 設定されていないプロパティはNoneが入る
     for label in propvalues:
      if label is not None:
       return label
    except RuntimeException:
     continue
 return getDiapatchCommandLabel
def createConfigReader(ctx, smgr):  # ConfigurationProviderサービスのインスタンスを受け取る高階関数。
 configurationprovider = smgr.createInstanceWithContext("com.sun.star.configuration.ConfigurationProvider", ctx)  # ConfigurationProviderの取得。
 def configReader(path):  # ConfigurationAccessサービスのインスタンスを返す関数。
  node = PropertyValue(Name="nodepath", Value=path)
  return configurationprovider.createInstanceWithArguments("com.sun.star.configuration.ConfigurationAccess", (node,))
 return configReader
def getBaseURL(ctx, smgr, doc):  # 埋め込みマクロ、オートメーション、マクロセレクターに対応してScriptingURLのbaseurlを返す。
 modulepath = __file__  # ScriptingURLにするマクロがあるモジュールのパスを取得。ファイルのパスで場合分け。
 ucp = "vnd.sun.star.tdoc:"  # 埋め込みマクロのucp。
 if modulepath.startswith(ucp):  # 埋め込みマクロの時。__file__はvnd.sun.star.tdoc:/4/Scripts/python/filename.pyというように返ってくる。
  filepath = modulepath.replace(ucp, "")  #  ucpを除去。
  transientdocumentsdocumentcontentfactory = smgr.createInstanceWithContext("com.sun.star.frame.TransientDocumentsDocumentContentFactory", ctx)
  transientdocumentsdocumentcontent = transientdocumentsdocumentcontentfactory.createDocumentContent(doc)
  contentidentifierstring = transientdocumentsdocumentcontent.getIdentifier().getContentIdentifier()  # __file__の数値部分に該当。
  macrofolder = "{}/Scripts/python".format(contentidentifierstring.replace(ucp, ""))  #埋め込みマクロフォルダへのパス。 
  location = "document"  # マクロの場所。 
 else:
  filepath = unohelper.fileUrlToSystemPath(modulepath) if modulepath.startswith("file://") else modulepath # オートメーションの時__file__はシステムパスだが、マクロセレクターから実行するとfileurlが返ってくる。
  pathsubstservice = smgr.createInstanceWithContext("com.sun.star.comp.framework.PathSubstitution", ctx)
  fileurl = pathsubstservice.substituteVariables("$(user)/Scripts/python", True)  # $(user)を変換する。fileurlが返ってくる。
  macrofolder =  unohelper.fileUrlToSystemPath(fileurl)  # fileurlをシステムパスに変換する。マイマクロフォルダへのパス。 
  location = "user"  # マクロの場所。
 relpath = os.path.relpath(filepath, start=macrofolder)  # マクロフォルダからの相対パスを取得。パス区切りがOS依存で返ってくる。
 return "vnd.sun.star.script:{}${}?language=Python&location={}".format(relpath.replace(os.sep, "|"), "{}", location)  # ScriptingURLのbaseurlを取得。Windowsのためにos.sepでパス区切りを置換。
def addMenuentry(menucontainer, menutype, i, props):  # i: index, propsは辞書。menutypeはActionTriggerかActionTriggerSeparator。
 menuentry = menucontainer.createInstance("com.sun.star.ui.{}".format(menutype))  # ActionTriggerContainerからインスタンス化する。
 [menuentry.setPropertyValue(key, val) for key, val in props.items()]  #setPropertyValuesでは設定できない。エラーも出ない。
 menucontainer.insertByIndex(i, menuentry)  # submenucontainer[i]やsubmenucontainer[i:i]は不可。挿入以降のメニューコンテナの項目のインデックスは1増える。
g_exportedScripts = macro, #マクロセレクターに限定表示させる関数をタプルで指定。
コンテクストメニューのMenuEntriesでScriptingURLで呼び出している関数enumerateMenuEntries()は、高階関数createEnumerator()の戻り値です。

26行目でenumerateMenuEntries()に渡したい引数をクロージャーで渡しています。

こうすることで、ScriptingURLで呼び出す関数をグローバル変数にするだけで、複数の引数を渡せるようになりました。

ちなみにやっていることは同じなのに、ScriptingURLで呼び出す関数にデコレーター構文を付けるとエラーになってしまいます。

上のCalcドキュメントに埋め込んであるマクロにはオートメーションのためのコードも含んでいますが、オートメーションやAPSOで実行するとグローバル変数が渡せないので結果が出力されません(Calc(36)コンテクストメニューをカスタマイズする: その5参照)。

次の関連記事:Calc(40)コンテクストメニューをカスタマイズする: その8

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ