「クリップボード履歴」メニューのマクロ化

提供: MeryWiki
ナビゲーションに移動 検索に移動

概要[編集]


公式フォーラム[1] にて「クリップボード履歴」メニューの将来的に廃止されることが 予告?検討? されていますが、後継機能としては 「クリップボード履歴」プラグイン の登場により今後も安泰のようです。

ということで、試験運用中だった自家用マクロを再度リサイクルして「クリップボード履歴スニペット」の統合版ポップアップメニューにしてみました。
一見して 誰得? なマクロですが、「クリップボード履歴」を「スニペット」機能とまとめてツールバーアイコン化できるという素敵なメリットがあります。

「クリップボード履歴」機能は Mery 2.8.1 以降の『延命措置』の新パラメータ[2] を利用したもの。
「スニペット」機能は スニペットプラグイン の設定ファイル snippets.txt を読み書きするというかたちにしています。


  • 「クリップボード履歴」機能は Mery 2.8.1 以降 でしか利用できません。
  • あらかじめ「includeライブラリ」の導入が必要です。
  • 外部実行ファイル「GetKeyState.exe」で機能を拡張できます(なくても差し支えありません)。
  • スニペットプラグイン」を導入していないでも「スニペット(定型文)」機能を利用できます。
snippets.txt 内のタブインデントによる階層構造をある程度ポップアップメニューに反映させました。(2019/11/05)
ref. 「スニペットプラグイン」のページの Snippets.txt の書き方 を参照のこと。
ただし、このマクロでは snippets.txt 内の空行などの扱いについて 「スニペットプラグイン」と異なる部分 があります。
  • 動作確認は Windows XP sp3 (32bit) × Mery ベータ版 2.8.6 (ポータブル) 以降でしかしていません。
なにかしらの支障を来たす不具合が見つかった場合は、このマクロを削除して使用を中止するか、または フォーラム にてご報告ください。


cf. マクロライブラリには「定型文を挿入」機能に特化したマクロも別途あります。



ツールメニューの「クリップボード履歴」と異なる部分[編集]

(2019/11/29)

  • オプション設定により、貼り付けたアイテムを「履歴の先頭に移動する」ことができます。
→ 以後、Ctrl+V でつづけて貼り付けできるようになります。
  • クリップボード内に重複するアイテム(完全におなじ文字列データ)がある場合、古いデータを削除します。
→ 16 件分、まるまる活用できます。
※削除するのはこのマクロを実行したときだけで、「コピー」操作を監視するものではありません。
  • クリップボードのすべての履歴を削除」 「履歴からアイテムをひとつ削除」 「貼り付けしてからアイテムを削除」 「履歴からアイテムをスニペットに登録」 の機能を追加してあります。
ポップアップメニュー内のアイテムの Ctrl+クリック で「貼り付けしてからアイテムを削除」する機能は、「GetKeyState.exe 」を導入している場合にかぎり利用できます。
  • 「クリップボードのすべての履歴を削除」 したあとでも、Windows OS のクリップボードの最新の1件がテキストデータである場合は、「クリップボード履歴」にアイテムが表示されます。
Mery 本来の「クリップボード履歴」機能でも「履歴を消去」したあとにも Ctrl+V によるペーストができることがあるので、これを可視化してあります。
  • 「クリップボード履歴」 に重複アイテムアイテムがある場合は、ひとつを残して削除します。



「スニペットプラグイン」と異なる部分[編集]

(2019/11/29)

  • ピン止めアイテムを Ctrl+クリックした場合、アイテムを貼り付けし、そのアイテムをクリップボードにコピーします(GetKeyState.exe 」を導入している場合のみ)。
→ 以後、Ctrl+V でつづけて貼り付けできるようになります。
  • このマクロからスニペットに登録したアイテムは、snippets.txt の末尾に追加されます。
  • snippets.txt 内の「 & 」記号によるアクセラレータをすべて無視します。
→ 有効文字列のある行を上から順に連番化し、番号をアクセラレータにします。
  • snippets.txt 内の "半角ハイフン 「 - 」 ひとつ + 空白だけの行" をセパレータにしません。
