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

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

概要[編集]


「クリップボード履歴」と「スニペット (テンプレート)」機能とをひとまとめにしたマクロです。

  • 2in1+α の機能をひとつのツールバーアイコンから利用できます。
  • 「クリップボード履歴」と「スニペット」間のデータの受け渡しができるようになります。


  • 「クリップボード履歴」機能は Mery 2.8.1 以降で利用できます。
  • 「スニペット」機能は スニペットプラグイン の設定ファイル snippets.txt を読み書きするというかたちにしています。


  • 「クリップボード履歴」機能は Mery 2.8.1 以降 でしか利用できません。
  • あらかじめ「includeライブラリ」の導入が必要です。


  • 外部実行ファイル「GetKeyState.exe」で機能を拡張できます(なくても差し支えありません)。
  • スニペットプラグイン」を導入していないでも「スニペット(定型文)」機能を利用できます。
snippets.txt 内のタブインデントによる階層構造をある程度ポップアップメニューに反映させられるようになっています。
ref. 「スニペットプラグイン」のページの Snippets.txt の書き方 を参照のこと。
※ ただし、このマクロでは snippets.txt 内の空行などの扱いについて 「スニペットプラグイン」と異なる部分 があります。


  • 動作確認は Windows XP sp3 (32bit) × Mery ベータ版 2.8.6 以降(ポータブル版)でしかしていません。
なにかしらの支障を来たす不具合が見つかった場合は、このマクロを削除して使用を中止するか、または フォーラム にてご報告ください。


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



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

(2019/11/29)

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


(2020/06/27 追加)

  • クリップボード履歴内の複数のアイテムをまとめて貼り付け(つなげて貼り付け)できます。
  • 追加コピー/追加切り取り」コマンドを利用できます(キーアサイン集バージョンとは異なり「追加コピー/追加切り取り」前後のアイテムがクリップボード履歴に累積しません)。
ref. 「追加コピー・追加切り取り」マクロ
  • GetKeyState.exe 」を導入していれば、アイテムを Ctrl+クリック したときに、アイテムを貼り付けし、そのアイテムをクリップボード履歴から削除します。
※ 設定項目 var ctrlEscape = true)にすると、¥ 記号を含むアイテム(ファイルパスなど)を Ctrl+クリック したときに ¥ を二重(¥¥)にして貼り付けます。



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

(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 内の空行やタブインデントでのクループ化が適切でない部分は、ポップアップメニューに正しく反映されません (その行または段落/グループをスキップし、ポップアップメニューに表示しません)。
※ タブインデントの階層を深くするさいは、かならず一段ずつ下げてください。
二段以上の差があるとエラーの元になったり、または予期せぬ階層に表示されたりします (これはスニペットプラグインでも起きえるものであり、不具合ではありません)。
※ 階層を浅くするさいは、二段以上の差があっても構いません。


(2020/06/27 追加)

  • オプション設定により snippets.txt 内で @@@ または @@@@ を含むアイテムの場合、「選択範囲を囲う」ことができます。


n 番目から m 番目まで貼り付け[編集]

クリップボード履歴内の連続する複数のアイテムを まとめて貼り付け します。

  • 「n 番目から m 番目まで貼り付け」
指定した複数のアイテムを そのままつなげて 貼り付けます。
  • 「n 番目から m 番目まで 任意の文字列 でつないで貼り付け...」
指定した複数のアイテムを 入力ダイアログで指定した文字列 でつなげて貼り付けます。
  • 「n 番目から m 番目までを 改行 でつないで貼り付け」
指定した複数のアイテムを 改行でつなげて 貼り付けます。

※ 「n 番目から m 番目までを 半角空白 でつないで貼り付け」「n 番目から m 番目までを 空行 でつないで貼り付け」コマンドもあります(ソースコード内 340 行目付近でコメントアウト)。
追加コマンドを選択すると入力ダイアログが2回(「任意の文字列」では3回)表示されますので、ポップアップメニュー内での 履歴アイテムの番号 を指定してください。

※ コピーした順番どおりにつなげて貼り付けするばあいは、「さいしょのアイテムの番号」>「さいごのアイテムの番号」です。
 e.g. 最新の3件をコピーした順につなげて貼り付けするなら

「さいしょのアイテムの番号」=
「さいごのアイテムの番号」=

※ 入力ダイアログが "空の状態" または "無効な文字列のみが入力された状態" でキャンセルされたばあいは、なにも貼り付けしません。

※ 設定項目 var unitToTop = true; にすると、つなげたテキストデータをクリップボード履歴の先頭に追加登録します (ただし 16 件目にあったアイテムは消えます)。
var unitToTop = true; でも var unitToTop = false; でも、つなげる前の各アイテムは削除しません (削除する仕様だと番号指定をまちがえたときに手戻りできなくなってしまう)。


ピン止めアイテムで選択範囲を囲う[編集]

設定項目 var sandwich = true にすると、snippets.txt 内で @@@ または @@@@ を含むアイテムは「選択範囲の囲い込み」に利用できるようになります。

囲い込み用文字列が改行を含んでいる場合、マルチカーソル/複数選択に非対応です(正しく囲い込みできません)。

  • @3つ@@@)ふくむアイテムは、選択範囲全体@@@ の前後の文字列で囲います。