(メイン階層を「クリップボード履歴」と共用しているため、体裁上の都合でメインメニュー上でのセパレータ付加は見送り)
※「-」×2 以上であれば、有効文字列として扱います (プラグインでは、これもセパレータになる)。
※「ピン止めアイテム/スニペット」サブメニュー内では、階層ごとに区切って全アイテムを列挙します。
  • snippets.txt 内の「空行・空白行」の扱い方などでスニペットプラグインと異なる解釈をしている部分があります。 このマクロでは、snippets.txt 内の「空行」と「タブ文字/半角空白だけの空白行」を完全に無視するので、「空文字のサブメニュー見出し」を作りません (ただし、全角空白や2つ以上のハイフン 「 -- 」 は有効な文字列として扱います)。
→ ポップアップメニューでの表示状態がスニペットプラグインと同じ階層構造にならないことがあります。
  • snippets.txt 内で、行頭が "「-」×1 + タブ文字" ではじまる行を無視します。
→ 行頭が "「-」+タブ文字" の行は "コメントアウトされた行" と見做します。
※ スニペットプラグインではタブ文字のあとに文字列があれば 「-」 をラベルとしてメニューのアイテムに追加しますが、このマクロでは無効な行となります。
  • snippets.txt 内の空行やタブインデントでのクループ化が適切でない部分は、ポップアップメニューに正しく反映されません。 その行または段落/グループをスキップし、ポップアップメニューに表示しません。
※ タブインデントの階層を深くするさいは、かならず一段ずつ下げてください。 二段以上の差があるとエラーの元になったり、または予期せぬ階層に表示されたりします (これはスニペットプラグインでも起きえるものであり、不具合ではありません)。
※ 階層を浅くするさいは、二段以上の差があっても構いません。

ダウンロード[編集]

>> 「ファイル:クリップボード履歴.zip」(アイコン入り)
最終更新: 2019/11/29
sunipetts.txt は「スニペットプラグイン」のものがあれば共用、なければ新規に生成しますので、マクロ本体の JS ファイルとアイコンだけをご利用ください。


ソースコード[編集]

#title = "クリップボード履歴..."
#tooltip = "クリップボード履歴 と スニペット"
#icon = "clipboard_history[1].ico"
#include "include/IO.js"

/**
 * --------------------------------------------------
 * 「クリップボード履歴 と スニペット」マクロ
 *  sukemaru, 2019/08/01 - 2019/11/29
 * --------------------------------------------------
 * 「クリップボード履歴」メニューと「スニペット」プラグインと同等の機能を
 *   ひとつのポップアップメニューに統合します。
 * 
 * ※ まだまだ不具合が残っているかもしれません。
 * ※ ソースコードの末尾に【能書き】
 */

// ---------- ▼ 設定項目 ▼ ---------- //

// ■ 貼り付けたアイテムをクリップボード履歴の先頭に移動する
var toTop = false;			// true: 移動する / false: 移動しない

// ■ ポップアップメニューを表示する位置
var menuPosMouse = true;	// true: マウス位置 / false: キャレット位置

// ■ ポップアップメニューに表示する文字数
var menuWidth = 60;

// ■ 半角英文字を全角で表示する	[!"%'(),.:;@\[\]`a-z{|}]
var toFullWidth = false;	// (true する / false しない)

// ■ GetKeyState.exe のフルパスを指定する場合( \ 記号はふたつがさね「\\」で)
// 未指定 "" なら、Mery インストールフォルダの Macros\GetKeyState.exe
var getKeyStatePath = "";	// ※ GetKeyState.exe なしのときも "" にする

// ---------- ▲ 設定項目 ▲ ---------- //


var start = new Date();

var Fso = new ActiveXObject( "Scripting.FileSystemObject" );
var WshShell = new ActiveXObject( "WScript.Shell" );

var meryPath	 = editor.FullName;
var meryDir		 = meryPath.replace( /[^\\]+$/, "" );
var isPortable	 = Fso.FileExists( meryPath.replace( /\.exe$/i, ".ini" ) );
var profileDir	 = ( isPortable )
                 ? meryDir
                 : WshShell.SpecialFolders( "APPDATA" ) + "\\Mery\\";
var snPath		 = profileDir + "Plugins\\Snippets\\Snippets.txt";
var snIsExist	 = Fso.FileExists( snPath );
var gksPath		 = getKeyStatePath || meryDir + "Macros\\GetKeyState.exe";
var gksIsExist	 = Fso.FileExists( gksPath );
var $ctrl		 = 0;
var versionCheck = VersionCheck( "2.8.1" );
var d	 = editor.ActiveDocument;
var s	 = d.selection;
var st	 = s.Text;
var $status = Status;

// ピン止めアイテム (スニペット)の準備
var snText = "",  snArray = [],  snCount = 0;
if ( snIsExist ) {
  // snippet.txt を読みこむ
  snText  = IO.LoadFromFile( snPath, "utf-8" ) || "";
  // 空白行とセパレータ用の "-" の行を除去した配列にする
  snArray = snText.split( "\n" );
  for ( var i = 0; i < snArray.length; i ++ ) {
    if ( /(?:^-\t|^\s*-?\s*$)/.test( snArray[i] ) ) {
      snArray.splice( i --, 1 );
    }
  }
}
snCount = snArray.length;

// クリップボード履歴の準備
var cb		 = ClipboardData;
var cbData	 = cb.GetData();
var cbArray	 = [];
if ( versionCheck ) {
  var cbData0 = cb.GetData( 0 );
  if ( cbData0 ) {
    outer:
    for ( var i = 0, cbItem;  ; i ++ ) {
      cbItem = cb.GetData( i ) || "";
      if ( ! cbItem ) { break outer; }
      // 重複するアイテムを履歴から削除する
      inner:
      for ( var j = i + 1, cbNextItem;  ; j ++ ) {
        cbNextItem = cb.GetData( j ) || "";
        if ( ! cbNextItem ) { break inner; }
        if ( cbItem == cbNextItem ) {
          cb.ClearData( j -- );
        }
      }
      // 履歴アイテムを配列に収納する
      cbArray.push( cbItem );
    }
  }
}
var cbCount	 = cbArray.length;
var width	 = String( Math.max( snCount, cbCount ) ).length;


// ポップアップメニューの準備
var menu = CreatePopupMenu();
var grayFlag = d.ReadOnly ? meMenuGrayed : 0;

// 「ピン止めアイテム/スニペット」サブメニュー
var sm0 = CreatePopupMenu();
var t   = ( snCount ) ? "▼ " : "";
menu.AddPopup( t + "ピン止めアイテム/スニペット (&S) " + t, sm0 );
if ( st ) {
  sm0.Add( "選択範囲を登録 (&P)", 300 );
}
if ( snIsExist ) {
  sm0.Add( "スニペットを編集 (&E) ...", 400 );
}
if ( st || snIsExist ) {
  sm0.Add( "", 0, meMenuSeparator );
  sm0.Add( "キャンセル & ", 0 );
}
if ( snCount ) {
  sm0.Add( "", 0, meMenuSeparator );
}