e.g.
・snippets.txt のアイテム:<pre>\n@@@\n</pre>
・選択範囲の文字列:
var d = editor.ActiveDocument;
var s = d.selection;
▼ 結果 ▼
<pre>
var d = editor.ActiveDocument;
var s = d.selection;
</pre>


  • @4つ@@@@)ふくむアイテムは、選択範囲の各行@@@@ の前後の文字列で囲います。
e.g.
・snippets.txt のアイテム:<code>@@@@</code>
・選択範囲の文字列:
var d = editor.ActiveDocument;
var s = d.selection;
▼ 結果 ▼
<code>var d = editor.ActiveDocument;</code>
<code>var s = d.selection;</code>


  • 設定項目 var cheese = "@"@ を任意の文字に変更可。


ダウンロード[編集]

>> 「ファイル:クリップボード履歴.zip」(アイコン入り)
最終更新: 2020/06/27


sunipetts.txt は「スニペットプラグイン」のものがあれば共用、なければ新規に生成(要 includeライブラリ)しますので、マクロ本体の JS ファイルとアイコンだけをご利用ください。


  • ポップアップメニュー内の連番アイテムの桁埋め(右寄せ/空白埋め)用の 空白文字 を設定する項目 var b1 = " "; を追加し、初期値を半角空白(U+0020 " ")にしました(※ これまで MS UI Gothic に最適化してあった桁埋め方法をカスタマイズできるようにした)。
・MS UI Gothic では2分アキ(EN SPACE) "\u2002"
・Meiryo UI では和字間隔(全角空白) "\u3000"
・Segoe UI では図形間隔(FIGURE SPACE) "\u2007" にすると具合がよいようです。


ソースコード[編集]

#title = "クリップボード履歴..."
#tooltip = "クリップボード履歴 と スニペット"
#include "include/IO.js"
#icon = "clipboard_history[1].ico"
// #icon = "Mery用 マテリアルデザインっぽいアイコン.icl",314