// スニペットのアイテムをメニューに
if ( snText ) {
  var label, label0, label1, label2, tab1, tab2;
  var subArray = [];	// SubMenu Array
  for ( var i = 0; i < 10; i ++ ) { subArray.push( [] ); }
  var subId,  subId1,  subId2;
  for ( var i = 0, j = 1, tab0 = 0, id; i < snCount; i ++, j ++ ) {
    try {
      label1 = snArray[i];
      label2 = snArray[j] || "";
      tab1   = ( label1.charAt( 0 ) == "\t" ) ? label1.search( /[^\t]/ ) : 0;
      tab2   = ( label2.charAt( 0 ) == "\t" ) ? label2.search( /[^\t]/ ) : 0;
      label  = MenuKey( label1.replace( /^\t+|\t+[^\t]*$/g, "" )
                      , j, width, menuWidth, true );
      id     = j + 400;
      // メインメニューにサブメニュー項目を追加
      if ( tab1 == 0 && tab1 + 1 == tab2 ) {
        subId = subArray[0].length;
        subArray[0].push( CreatePopupMenu() );
        if ( grayFlag ) {
          menu.Add( label + "\t▶", id, grayFlag );
        }
        else {
          menu.AddPopup( label, subArray[0][ subId ] );
        }
        sm0.Add( "", 0, meMenuSeparator );
        label0 = false;
      }
      // メインメニューにアイテムを追加
      else if ( tab1 == 0 && ( tab1 == tab2 || tab1 + 1 < tab2 ) ) {
        menu.Add( label, id, grayFlag );
        if ( tab1 < tab0 && label0 ) {
          sm0.Add( "", 0, meMenuSeparator );
        }
        sm0 .Add( label, id, grayFlag );
        label0 = true;
      }
      // サブメニュー内にサブメニュー項目を追加
      else if ( tab1 > 0 && tab1 + 1 == tab2 ) {
        subId1 = subArray[ tab1 ].length;
        subId2 = subArray[ tab1 -1 ].length - 1;
        subArray[ tab1 ].push( CreatePopupMenu() );
        subArray[ tab1 -1 ][ subId2 ].AddPopup( label, subArray[ tab1 ][ subId1 ] );
        if ( label0 ) {
          sm0.Add( "", 0, meMenuSeparator );
        }
        label0 = false;
      }
      // サブメニューにアイテムを追加
      else if ( tab1 > 0 && ( tab1 >= tab2 || tab1 + 1 < tab2 ) ) {
        subId = subArray[ tab1 -1 ].length - 1;
        subArray[ tab1 -1 ][ subId ].Add( label, id, grayFlag );
        if ( tab1 < tab0 && label0 ) {
          sm0.Add( "", 0, meMenuSeparator );
        }
        sm0.Add( label, id, grayFlag );
        label0 = true;
      }
      tab0 = tab1;

    }
    catch( e ) {
      /**
       * snArray[i] (行_A) と次の有効文字列の行 snArray[j] (行_B) の
       * タブインデントを比較して、行_A よりも 行_B が二段以上深い場合、
       * 行_A をサブメニュー見出し項目にせず、通常アイテムとして扱います。
       * 行_B から始まるグループは、親になる項目が適切に処理されないことにより、
       * メインメニュー部分の階層構造に正しく反映されなくなります
       * (状況によっては部分的にエラー扱いになります)。
       * ただし「▼ ピン止めアイテム/スニペット ▼」配下の
       * ベタ置きのアイテムとしては(状況によっては不完全に)表示されます。
       */
    }
  }
}
menu.Add( "", 0, meMenuSeparator );

// クリップボード履歴のアイテムをメニューに
if ( versionCheck ) {
  if ( cbCount ) {
    var sm1  = CreatePopupMenu();
    var sm2  = CreatePopupMenu();
    var sm3  = CreatePopupMenu();
    var sm4  = CreatePopupMenu();
    menu.AddPopup( "▼ クリップボード履歴の一覧 (&C) ▼", sm1 );
    for ( var i = 0, id, label; i < cbCount; i ++ ) {
      id = i + 1;
      label = MenuKey( cbArray[i], id, width, menuWidth );
      menu.Add( label, id, grayFlag );
      sm2. Add( label, id + 100, grayFlag );
      sm3. Add( label, id + 200 );
      sm4. Add( label, id + 300 );
    }
    menu.Add( "", 0, meMenuSeparator );
    menu.Add( "クリップボードのすべての履歴を削除する (&E)", 99 );
    sm1.AddPopup( "履歴からアイテムをスニペットに登録 (&P)", sm4 );
    sm1.AddPopup( "貼り付けしてからアイテムを削除 (&M)", sm2 );
    sm1.AddPopup( "履歴からアイテムを削除 (&D)", sm3 );
    sm1.Add( "", 0, meMenuSeparator );
    sm1.Add( "すべての履歴を削除 (&E)", 99 );
    sm1.Add( "", 0, meMenuSeparator );
    sm1.Add( "キャンセル & ", 0 );
  }
}
if ( ! cbCount && cbData.length ) {
  menu.Add( "▼ クリップボード ▼", 0, meMenuGrayed );
  label = MenuKey( cbData, 1, width, menuWidth );
  menu.Add( label, 17, grayFlag );
} 
else if ( ! cbCount && ! cbData.length ) {
  menu.Add( "※ クリップボードにテキストデータはありません ※"
          , 0, meMenuGrayed );
}