/**
 * --------------------------------------------------
 * 「クリップボード履歴 と スニペット」マクロ
 *  sukemaru, 2019/08/01 - 2020/06/27
 *  https://www.haijin-boys.com/wiki/「クリップボード履歴」メニューのマクロ化
 * --------------------------------------------------
 * 「クリップボード履歴」メニューと「スニペット」プラグインと同等の機能を
 *   ひとつのポップアップメニューに統合します。
 * 
 * ※ ver 2.8.0 以前の Mery では「クリップボード履歴」機能を使用できません。
 * ※「スニペット」プラグインを導入していない場合でも
 *   「スニペット」機能(ピン止め)を使用できます。
 * 
 * ※「include ライブラリ」が必要です。
 *    https://www.haijin-boys.com/wiki/includeライブラリ
 * ※「GetKeyState.exe」で機能を拡張できます。
 *    https://www.haijin-boys.com/wiki/GetKeyState.exe(キー状態取得実行ファイル)
 */


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

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

  // true にすると、Ctrl+V でつづけて貼り付け可
  // false なら、クリップボードのアイテムの順番を維持する

  // ■ toTop = true のとき
  //   「n 番目から m 番目までをつなげて貼り付け」したアイテムを
  //    クリップボード履歴の先頭に登録する(※ただし 16 番目だったアイテムが消える)
  var unitToTop = false;    // true: 登録する / false: 登録しない


// ■ 「任意の文字列でつなげて貼り付け」で入力した文字列の一時記憶方法
var tagType = 2;

  // 0 : 一時記憶なし
  // 1 : タブ(文書)ごとに一時記憶する(Document.Tag)
  // 2 : ウインドウごとに一時記憶する(Editor.Tag)
  // 3 : すべてのタブとウインドウ共通で一時記憶する(window.Tag)


// ■ "@@@" をふくむピン止めアイテムを "選択範囲の囲いこみ用" にする
var sandwich = false;       // true: する / false: しない
var cheese = "@"            // 初期値:"@"

  // sandwich = true なら、ピン止めアイテムが "@@@" をふくむときに...
  // ⇒ "@@@@" (cheese × 4) で区切られた前後の文字列で「選択範囲の各行」を囲う
  // ⇒ "@@@"  (cheese × 3) で区切られた前後の文字列で「選択範囲全体」を囲う
  // cheese = "@" の「@」は任意の文字に変更可


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

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

// ■ 半角英小文字や ascii 記号を全角で表示する
var toWideWidth = 0;        // (0 しない / 1 文字間を広げる / 2 全角にする)


// ■ 連番アイテムの桁埋め(右寄せ/空白埋め)用の空白文字の定義
var blankChr = " ";     // " " or "\u2002" or "\u3000" or "\u2007" or ...

  /* 半角数字の幅に等しい空白文字として
     MS UI Gothic では「2分アキ」 = EN SPACE: "\u2002"
     (MeiryoKe_UI Gothic は MS UI Gothic に同じ)
     Meiryo UI では「和字間隔」 = 全角空白: "\u3000"
     Segoe UI では「図形間隔」 = FIGURE SPACE: "\u2007"
     にすると具合がよさそうですが、ウエイトによって幅が変わるので... */


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

  // ■ クリップボードのアイテムの Ctrl+クリックで
  //    ¥ を ¥¥ に置換して貼り付ける
  var ctrlEscape = false;

    // true なら、 Ctrl+クリックで ¥ を ¥¥ に置換して貼り付け
    // false なら、Ctrl+クリックで 貼り付けしてからアイテムを削除
    // 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 versionCheck = VersionCheck( "2.8.1" );
var d  = editor.ActiveDocument;
var s  = d.selection;
var st = s.Text;
var $status = Status;

var gksPath   = getKeyStatePath || meryDir + "Macros\\GetKeyState.exe";
var gksExists = Fso.FileExists( gksPath );
var $ctrl = false;
var $Ctrl = function() {
  return ( gksExists
  && WshShell.Run( '"' + gksPath + '" c', 0, true ) === 1 );
}

var p = "";
var tagKey = "ClipboardMenu";
var ham  = cheese ? cheese.replace( /[.*+?^=!:${}()|[\]\/\\]/g, "\\$&" ) : "";
sandwich = ham ? sandwich : false;

var b1 = ( ! blankChr || ( "" + blankChr ).length > 1 )
       ? " " : blankChr;
Object.prototype.PadSpace = function(len, chr) {
  var dgt = String(this);
  len = Number(len) || dgt.length;
  chr = chr ? chr.toString() : b1;
  if (len < 0 || len < dgt.length || chr.length !== 1) return dgt;
  for (var i = 0; i < len; i++) chr += chr;
  return (chr + dgt).slice( - len);
};