menu.Add( "", 0, meMenuSeparator );
menu.Add( "キャンセル & ", 0 );


// ステータスバーの表示
if ( grayFlag ) {
  Status = " ドキュメントは書き換え禁止です。";
}
else if ( ! versionCheck ) {
  Status = " 「クリップボード履歴」機能の動作要件は"
         + " \"Mery ver 2.8.1\" 以上です。";
}
else if ( gksIsExist && cbCount ) {
  Status = " 履歴アイテムの Ctrl+クリック で"
         + " 「貼り付けしてからアイテムを削除」";
}
else if ( gksIsExist && snCount ) {
  Status = " ピン止めアイテムの Ctrl+クリック で"
         + " 「クリップボードにコピーして貼り付け」";
}
else {
  Status = " 「クリップボード履歴 と スニペット」マクロ";
}
Status += " [ "
       + ( ( new Date() - start ) / 1000 ).toFixed( 3 ).replace( /\./, ". " )
       + " 秒 ]";


// ポップアップメニューを表示
var r = menu.Track( + menuPosMouse );
var confirmStr = "クリップボードのすべての履歴を削除しますか? ";
Status = $status;

if ( r == 0 ) {  /* Status = $status */ ; }

// 1 ~ 17: クリップボード履歴のアイテムを貼り付け
else if ( r <= 16 ) {
  s.Text = cbArray[ r -1 ];
  // Ctrl キーを押しながらのときは、貼り付けしてからアイテムを削除
  if ( gksIsExist ) {
    $ctrl = WshShell.Run( "\"" + gksPath + "\" c", 0, true );
    if ( $ctrl == 1 ) {
      cb.ClearData( r -1 );
      Status = " クリップボード履歴からアイテムを削除しました。";
    }
  }
  if ( toTop && ! $ctrl ) {
    cbArray.unshift( cbArray.splice( r -1, 1 ) );
    for ( var i = 0; i < cbCount; i ++ ) {
      cb.SetData( cbArray[i], i );
    }
    cb.SetData( cbArray[0] );
  }
}
else if ( r == 17 ) {
  s.Text = cbData ;
}

// 99: クリップボード履歴をすべて削除する
else if ( r == 99 && Confirm( confirmStr ) ) {
  for ( var i = 0; i < cbCount; i ++ ) {
    cb.ClearData( i );
  }
  Status = " クリップボード履歴からすべてのアイテムを削除しました。";
}

// 101 ~ 116: 貼り付けしてからアイテムを削除
else if ( r > 100 && r < 200 ) {
  s.Text = cbArray[ r -101 ];
  cb.ClearData( r -101 );
  Status = " 履歴からアイテムを削除しました。";
}

// 201 ~ 216: クリップボード履歴からアイテムを削除
else if ( r > 200 && r < 300 ) {
  cb.ClearData( r -201 );
  Status = " 履歴からアイテムを削除しました。";
}

// 300 ~ 316: スニペットに登録(ピン止め)
else if ( r >= 300 && r < 400 ) {
  try {
    var snippetsDir = Fso.GetParentFolderName( snPath );
    var pluginsDir = Fso.GetParentFolderName( snippetsDir );
    if ( ! Fso.FolderExists( pluginsDir ) ) {
      Fso.CreateFolder( pluginsDir );
    }
    if ( ! Fso.FolderExists( snippetsDir ) ) {
      Fso.CreateFolder( snippetsDir );
    }
    // 選択範囲 または クリップボード履歴のアイテム
    var str = ( r == 300 )
            ? st
            : ( cb.GetData( r -301 ) || cb.GetData() );
    str = snText
        + ( ! snText || snText.charAt( snText.length -1 ) == "\n"
          ? "" : "\n" )
        + str.replace( /\\/g, "\\\\" )
             .replace( /\t/g, "\\t" )
             .replace( /\n/g, "\\n" )
             .replace( /\r/g, "\\r" );
    IO.SaveToFile( snPath, str, "utf-8", true );
    var copyFrom = ( r == 300 ) ? "範囲" : "したアイテム";
    Status = " 選択" + copyFrom + "をスニペットに登録しました。";
  } catch( e ) {
    Status = " スニペットに登録できませんでした。";
  }
}