// ピン止めアイテム (スニペット)の準備
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 < 16 ; 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 numWidth = String( Math.max( snCount, cbCount ) ).length;


// ポップアップメニューの準備
var menu = CreatePopupMenu();
var grayFlag = d.ReadOnly ? meMenuGrayed : 0;
var t = ( snCount ) ? "▼ " : "";
var label,  id;


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

// スニペットのアイテムをメニューに
if ( snText ) {
  var sn0, sn1, sn2, tab1, tab2;
  var subArray = [];            // サブメニュー: 10 階層までを想定
  for ( var i = 0; i < 10; i ++ ) {
    subArray.push( [] );
  }
  var subID, subID1, subID2;
  for ( var i = 0, j = 1, tab0 = 0; i < snCount; i ++, j ++ ) {
    try {
      sn1 = snArray[i];
      sn2 = snArray[j] || "";   // 次の有効文字列の行(空行やセパレータ行は除外)
      // 行頭のタブのケタ数(階層の深さ)
      tab1 = ( sn1.charAt( 0 ) == "\t" )
           ? sn1.search( /[^\t]/ ) : 0;
      tab2 = ( sn2.charAt( 0 ) == "\t" )
           ? sn2.search( /[^\t]/ ) : 0;
      // メニューに表示させるラベル部分とメニューID
      label = MenuKey( sn1.replace( /^\t+|\t+[^\t]*$/g, "" )
                     , j, numWidth, menuWidth, b1, true );
      id = j + 400;
      // メインメニューにサブメニュー項目(見出し)を追加
      if ( tab1 == 0 && tab1 + 1 == tab2 ) {
        subID = subArray[0].length;
        subArray[0].push( CreatePopupMenu() );
        if ( grayFlag ) {
          menu.Add( label + "\t\u25B6", id, grayFlag ); //「▶」(\u25B6)
        }
        else {
          menu.AddPopup( label, subArray[0][ subID ] );
        }
        subMenu0.Add( "", 0, meMenuSeparator );
        sn0 = false;
      }
      // メインメニュー直下にアイテムを追加
      else if ( tab1 == 0 && ( tab1 == tab2 || tab1 + 1 < tab2 ) ) {
        menu.Add( label, id, grayFlag );
        if ( tab1 < tab0 && sn0 ) {
          subMenu0.Add( "", 0, meMenuSeparator );
        }
        subMenu0 .Add( label, id, grayFlag );
        sn0 = 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 ( sn0 ) {
          subMenu0.Add( "", 0, meMenuSeparator );
        }
        sn0 = 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 && sn0 ) {
          subMenu0.Add( "", 0, meMenuSeparator );
        }
        subMenu0.Add( label, id, grayFlag );
        sn0 = true;
      }
      tab0 = tab1;
    }
    catch( e ) { ;
      /**
       * snArray[i] (行_A) と次の有効文字列の行 snArray[j] (行_B) の
       * タブインデントを比較して、行_A よりも 行_B が二段以上深い場合、
       * 行_A をサブメニュー見出し項目にせず、通常アイテムとして扱います。
       * 行_B から始まるグループは、親になる項目が適切に処理されないことにより、
       * メインメニュー部分の階層構造に正しく反映されなくなります
       * (状況によっては部分的にエラー扱いになります)。
       * ただし「▼ ピン止めアイテム/スニペット ▼」配下の
       * ベタ置きのアイテムとしては(状況によっては不完全に)表示されます。
       */
    }
  }
}
menu.Add( "", 0, meMenuSeparator );


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

if ( ! ctrlEscape && ( cbData0 || cbData ).indexOf( "\\" ) > -1 ) {
  menu.Add( "", 0, meMenuSeparator );
  menu.Add( "\u00A5 を \u00A5\u00A5 に置換して "    //「¥」(\u00A5)
          + "履歴の先頭アイテム を貼り付け (&E)"
          , 18, grayFlag );
}