// 400: スニペットを編集(snippets.txt を開く)
else if ( r == 400 ) {
  WshShell.Run( "\"" + editor.FullName + "\" \"" + snPath + "\"" );
  Status = " " + snPath;
}

// 401 ~ : ピン止めアイテムを貼り付け
else if ( r > 400 ) {
  var str = snArray[ r -401 ].replace( /^(?:\t)*[^\t]*\t/, "" )
                             .split( "\\\\" );
  for ( var i = 0, len = str.length; i < len; i ++ ) {
    str[i] = str[i].replace( /\\t/g, "\t" )
                   .replace( /\\r/g, "\r" )
                   .replace( /\\n/g, "\n" )
                   .replace( /\\/g, "" );
  }
  str = str.join( "\\" );
  // Ctrl キーを押しながらのときは、貼り付けしたアイテムをコピー
  if ( gksIsExist ) {
    $ctrl = WshShell.Run( "\"" + gksPath + "\" c", 0, true );
    if ( $ctrl == 1 ) {
      cb.SetData( str );
      Status = " クリップボードにアイテムをコピーしました。";
    }
  }
  s.Text = str;
}


// ---------- ▼ 関数 ▼ ---------- //

/**
 * 関数 VersionCheck( versionStr )
 * Mery 本体が引数で指定したバージョン以上かチェックする( e.g. "2.6.9" )
 * 戻り値は、真偽値 true/false
 */
function VersionCheck( versionStr ) {
  var editorVer, requirement;
  var Pad2 = function( str ) {
    return str.replace( /[0-9]+/g , function( digit ) {
      return digit.length < 2 ? "0" + digit : digit
    } )
  };
  editorVer = + ( Pad2( editor.Version ).replace( /\./g, "" ).slice( 0, 6 ) );
  requirement = + ( Pad2( versionStr ).replace( /\./g, "" ).slice( 0, 6 ) );
  return ( editorVer >= requirement );
}

/**
 * 関数 MenuKey( str, num, numWidth, menuWidth, boolean )
 * ポップアップメニューに表示するラベルを生成する
 * ・行頭空白を除去、空白文字を圧縮:		→ 「›」(U+203A)
 * ・改行記号を可視化:					→ 「↲」(U+21B2) または 「⏎」(U+23CE)
 * ・削られてしまう 「&」 を補完
 * ・「¥」(U+005C) を 「∖」(U+2216) に置換:		または 「╲」(U+2572), 「﹨」(U+FE68)
 * ・ゼロ幅や特殊な空白文字を豆腐に置換:	→ 「⊠」(U+22A0) または 「▯」(U+25AF)
 * ・判別しづらい半角記号を全角に置換:		!"%'(),.:;@[]`{|}
 *     + 「a-z」 を全角に置換
 *   または半角記号の前後にスキマをつける:	HAIR SPACE 「 」(U+200A)
 * ・行番号を空白でケタ埋め:				EN SPACE 「 」(U+2002)
 * ・文字数を切り詰め
 */
function MenuKey( str, num, numWidth, menuWidth, conv ) {
  var keyWidth = menuWidth;
  if ( conv ) {
    var strArr = str.replace( /^[^\t]*\t/, "" ).split( "\\\\" );
    for ( var i = 0; i < strArr.length; i ++ ) {
      strArr[i] = strArr[i].replace( /\\t/g, " › " )
                           .replace( /\\r|(\\r)?\\n/g, " ↲ " )
                           .replace( /\\/g, "" );
    }
    str = strArr.join( "\\" );
  }
  var reg = /[\u00A0\u1680\u180E\u2000-\u200E\u2028\u2029\u202F\u205F\u2062\u2063\uFEFF]/g;
  var menuKey = str.replace( /(?:\t|[ ]{3,}|[ ]{2,})+/g, " › " )
                   .slice( 0, menuWidth *2)
                   .replace( /(?:\r?\n|\r)/g, " ↲ " )	// ⏎ ↵
                   .replace( /[&]/g, "&&" )
                   .replace( /[\\]/g, "∖" )
                   .replace( reg, "▯" );
  if ( toFullWidth ) {
    menuKey = menuKey.replace( /[!"%'(),.:;@\[\]`a-z{|}]/g,
                function( tmp ) {
                  return String.fromCharCode( tmp.charCodeAt( 0 ) + 0xFEE0 )
                } );
  }
  else {
    // スラッシュ 以外の ascii 記号と fijl に HAIR SPACE (U+200A) を付加
    // [!"#$%'()*+,-.:;<=>?@\[\]^_`{|}~] + "&&"
    menuKey = menuKey.replace( /[!-%'-.:-@\[-`{-~fijl¥⊠▯]|&&/g, " $& " )
                     .replace( /\u200A+/g, " " )
                     .replace( /\u2002\u200A/g, " " );
    keyWidth = keyWidth
             + ( menuKey.match( /[\u200A›↲]|&&/g ) || "" ).length;
  }
  num = ( "   " + num ).slice( - numWidth ).replace( /\d$/, "&$&: " );
  menuKey = ( menuKey.length > keyWidth )
          ? menuKey.slice( 0, keyWidth ) + " ..."
          : menuKey;
  return num + menuKey;
}


// ---------- ▼ 能書き ▼ ---------- //

/**
 【このマクロの仕様・制限事項】
 ・ver 2.8.1 以前の Mery では「クリップボード履歴」機能を使用できません。
 
 ・「クリップボード履歴」は Mery 本体のメモリストアのものを使用します。
 
 ・「スニペット」プラグインの設定ファイル snippets.txt の読み書きをします。
   ※「include ライブラリ」が必要です(snippets.txt ファイルの読み書きに使用)。
   ※「スニペット」プラグインを導入していない場合でも、
     「ピン止めアイテム/スニペット」機能を使用できます。
     snippets.txt ファイルの保存場所は Mery\Plugins\Snippets フォルダです
   (インストーラ版では %AppData%\Mery\Plugins\Snippets フォルダ)。
 
 ・「GetKeyState.exe(キー状態取得実行ファイル) 」で機能を拡張できます。
      → クリップボード履歴のアイテムを Ctrl+クリックした場合、
         アイテムを貼り付けし、貼り終えたアイテムを削除します。
         また、ピン止めアイテムを Ctrl+クリックした場合、
         アイテムを貼り付けし、そのアイテムをクリップボードにコピーします。

   ※「GetKeyState.exe」は Mery\Macros フォルダに配置してください
     (Macros フォルダ以外の場所にあるなら設定項目でパスを指定する)。
     (Macros フォルダ以外の場所にあるなら設定項目でパスを指定する)。
   ※「GetKeyState.exe」がなくても、クリップボード履歴の
      サブメニュー項目から「貼り付けしてからアイテムを削除」できます。
 
 【ツールメニューの「クリップボード履歴」と異なる部分】 (2019/11/05)

 ・「クリップボードのすべての履歴を削除」したあとでも、
   Windows OS のクリップボードの最新の1件がテキストデータである場合は、
  「クリップボード履歴」にアイテムが表示されます。
   ※ Mery 本来の「クリップボード履歴」機能でも「履歴を消去」したあとにも
     Ctrl+V によるペーストができることがあるので、これを可視化してあります。
 
 【スニペットプラグインと異なる部分】 (2019/11/27)

 ・ このマクロからスニペットに登録したアイテムは、
    snippets.txt の末尾に追加されます。

 ・ snippets.txt 内の 「-」×1 と空白だけの行をセパレータにしません。
      → メイン階層を「クリップボード履歴」と共用しているため、
         体裁上の都合で「ピン止めアイテム」内でのセパレータは見送り。
   ※ 「-」×2 以上であれば、有効文字列として扱います。
   ※「ピン止めアイテム/スニペット」サブメニュー内では
      階層ごとに区切ってベタで列挙します。

 ・ snippets.txt 内で、行頭が "「-」×1 + タブ文字" ではじまる行を無視します。
      → 行頭が "「-」 +タブ文字" の行は "コメントアウトされた行" と見做します。
   ※ スニペットプラグインではタブ文字のあとに文字列があれば
      「-」 をラベルとしてメニューのアイテムに追加しますが、
      このマクロでは無効な行となります。

 ・ snippets.txt 内の 「&」 記号によるアクセラレータはすべて無視します。
      → 有効文字列のある行を上から順に連番化し、番号をアクセラレータにします。

 ・ snippets.txt 内の「空行・空白行」の扱い方などで
    スニペットプラグインと異なる解釈をしている部分があります。
   ※ このマクロでは、snippets.txt 内の 空行、タブ文字だけの空白行、「-」 だけの行を
      完全に無視するので、「空文字や "-" だけのサブメニュー見出し」を作りません
    (ただし、全角空白や2つ以上のハイフン 「--」 は文字列として扱う)。
        → ポップアップメニューでの表示状態がスニペットプラグインと
          同じ階層構造にならないことがあります。
   ※ snippets.txt 内の空行や 「-」 だけの行、タブインデントでのクループ化が
      適切でない部分は、ポップアップメニューに正しく反映されません。

 ・ タブインデントの階層を深くするさいは、かならず一段ずつ下げてください
  (二段以上の差があるとエラーの元)。
      → その行またはグループをスキップし、ポップアップメニューに表示しません。
   ※ 階層を浅くするさいは、二段以上の差があっても構いません。
 */


更新履歴[編集]

• 2019/08/01: 初版
• 2019/08/06:
 ・「ピン止めアイテムを貼り付け」コードの変数ミスを修正
 ・クリップボード履歴内の重複アイテムを削除する設定を追加。
 ・ピン止めアイテムが少ないときはメインメニューにアイテムを表示。
 ・snippets.txt 内の空行を無視。
 ・クリップボード履歴に長大なデータがあるときの動作速度を少しだけ改善。
• 2019/11/01:
 ・ステータス表示を追加
 ・クリップボードにテキストデータがないときのメニュー構成を変更
 ・アイテムの行数(改行数+1)をメニュー内に追加表示
 ・メニュー内に表示する改行コードの矢印を「↲」(U+21B2) に変更
 ・スニペット機能での「ピン止め/貼り付け」のさいの、改行コードやタブコードではない文字列「\n」や「\t」の扱いを修正
 ・snippets.txt がないときのエラーを修正
• 2019/11/05
 ・各アイテムの行数表示を廃止
 ・クリップボード履歴からスニペットにアイテムを登録するコマンドを追加
 ・メインメニューに表示するピン止めアイテム数の上限設定を廃止
 ・スニペット機能での「ピン止め/貼り付け」のさいの、改行コードやタブコードではない文字列「\n」や「\t」の扱いを再修正
  ( … したつもりだが、まだ不完全かも)
 ・スニペットプラグインのサブメニュー化階層構造をある程度再現した
  ( … つもりだが、アレンジしたので同じ表示状態にならないことがある)
• 2019/11/09
 ・ver 2.8.0 以前の Mery でもスニペット機能だけ利用できるように変更
 ・「クリップボード履歴の重複アイテム削除」の設定項目を廃止( "削除する" で固定)
 ・snippets.txt 内の単独「\」の処理をスニペットプラグインにあわせる修正
 ・snippets.txt 内のセパレータ用の「-」の行をスキップする処理を追加
• 2019/11/29
 ・「半角英文字を全角で表示」しない設定のとき、ascii 記号の前後に HAIR SPACE
 ・snippets.txt 内で、行頭が "「-」×1 + タブ文字" の行を無視
 ・クリップボード履歴から貼り付けたアイテムを、履歴の先頭に移動させるオプションを追加
 ・ピン止めアイテムを Ctrl+クリックした場合、アイテムを貼り付けし、そのアイテムをクリップボードにコピー
これでだいたいはスニペットの機能を再現できたとおもいますが…。
スポンサーリンク