if ( cbCount > 1 ) {
  var u1 = "履歴の n 番目から m 番目までを";
  var u2 = "つなげて貼り付け ";
  menu.Add( "", 0, meMenuSeparator );
  menu.Add( u1 + u2 + "(&M)", 94, grayFlag );
  menu.Add( u1 + " 任意の文字列 で" + u2 + "(&P)...", 95, grayFlag );
  // menu.Add( u1 + " 半角空白 で" + u2 + "(&W)", 96, grayFlag );
  menu.Add( u1 + " 改行 で" + u2 + "(&N)", 97, grayFlag );
  // menu.Add( u1 + " 空行 で" + u2 + "(&B)", 98, grayFlag );
}

  menu.Add( "", 0, meMenuSeparator );
  menu.Add( "追加コピー (&C)", 92 );
  menu.Add( "追加切り取り (&T)", 93, grayFlag );

if ( cbData || cbCount ) {
  menu.Add( "", 0, meMenuSeparator );
  menu.Add( "クリップボードのすべての履歴を削除する (&E)", 99 );
}

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


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


// ポップアップメニューを表示
var r = menu.Track( + menuPosMouse );


main: {
  if ( ! r ) {
    Status = $status;  break main;
  }
  editor.ExecuteCommandByID( MEID_WINDOW_ACTIVE_PANE = 2189 );


  // 94, 95, 96, 97, 98: n 番目から m 番目まで貼り付け
  unit:
  if ( r >= 94 && r <= 98 ) {
    $ctrl = $Ctrl();
    // 半角変換した ascii 文字列を返す関数
    var ToHalfWidth = function( strVal ) {
      return strVal.replace( /[!-~]/g, function( tmp ) {
          return String.fromCharCode( tmp.charCodeAt(0) - 0xFEE0 )
    } ) };

    // 入力ダイアログ×2回	※全角/半角の数字のみを有効文字列として取得する
    var p1 = + ToHalfWidth( Prompt( "さいしょのアイテムの番号", "" ) )
               .replace( /\D/g, "" );
    if ( p1 <= 0 || p1 > cbCount ) {
      Status = " キャンセル";  break unit;
    }
    var p2 = + ToHalfWidth( Prompt( "さいごのアイテムの番号", p1 ) )
               .replace( /\D/g, "" );
    if ( p2 <= 0 || p2 > cbCount ) {
      Status = " キャンセル";  break unit;
    }

    // 入力された番号が同一なら、通常の貼り付けコマンド r == 1~16 に飛ぶ
    if ( p1 === p2 ) {
      r = p1;  break unit;
    }

    // 上から順 または 下から順
    else {
      var str = "";
      var b = ( r === 95 ) ? AddStrPrompt( tagType, tagKey )
            : ( r === 96 ) ? " "
            : ( r === 97 ) ? "\n"
            : ( r === 98 ) ? "\n\n"
            :/* r ===94 */   "";
      if ( p1 < p2 ) {  // 上から順
        for ( var i = p1; i <= p2; i ++ ) {
          str += cbArray[ i - 1 ] + b;
        }
      }
      else {            // 下から順
        for ( var i = p2; i <= p1; i ++ ) {
          str = cbArray[ i - 1 ] + b + str;
        }
      }
      str = str.slice( 0, b ? - b.length : str.length );

      // 貼り付け	※ Ctrl キーを押しながらのときは、 ¥ を ¥¥ に置換
      s.Text = ( ctrlEscape && $ctrl )
             ? str.replace( /\\/g, "\\\\" ) : str;

      // まとめたデータを履歴の先頭に登録
      if ( toTop && unitToTop ) { cb.SetData( pStr ); }
    }
  }	// unit:{}


  // 1 ~ 17: クリップボード履歴のアイテムを貼り付け
  // ※ Ctrl キーを押しながらのときは、 ¥ を ¥¥ に置換
  if ( ctrlEscape && r > 0 && r <= 17 ) {
    $ctrl = $ctrl || $Ctrl();
    var str = ( r === 17 ) ? cbData : cbArray[ r -1 ];
    s.Text = $ctrl ? str.replace( /\\/g, "\\\\" ) : str;

    // 貼り付けたデータを履歴の先頭に移動
    if ( toTop && r <= 16 ) {
      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 > 0 && r <= 16 ) {
    s.Text = cbArray[ r - 1 ];

    // Ctrl キーを押しながらのときは、貼り付けしてからアイテムを削除
    if ( $Ctrl() ) {
      cb.ClearData( r - 1 );
      if ( r === 1 ) { cb.ClearData(); }
      Status = " クリップボード履歴からアイテムを削除しました。";
    }

    // 貼り付けたデータを履歴の先頭に移動
    else if ( toTop ) {
      cbArray.unshift( cbArray.splice( r - 1, 1 ) );
      for ( var i = 0; i < cbCount; i ++ ) {
        cb.SetData( cbArray[i], i );
      }
      cb.SetData( cbArray[0] );
    }
  }


  // Windows のクリップボードデータから貼り付け
  else if ( r === 17 ) {
    s.Text = cbData;
    // Ctrl キーを押しながらのときは、貼り付けしてからアイテムを削除
    if ( $Ctrl() ) {
      cb.ClearData();
      Status = " クリップボードからアイテムを削除しました。";
    }
  }


  // ¥ を ¥¥ に置換して 履歴の先頭アイテム を貼り付け
  else if ( r === 18 ) {
    s.Text = ( cbData0 || cbData ).replace( /\\/g, "\\\\" );
  }


  // 追加コピー
  else if ( r === 92 ) {
    var line = d.GetLine( s.GetActivePointY( mePosLogical ), 0 );
    var str = s.IsEmpty ? ( line ? line + "\n" : "" )
                        : s.Text;
    if ( str ) {
      var oldData = cbData0 || cbData || "";
      cb.ClearData( 0 );
      cb.SetData( oldData + str );
    }
  }

  // 追加切り取り
  else if ( r === 93 ) {
    if ( s.IsEmpty ) { s.SelectLine(); }
    var str = s.Text;
    if ( str ) {
      var oldData = cbData0 || cbData || "";
      cb.ClearData( 0 );
      cb.SetData( oldData + str );
    }
    s.Delete();
  }


  // 99: クリップボード履歴をすべて削除する
  else if ( r === 99
  && Confirm( "クリップボードのすべての履歴を削除しますか? " ) ) {
    for ( var i = cbCount - 1; i >= 0; i -- ) {
      cb.ClearData( i );
    }
    cb.ClearData();
    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 {
      // データフォルダに Plugins\\Snippets フォルダがなければフォルダを生成
      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() );
      // snippets.txt の末尾に追加登録する
      str = snText
          + ( ( ! snText || snText.charAt( snText.length - 1 ) === "\n" )
            ? "" : "\n" )
          + str.replace( /[\\\t\n\r]/g, function( tmp ) {
              return ( tmp === "\\" ) ? "\\\\"
                   : ( tmp === "\t" ) ? "\\t"
                   : ( tmp === "\n" ) ? "\\n"
                   :/* tmp === "\r" */  "\\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( "\"" + meryPath + "\" \"" + snPath + "\"" );
    Status = " " + snPath;
  }


  // 401 ~ : ピン止めアイテムを貼り付け
  else if ( r > 400 ) {
    var $ctrl = $Ctrl();
    var snStr = snArray[ r - 401 ].replace( /^(?:\t)*[^\t]*\t/, "" )
                                  .split( "\\\\" );
    for ( var i = 0, len = snStr.length; i < len; i ++ ) {
      snStr[i] = snStr[i].replace( /\\t|\\r|\\n|\\/g, function( tmp ) {
        return ( tmp === "\\t" ) ? "\t"
             : ( tmp === "\\r" ) ? "\r"
             : ( tmp === "\\n" ) ? "\n"
             :/* tmp === "\\" */   "";
      } );
    }
    snStr = snStr.join( "\\" );

    // ピン止めアイテムで選択範囲を囲う
    var ham3 = ham + ham + ham,  ham4 = ham3 + ham;
    var reg = /\n?$/,  n = st.match( reg );
    var str = st.replace( reg, "" );

    // "@@@@" で区切られた前後の文字列で選択範囲の各行を囲う
    if ( sandwich && snStr.indexOf( ham4 ) > -1 ) {
      var a = str.split( "\n" );
      for ( var i = 0, len = a.length; i < len; i ++ ) {
        a[i] = snStr.replace( RegExp( ham4, "g" ), a[i] );
      }
      // クリップボード経由で「貼り付け」
      cb.SetData( str = a.join( "\n" ) + n );
      s.Paste();
      // クリップボードデータを復帰
      cb.ClearData();
      for ( var i = 0; i < 16; i ++ ) {
        cb.SetData( cbArray[i] || "", i );
      }
      cb.SetData( cbArray[0] );
      if ( $ctrl ) { cb.SetData( str ); }
    }

    // "@@@" で区切られた前後の文字列で選択範囲全体を囲う
    else if ( sandwich && snStr.indexOf( ham3 ) > -1  ) {
      s.Text = str = snStr.replace( RegExp( ham3, "g" ), str ) + n;
      if ( $ctrl ) { cb.SetData( str ); }
    }

    // ピン止めアイテムを貼り付け
    else {
      s.Text = snStr;
      // Ctrl キーを押しながらのときは、貼り付けたアイテムをコピー
      if ( $ctrl ) {
        cb.SetData( snStr );
        Status = " クリップボードにアイテムをコピーしました。";
      }
    }
  }

}	// main:{}


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

/**
 * 関数 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 ( Number( editorVer ) >= Number( requirement ) );
}


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


/**
 * 関数 GetTag( tagType, tagKey, property )
 * 指定された Tag の値を返す
 */
function GetTag( tagType, tagKey, property ) {
  try {
    var obj = ( typeof tagType === "object" ) ? tagType
            : ( tagType === 1 ) ? editor.ActiveDocument
            : ( tagType === 2 ) ? editor
            : ( tagType === 3 ) ? window
            : window;
    return ( obj.Tag.Exists( tagKey )
    && ( property ? property in obj.Tag( tagKey ) : true ) )
    ? property
      ? obj.Tag( tagKey )[ property ]
      : obj.Tag( tagKey )
    : null;
  } catch( e ) { Status = e;  return null; }
}

/**
 * 関数 SetTag( value, tagType, tagKey, property )
 * 指定された値を Tag に書き込む
 */
function SetTag( value, tagType, tagKey, property ) {
  try {
    var obj = ( typeof tagType === "object" ) ? tagType
            : ( tagType === 1 ) ? editor.ActiveDocument
            : ( tagType === 2 ) ? editor
            : ( tagType === 3 ) ? window
            : window;
    if ( property ) {
      if ( obj.Tag.Exists( tagKey ) ) {
        obj.Tag( tagKey )[ property ] = value;
      }
      else { obj.Tag( tagKey ) = { property: value }; }
    }
    else { obj.Tag( tagKey ) = value; }
  }
  catch( e ) { Status = e; }
  finally { return; }
}


/**
 * 関数 AddStrPrompt( tagType, tagKey )
 * 「任意の文字列」コマンド
 * 入力ダイアログで指定された文字列を返す
 */
function AddStrPrompt( tagType, tagKey ) {
  // 前回使用した文字列があればダイアログの初期値に再利用
  var str = "";
  if ( tagType && ( t = GetTag( tagType, tagKey, "addStr" ) ) ) {
    str = t;
  }
  // 入力ダイアログ
  var msg = "連結用文字列:\t"
          + "改行 = \\\\\\n ; タブ = \\\\\\t  (注:¥記号3つ)";
  p = Prompt( msg, str ) || "";
  if ( p && tagType ) {
    SetTag( p, tagType, tagKey, "addStr" );
  }
  return p.replace( /\\\\\\t/g, "\t" ).replace( /\\\\\\n/g, "\n" );
}


更新履歴[編集]

• 2020/06/27
 ・GetKeyState.exe による機能拡張の追加
  (クリップボード履歴アイテムの Ctrl+クリックで ¥ を ¥¥ に置換して貼り付け)
 ・「追加コピー」「追加切り取り」コマンドを追加
 ・「n 番目から m 番目まで貼り付け」コマンドを追加
 ・「n 番目から m 番目まで貼り付け」したデータのまとまりを履歴の先頭に登録する設定項目を追加
 ・「ピン止めアイテムで選択範囲を囲う」機能と設定項目を追加
 ・連番数字のケタ埋め用の空白文字のカスタマイズ用設定項目を追加
 ・ソースコード内の「能書き (詳細説明)」を削除
 ・ソースコード内の unicode 文字をコードポイント \uHHHH に書き換え
• 2020/06/05
 ・「追加コピー」マクロの節を削除
 ⇒「追加コピー・追加切り取り」マクロ(※ Mery ver 2.8.1 以降用)のページを新設
• 2020/05/02
 ・「n 番目から m 番目まで貼り付け」 を追加掲載(ダウンロードの ZIP 書庫のソースコードは変更していません)
 ・「追加コピー」マクロの節を追加(ZIP 書庫には未収録)
ここまでで、だいたいはスニペットの機能を再現できたとおもいます…。
• 2019/11/29
 ・「半角英文字を全角で表示」しない設定のとき、ascii 記号の前後に HAIR SPACE
 ・snippets.txt 内で、行頭が "「-」×1 + タブ文字" の行を無視
 ・クリップボード履歴から貼り付けたアイテムを、履歴の先頭に移動させるオプションを追加
 ・ピン止めアイテムを Ctrl+クリックした場合、アイテムを貼り付けし、そのアイテムをクリップボードにコピー
• 2019/11/09
 ・ver 2.8.0 以前の Mery でもスニペット機能だけ利用できるように変更
 ・「クリップボード履歴の重複アイテム削除」の設定項目を廃止( "削除する" で固定)
 ・snippets.txt 内の単独「\」の処理をスニペットプラグインにあわせる修正
 ・snippets.txt 内のセパレータ用の「-」の行をスキップする処理を追加
• 2019/11/05
 ・各アイテムの行数表示を廃止
 ・クリップボード履歴からスニペットにアイテムを登録するコマンドを追加
 ・メインメニューに表示するピン止めアイテム数の上限設定を廃止
 ・スニペット機能での「ピン止め/貼り付け」のさいの、改行コードやタブコードではない文字列「\n」や「\t」の扱いを再修正
  ( … したつもりだが、まだ不完全かも)
 ・スニペットプラグインのサブメニュー化階層構造をある程度再現した
  ( … つもりだが、アレンジしたので同じ表示状態にならないことがある)
• 2019/11/01:
 ・ステータス表示を追加
 ・クリップボードにテキストデータがないときのメニュー構成を変更
 ・アイテムの行数(改行数+1)をメニュー内に追加表示
 ・メニュー内に表示する改行コードの矢印を「↲」(U+21B2) に変更
 ・スニペット機能での「ピン止め/貼り付け」のさいの、改行コードやタブコードではない文字列「\n」や「\t」の扱いを修正
 ・snippets.txt がないときのエラーを修正
• 2019/08/06:
 ・「ピン止めアイテムを貼り付け」コードの変数ミスを修正
 ・クリップボード履歴内の重複アイテムを削除する設定を追加。
 ・ピン止めアイテムが少ないときはメインメニューにアイテムを表示。
 ・snippets.txt 内の空行を無視。
 ・クリップボード履歴に長大なデータがあるときの動作速度を少しだけ改善。
• 2019/08/01: 初版
スポンサーリンク