スクリプト

スクリプトランチャー「SDCLauncher.jsx」

[概要]

スクリプトをフォルダごとに並ぶボタンで一覧にし、パネルに配置しておけるランチャー。スクリプトと同名の.png画像を準備すれば、ボタンとして拾う。

無料版と、Pro版の2バージョン展開。

Pro版は無料版の全機能に、

・設定のプリセット機能
・登録フォルダを全削除機能
・フォルダごとのボタンサイズ調節機能

が追加されます。

Boothにて。
https://shadeco.booth.pm/items/8197470

無料版でも十分な機能を持たせています。

[使い方]

事前準備として、スクリプトを任意のフォルダに分けて整理しておく。
「タイムライン操作系」「シェイプ生成系」などジャンルごとに分けても、「一軍」「二軍」など優先度別でもいい。フォルダ名がランチャー上でグループ名として機能するので、自分が分かりやすいフォルダ名にしておく。

にSDCLauncher.jsxを入れる。これで次回起動時にAfter Effectsの上部メニュー「ウィンドウ」に「SDCLauncher.jsx」が出てくる。ここから起動すると、スクリプトを他のパネルとドッキングできるScript UI Panelとして起動できる。

  1. ウィンドウからスクリプトをUIパネルで実行する
  2. [設定]ボタンから設定パネルを開く
  3. 登録フォルダパスにスクリプトのアドレス(フルパス)を入れて[スクリプトフォルダ登録]をクリック
  4. メインパネルにフォルダ名が読み込まれ、スクリプトがボタンで並ぶ
  5. スクリプトを格納したフォルダが複数あればガンガン登録する
  6. ボタンクリックで該当スクリプトを実行して便利に使う

↓3.のスクリプトフォルダを登録したところ

↓ボタン表示、リスト表示風にも使える

[オプション]

メインパネル

  • スクリプトのボタンはパネルのリサイズに応じてグリッド整列する
  • 名前順で並ぶので、元のスクリプトは冒頭に01,02…と連番を降ってリネームすると整理できる
  • [Fn]チェックボックスをONで各フォルダのファンクションボタンを表示/非表示できる
    • ファンクションボタンは[×][↑][↓]の3つ
    • [×]ボタンはメインパネルからそのフォルダカードを消す
    • [↑][↓]ボタンはフォルダカードの順序入れ替え
  • [設定]ボタンで設定パネルを開く
    • スクリプトのあるフォルダーをガンガン登録できる。
    • [Pro版][フォルダ全削除]ボタンで登録フォルダを0にできる。
    • 「ボタンサイズ」でスクリプトボタンのサイズを調節できる。(デフォルト180px, 30px)
    • ボタンサイズは全ボタン一括指定
    • 「カード右余白」でフォルダカードの隙間を調節できる。(デフォルト10px)
    • 「フォルダ名の最小確保width」でファンクションボタンが被ってフォルダ名が隠れない領域を確保する。(デフォルト72px)
    • 数値はキーボードの↑↓キーで1ずつ変えられる
    • [Pro版]配置設定を5つまでプリセットとして登録しておける
      • [Pro版]プリセットドロップダウンで切り替え
      • [Pro版][別スロットに複製]ボタンで現在のプリセットを他のスロットにペーストする
      • [Pro版][エクスポート]ボタンで現在の設定をJSON形式で表示できる
        • [Pro版]メモ帳などにコピペし保存しておける
      • [Pro版][インポート]ボタンでJSON形式の設定テキストを読み取りできる

各フォルダカード

  • 登録したフォルダ内のスクリプトを一覧で表示するエリア
  • ボタンはパネルに入る限り右方向に整列する
  • 右端に到達すると下へ折り返す
  • 「フォルダ名のテキスト部分」をctrlかcmd+クリックでフォルダの設定パネルが開く
    • 「ボタンの固定折り返し」が0なら右方向にボタンが並ぶだけ整列し、右端に到達すると下へ折り返す(デフォルトは0)
    • 「ボタンの固定折り返し」が1以上の数値で横に並ぶボタン数を固定できる
    • 「フォルダカード折り返し」ONでパネル右側の余白があっても次のフォルダカードは下に折り返す+フォルダカード右側に線が入る(デフォルトはOFF)
    • 「ボタンのアイコン設定」ONで.png画像を用意していても、このフォルダ内のみ全部テキストボタンにする
    • [Pro版]ボタンサイズでそのフォルダのみのボタンサイズを変更できる(デフォルト空欄)
      • [Pro版]空欄なら設定パネルで一括指定したボタンサイズになる
    • [反映]ボタンでこの設定を実行、パネルを閉じればキャンセル
    • [カード削除]ボタンはファンクションボタンの[×]と同じく、このフォルダをメインパネルから消す

スクリプトボタン

  • クリックでスクリプト起動
  • スクリプトと同名の.png画像を同じフォルダ内に用意すれば自動で拾って表示する
  • ctrlかcmd+クリックでボタンの設定パネルが開く
    • 「表示ラベル」でボタンに表示される名前をカスタマイズできる(SDCLauncher上での表示名のみ変更)
      • 空欄でOKすれば初期名に復元する
    • 「ボタンのアイコン設定」ONで.png画像を用意していても、このボタンのみテキストボタンにする
    • [OK]ボタンで反映、パネルを閉じればキャンセル

[解説]

長いのでコード折りたたみ
JavaScript
#target aftereffects
var scName="ScriptDirectoryCardsLauncher";
var scNameShort="SDCLauncher";
var scVer="Ver.1.0";

(function(thisObj){

	//==========
	// 初期設定もろもろ準備
	//==========

	var OP_label_items="items";
	var OP_label_btn=["buttonWidth","buttonHeight"];
	var OP_label_newPath="newPath";
	var OP_label_marginR="cardRightMargin";
	var OP_label_minLabelW="minLabelWidth";
	var OP_label_btnLabelMap="btnLabelMap";
	var OP_label_btnNoPngMap="btnNoPngMap";
	var OP_label_showFnBtns="showFnButtons";
	var OP_maxPresets=1;

	var DEF_btnSize=[180,30];
	var DEF_marginR=10;
	var DEF_minLabelSizeX=72;

	var DEV_btnMargin=[4,4];
	var DEV_pnlMargin=[5,5];
	var DEV_footerAreaH=30;

	var DEV_labelSizeY=18;
	var DEV_editSizeY=22;

	var DEV_holdBtnSize=[26,18];//カードヘッダーで予約する最小幅
	var DEV_btnSize=[48,22];
	var DEV_scrollSizeX=16;

	var DEV_inputWidth=60;

	var CARD_minW=20;
	var CARD_padding=[6,4];
	var CARD_breakSizeY=34;

	var folDataList=[];
	var btnLabelMap={};
	var btnNoPngMap={};

	var scriptFile=new File($.fileName);
	var baseFolder=scriptFile.parent;

	var pal=(thisObj instanceof Panel)?thisObj:new Window("palette",scName,undefined,{resizeable:true,closeButton:true});
	if(!pal) return;

		pal.viewareaGroup=null;
		pal.bgGroup=null;
		pal.scrollBarCore=null;
		pal.contentOffsetY=0;
		pal.contentSizeYeight=0;
		pal.folderSections=[];
		pal.emptyInfoText=null;
		pal.topSettingsBtn=null;
		pal.topFnCheckbox=null;

	var settingsWin=null;
	var settingsNewPathLabel=null;
	var settingsAddPathBtn=null;
	var settingsNewPathInput=null;
	var settingsApplyBtn=null;
	var settingsResetBtn=null;
	var settingsSizeLabelW=null;
	var settingsWidthInput=null;
	var settingsSizeLabelH=null;
	var settingsHeightInput=null;
	var settingsRightMarginInput=null;
	var settingsRightMarginLabel=null;
	var settingsMinLabelWidthInput=null;
	var settingsClearFoldersBtn=null;
	var settingsSuspendSave=false;

	//==========
	// GUI準備
	//==========

	// 汎用ラベル
	function f_addLabel(parent,width,text){
		return parent.add("statictext",[0,0,width,DEV_labelSizeY],text);
	}

	// 汎用ボタン
	function f_addButton(parent,text,width,height,tip){
		var button=parent.add("button",undefined,text);
		if(width!==undefined&&width!==null) button.preferredSize.width=width;
		if(height!==undefined&&height!==null) button.preferredSize.height=height;
		if(tip) button.helpTip=tip;
		return button;
	}

	// edittext ↑↓キーで増減
	function f_editNumUpDn(editText,minValue){
		editText.addEventListener("keydown",function(event){
			if(event.keyName!=="Up"&&event.keyName!=="Down") return;
			var value=parseInt(this.text,10);
			if(isNaN(value)) return;
			if(event.keyName==="Up"){
				value+=1;
			}else if(event.keyName==="Down"){
				value-=1;
			}
			if(minValue!==undefined&&minValue!==null&&value<minValue){
				value=minValue;
			}
			this.text=String(value);
			try{event.preventDefault();}catch(e){}
		});
	}

	// ctrlかcmdキー押されてるか検知
	function f_isCtrlOrCmdPressed(){
		var keyState=ScriptUI.environment.keyboardState;
		if(!keyState) return false;
		return !!(keyState.ctrlKey||keyState.metaKey);
	}

	// ファイルパスの2バイト文字を文字化けしないように
	function f_pathDecodeName(fileObj){
		var text="";
		try{
			text=String(fileObj.displayName||fileObj.name||"");
		}catch(e){
			text=String(fileObj.name||"");
		}
		try{
			text=Folder.decode(text);
		}catch(e2){
			try{
				text=decodeURI(text);
			}catch(e3){}
		}
		return text;
	}

	// 今のラベルを取得
	function f_nowBtnLabel(fileObj){
		var key=String(fileObj.fsName);
		if(btnLabelMap.hasOwnProperty(key)) return String(btnLabelMap[key]);
		return f_pathDecodeName(fileObj).replace(/\.(jsx|jsxbin)$/i,"");
	}

	function f_get_savedShowFnBtns(){
		if(app.settings.haveSetting(scNameShort,f_get_preset(OP_label_showFnBtns))){
			return app.settings.getSetting(scNameShort,f_get_preset(OP_label_showFnBtns))==="true";
		}
		return false;
	}

	function f_get_showFnBtns(){
		if(pal&&pal.topFnCheckbox) return !!pal.topFnCheckbox.value;
		return f_get_savedShowFnBtns();
	}

	function f_serializeBtnLabelMap(mapObj){
		var result=[];
		for(var key in mapObj){
			if(!mapObj.hasOwnProperty(key)) continue;
			result.push(f_encodeSettingVal(key)+"="+f_encodeSettingVal(String(mapObj[key])));
		}
		return result.join("||");
	}

	function f_deserializeBtnLabelMap(raw){
		var result={};
		if(!raw||raw==="") return result;
		var parts=raw.split("||");
		for(var i=0;i<parts.length;i++){
			var part=parts[i];
			if(part==="") continue;
			var eq=part.indexOf("=");
			if(eq<0) continue;
			var key=f_decodeSettingVal(part.substring(0,eq));
			var value=f_decodeSettingVal(part.substring(eq+1));
			result[String(key)]=String(value);
		}
		return result;
	}

	function f_serializeBoolMap(mapObj){
		var result=[];
		for(var key in mapObj){
			if(!mapObj.hasOwnProperty(key)) continue;
			result.push(f_encodeSettingVal(key)+"="+(mapObj[key]?"1":"0"));
		}
		return result.join("&");
	}

	function f_deserializeBoolMap(raw){
		var result={};
		if(!raw||raw==="") return result;
		var parts=raw.split("&");
		for(var i=0;i<parts.length;i++){
			var part=parts[i];
			if(!part) continue;
			var eq=part.indexOf("=");
			if(eq<0) continue;
			var key=f_decodeSettingVal(part.substring(0,eq));
			var value=part.substring(eq+1)==="1";
			result[String(key)]=value;
		}
		return result;
	}

	function f_saveAndRebuild(showAlert){
		f_saveState();
		f_rebuildUI(!!showAlert);
	}

	function f_addPxInputRow(parent,labelText,initialValue,minValue){
		var row=parent.add("group");
		row.orientation="row";
		row.alignChildren=["left","center"];
		row.spacing=6;
		f_addLabel(row,100,labelText);
		var input=row.add("edittext",[0,0,DEV_inputWidth,DEV_editSizeY],String(initialValue));
		input.justify="right";
		f_editNumUpDn(input,minValue);
		row.add("statictext",undefined,"px");
		return input;
	}

	function f_addRightPxInputRow(parent,initialValue,minValue){
		var row=parent.add("group");
		row.orientation="row";
		row.alignment=["right","center"];
		row.alignChildren=["right","center"];
		row.spacing=6;
		var input=row.add("edittext",[0,0,DEV_inputWidth,DEV_editSizeY],String(initialValue));
		input.justify="right";
		f_editNumUpDn(input,minValue);
		row.add("statictext",undefined,"px");
		return input;
	}

	function f_applySettingsInputEvents(){
		settingsNewPathInput.onChange=function(){f_saveState();};
		settingsWidthInput.onChange=function(){f_saveState();};
		settingsHeightInput.onChange=function(){f_saveState();};
		settingsRightMarginInput.onChange=function(){f_saveState();};
		settingsMinLabelWidthInput.onChange=function(){f_saveState();};
	}

	// 個別の「ボタン設定」ウィンドウ
	function f_btnLabelPnlGUI(fileObj){
		var dlg=new Window("dialog","["+f_nowBtnLabel(fileObj)+"]ラベル設定");
			dlg.orientation="column";
			dlg.alignChildren=["fill","top"];
			dlg.spacing=10;
			dlg.margins=15;

		var labelPnl=dlg.add("panel",undefined,"表示ラベル");
			labelPnl.orientation="column";
			labelPnl.alignChildren=["fill","top"];
			labelPnl.spacing=6;
			labelPnl.margins=10;

		var input=labelPnl.add("edittext",undefined,f_nowBtnLabel(fileObj));
			input.characters=28;
			input.active=true;

		var subInfo=labelPnl.add("statictext",undefined,"空欄でOKすると初期名に戻ります");
			subInfo.alignment=["left","top"];

		var iconPnl=dlg.add("panel",undefined,"ボタンのアイコン設定");
			iconPnl.orientation="column";
			iconPnl.alignChildren=["left","top"];
			iconPnl.spacing=6;
			iconPnl.margins=10;

		var noPngChk=iconPnl.add("checkbox",undefined,"ボタンにpng画像を使わない");
		var key=String(fileObj.fsName);
			noPngChk.value=!!btnNoPngMap[key];

		var row=dlg.add("group");
			row.orientation="row";
			row.alignChildren=["center","center"];
			row.alignment=["center","top"];
			row.spacing=8;

		var okBtn=f_addButton(row,"OK",80,24,"変更を保存");

		okBtn.onClick=function(){
			var value=String(input.text).replace(/^\s+|\s+$/g,"");
			var key=String(fileObj.fsName);
			if(value===""||value===String(fileObj.name).replace(/\.(jsx|jsxbin)$/i,"")){
				delete btnLabelMap[key];
			}else{
				btnLabelMap[key]=value;
			}
			if(noPngChk.value){
				btnNoPngMap[key]=true;
			}else{
				delete btnNoPngMap[key];
			}
			f_saveAndRebuild(false);
			dlg.close();
		};

		dlg.center();
		dlg.show();
	}

	// 「設定」ウィンドウ
	function f_OptionPnlGUI(){
		if(settingsWin){
			try{settingsWin.close();}catch(e){}
			settingsWin=null;
		}
			settingsWin=new Window("palette",scNameShort+" "+scVer+"設定",undefined,{resizeable:false,closeButton:true});
			settingsWin.orientation="column";
			settingsWin.alignChildren=["fill","top"];
			settingsWin.spacing=8;
			settingsWin.margins=10;

		var wrap=settingsWin.add("group");
			wrap.orientation="column";
			wrap.alignChildren=["fill","top"];
			wrap.spacing=8;

		var presetRow=wrap.add("group");

		var presetNames=[];
		for(var p=0;p<OP_maxPresets;p++){
			presetNames.push("Preset"+String(p+1));
		}

		var targetTopGr=wrap.add("group");
			targetTopGr.orientation="row";
			targetTopGr.alignChildren=["center","center"];
			targetTopGr.alignment=["fill","top"];

		var targetTopLeft=targetTopGr.add("group");
			targetTopLeft.orientation="row";
			targetTopLeft.alignChildren=["left","center"];
			targetTopLeft.alignment=["left","center"];
				settingsNewPathLabel=targetTopLeft.add("statictext",undefined,"↓登録フォルダパス");

		var targetTopSpacer=targetTopGr.add("group");
			targetTopSpacer.alignment=["fill","fill"];


		var inputPath=wrap.add("edittext",[0,0,200,DEV_editSizeY],f_get_savedNewPath());
			inputPath.justify="right";
		settingsNewPathInput=inputPath;

		var addBtnRow=wrap.add("group");
			addBtnRow.orientation="row";
			addBtnRow.alignChildren=["center","center"];
			addBtnRow.alignment=["center","top"];
			addBtnRow.spacing=6;
			settingsAddPathBtn=f_addButton(addBtnRow,"スクリプトフォルダ登録",null,22,"入力中のフォルダを追加");
			settingsAddPathBtn.onClick=function(){
				var newPath=String(settingsNewPathInput.text);
				var trimmed=newPath.replace(/^\s+|\s+$/g,"");
				if(trimmed==="")return;

				var folder=new Folder(trimmed);
				if(!folder.exists){
					alert("フォルダが見つかりません:\n"+folder.fsName);
					return;
				}

				folDataList.push(f_makeFolderItem(trimmed,false));
				f_saveAndRebuild(true);
			};

		var sizePanel=wrap.add("panel",undefined,"ボタンサイズ");
			sizePanel.orientation="column";
			sizePanel.alignChildren=["left","center"];
			sizePanel.spacing=6;
			sizePanel.margins=10;

		settingsWidthInput=f_addPxInputRow(sizePanel,"width",f_get_savedBtnSizeX(),1);
		settingsSizeLabelW=sizePanel.children[0].children[0];
		settingsHeightInput=f_addPxInputRow(sizePanel,"height",f_get_savedBtnSizeY(),1);
		settingsSizeLabelH=sizePanel.children[1].children[0];

		var marginPanel=wrap.add("panel",undefined,"カード右余白");
			marginPanel.orientation="column";
			marginPanel.alignChildren=["fill","top"];
			marginPanel.margins=10;

		settingsRightMarginInput=f_addRightPxInputRow(marginPanel,f_get_savedMarginR(),0);

		var minLabelPnl=wrap.add("panel",undefined,"フォルダ名の最小確保width");
			minLabelPnl.orientation="column";
			minLabelPnl.alignChildren=["fill","top"];
			minLabelPnl.margins=10;
				settingsRightMarginLabel=f_addLabel(minLabelPnl,160,"狭くてもボタンが被らない領域");
				settingsRightMarginLabel.alignment=["center","center"];
				settingsRightMarginLabel.justify="center";

		settingsMinLabelWidthInput=f_addRightPxInputRow(minLabelPnl,f_get_savedMinLabelSizeX(),0);

		var applyGr=wrap.add("group");
			applyGr.orientation="row";
			applyGr.alignChildren=["center","center"];
			applyGr.alignment=["center","top"];
			applyGr.spacing=6;
				settingsApplyBtn=f_addButton(applyGr,"更新",80,24,"ボタンサイズ以降の設定を反映");
				settingsApplyBtn.onClick=function(){f_saveAndRebuild(false);};
				settingsResetBtn=f_addButton(applyGr,"初期化",80,24,"全設定を初期値に戻す");
				settingsResetBtn.onClick=function(){
					var ok=confirm("フォルダ、ボタン設定に初期値を上書きしますか?",true,scNameShort);
					if(!ok) return;
					f_reset_defState();
					f_rebuildUI(false);
				};

		f_applySettingsInputEvents();
		settingsWin.onClose=function(){f_saveState();return true;};
		settingsWin.layout.layout(true);
		return settingsWin;
	}

	//==========
	// 描画系
	//==========

	// いくつ横に並ぶか計算
	function f_howManyBtn(cardSizeX,btnSizeX){
		var okAreaX=cardSizeX-(CARD_padding[0]*2);
		var perRow=Math.floor((okAreaX+DEV_btnMargin[0])/(btnSizeX+DEV_btnMargin[0]));
		if(perRow<1) perRow=1;
		return perRow;
	}

	// 折り返すカード調べ
	function f_breakOnOffBtnNum(sec,cardSizeX,btnSizeX){
		var autoPerRow=f_howManyBtn(cardSizeX,btnSizeX);
		if(!sec||sec.type==="break") return autoPerRow;
		if(sec.lockButtonsPerRow>0) return sec.lockButtonsPerRow;
		return autoPerRow;
	}

	// フォルダカードの幅から高さを計算
	function f_byWidthCardSizeY(sec,cardSizeX,btnSizeX,btnSizeY){
		if(sec.type==="break") return CARD_breakSizeY;
		var perRow=f_breakOnOffBtnNum(sec,cardSizeX,btnSizeX);
		var rows=0;
		if(sec.buttons.length>0) rows=Math.ceil(sec.buttons.length/perRow);
		var buttonsH=0;
		if(rows>0) buttonsH=(rows*btnSizeY)+((rows-1)*DEV_btnMargin[1]);
		return CARD_padding[1]+16+6+buttonsH+2;
	}

	// フォルダカードの最小幅
	function f_cardMinSizeX(sec,btnW){
		if(sec.type==="break") return CARD_minW;
		var oneBtnNeed=(CARD_padding[0]*2)+btnW;
		if(oneBtnNeed<1) oneBtnNeed=1;
		return oneBtnNeed;
	}

	// マジの最小幅
	function f_canCardMinSizeX(rowCount,btnSizeX){
		if(rowCount<=0) return CARD_minW;
		return(CARD_padding[0]*2)+(rowCount*btnSizeX)+((rowCount-1)*DEV_btnMargin[0]);
	}

	// フォルダカードの絶妙な幅を計算
	function f_cardMaxJustSizeX(sec,btnSizeX){
		if(sec.type==="break") return CARD_minW;

		var buttonGapX=2;
		var minLabelW=f_get_minLabelSizeX();
		var fnAreaW=f_get_showFnBtns()?((DEV_holdBtnSize[0]*3)+(buttonGapX*2)):0;
		var headerNeedSizeX=(CARD_padding[0]*2)+minLabelW+(fnAreaW>0?(buttonGapX+fnAreaW):0);

		var rowCount=1;
		if(sec.lockButtonsPerRow>0){
			rowCount=Math.min(sec.lockButtonsPerRow,sec.buttons.length||1);
		}else{
			rowCount=sec.buttons.length||1;
		}
		if(rowCount<1) rowCount=1;

		var targetSizeX=f_canCardMinSizeX(rowCount,btnSizeX);
		return Math.max(f_cardMinSizeX(sec,btnSizeX),headerNeedSizeX,targetSizeX);
	}

	// ボタンを並べる
	function f_alignBtns(sec,left,top,cardSizeX,btnSizeX,btnSizeY,contentTop){
		var perRow=f_breakOnOffBtnNum(sec,cardSizeX,btnSizeX);
		var innerLeft=left+CARD_padding[0];
		var innerTop=top+contentTop;
		var index=0;
		for(var i=0;i<sec.buttons.length;i++,index++){
			var col=index%perRow;
			var row=Math.floor(index/perRow);
			var x=innerLeft+(col*(btnSizeX+DEV_btnMargin[0]));
			var y=innerTop+(row*(btnSizeY+DEV_btnMargin[1]));
			sec.buttons[i].bounds=[x,y,x+btnSizeX,y+btnSizeY];
		}
	}

	// 折り返しオブジェクトを配置
	function f_layoutBreaker(sec,left,top,cardSizeX){
		sec.cardPanel.bounds=[left,top,left+cardSizeX,top+CARD_breakSizeY];
		var rightX=left+cardSizeX-CARD_padding[0];
		sec.btnDown.bounds=[rightX-DEV_holdBtnSize[0],top+CARD_padding[1],rightX,top+CARD_padding[1]+DEV_holdBtnSize[1]];
		rightX-=DEV_holdBtnSize[0]+4;
		sec.label.bounds=[left+CARD_padding[0],top+CARD_padding[1],rightX,top+CARD_padding[1]+DEV_labelSizeY];
	}

	// [設定][Fn]ボタンの下張り付き配置
	function f_layoutFooter(panelSizeY){
		var footerTop=panelSizeY-DEV_footerAreaH+4;

		if(pal.topSettingsBtn){
			pal.topSettingsBtn.bounds=[
				DEV_pnlMargin[0],
				footerTop,
				DEV_pnlMargin[0]+DEV_btnSize[0],
				footerTop+DEV_btnSize[1]
			];
		}

		if(pal.topFnCheckbox){
			pal.topFnCheckbox.bounds=[
				DEV_pnlMargin[0]+DEV_btnSize[0]+8,
				footerTop+2,
				DEV_pnlMargin[0]+DEV_btnSize[0]+54,
				footerTop+20
			];
		}
	}

	// フォルダごとのカードを配置
	function f_layoutFolderCard(sec,left,top,cardSizeX,btnSizeX,btnSizeY){
		var cardH=sec.cardBounds[3]-sec.cardBounds[1];
		var cardRight=left+cardSizeX;
		var rightX=cardRight-CARD_padding[0];
		var labelLeft=left+CARD_padding[0];
		var labelTop=top+CARD_padding[1];
		var minLabelW=f_get_minLabelSizeX();
		var buttonGapX=2;
		var showFnBtns=f_get_showFnBtns();
		var buttonAreaW=showFnBtns?((DEV_holdBtnSize[0]*3)+(buttonGapX*2)):0;
		var safeLabelRight=labelLeft+minLabelW;
		var buttonLeft=rightX-buttonAreaW;

		if(showFnBtns&&buttonLeft<safeLabelRight+buttonGapX){
			buttonLeft=safeLabelRight+buttonGapX;
		}

		var labelRight=showFnBtns?(buttonLeft-buttonGapX):rightX;
		if(labelRight<safeLabelRight){
			labelRight=safeLabelRight;
		}

		sec.cardPanel.bounds=[left,top,left+cardSizeX,top+cardH];
		sec.label.bounds=[labelLeft,labelTop,labelRight,labelTop+DEV_labelSizeY];

		sec.btnDelete.visible=showFnBtns;
		sec.btnUp.visible=showFnBtns;
		sec.btnDown.visible=showFnBtns;

		if(showFnBtns){
			var x1=buttonLeft;
			var x2=x1+DEV_holdBtnSize[0]+buttonGapX;
			var x3=x2+DEV_holdBtnSize[0]+buttonGapX;
			sec.btnDelete.bounds=[x1,labelTop,x1+DEV_holdBtnSize[0],labelTop+DEV_holdBtnSize[1]];
			sec.btnUp.bounds=[x2,labelTop,x2+DEV_holdBtnSize[0],labelTop+DEV_holdBtnSize[1]];
			sec.btnDown.bounds=[x3,labelTop,x3+DEV_holdBtnSize[0],labelTop+DEV_holdBtnSize[1]];
		}else{
			sec.btnDelete.bounds=[0,0,0,0];
			sec.btnUp.bounds=[0,0,0,0];
			sec.btnDown.bounds=[0,0,0,0];
		}

		f_alignBtns(sec,left,top,cardSizeX,btnSizeX,btnSizeY,CARD_padding[1]+16+6);
	}

	// パネルの再描画
	function f_rebuildUI(showAlert){
		if(showAlert===undefined) showAlert=false;
		if(!pal) return;

		while(pal.children.length>0){
			pal.remove(pal.children[0]);
		}

		pal.viewareaGroup=null;
		pal.bgGroup=null;
		pal.scrollBarCore=null;
		pal.contentOffsetY=0;
		pal.contentSizeYeight=0;
		pal.folderSections=[];
		pal.emptyInfoText=null;
		pal.topSettingsBtn=null;
		pal.topFnCheckbox=null;

		var panelW=pal.size.width;
		var panelSizeY=pal.size.height;
		if(panelW<=0) panelW=260;
		if(panelSizeY<=0) panelSizeY=200;

		var contentViewH=panelSizeY-DEV_footerAreaH;
		if(contentViewH<40) contentViewH=40;

		pal.viewareaGroup=pal.add("group",[0,0,panelW-DEV_scrollSizeX,contentViewH]);
		pal.viewareaGroup.clipped=true;
		pal.bgGroup=pal.viewareaGroup.add("group",[0,0,panelW-DEV_scrollSizeX,contentViewH]);
		pal.scrollBarCore=pal.add("scrollbar",[panelW-DEV_scrollSizeX,0,panelW,contentViewH]);
		pal.scrollBarCore.stepdelta=20;
		pal.scrollBarCore.onChanging=function(){
			var viewW=pal.viewareaGroup.size.width;
			var contentSizeY=pal.contentSizeYeight;
			if(!isFinite(contentSizeY)||contentSizeY<0) contentSizeY=0;
			pal.contentOffsetY=-Math.round(this.value);
			pal.bgGroup.bounds=[0,pal.contentOffsetY,viewW,contentSizeY+pal.contentOffsetY];
		};
		pal.scrollBarCore.onChange=pal.scrollBarCore.onChanging;

		var btnSizeX=f_get_btnSizeX();
		var btnSizeY=f_get_btnSizeY();

		var visibleCardCount=0;
		for(var vc=0;vc<folDataList.length;vc++){
			if(f_isFolderItem(folDataList[vc])||f_isBreaker(folDataList[vc])) visibleCardCount++;
		}

		pal.topSettingsBtn=f_addButton(pal,"設定",DEV_btnSize[0],DEV_btnSize[1],"設定を開く");
		pal.topSettingsBtn.onClick=function(){
			var windowObj=f_OptionPnlGUI();
			windowObj.center();
			windowObj.show();
		};

		pal.topFnCheckbox=pal.add("checkbox",undefined,"Fn");
		pal.topFnCheckbox.value=f_get_savedShowFnBtns();
		pal.topFnCheckbox.helpTip="ONで機能ボタンを表示";
		pal.topFnCheckbox.onClick=function(){
			f_saveAndRebuild(false);
		};

		if(visibleCardCount===0){
			pal.emptyInfoText=pal.bgGroup.add("statictext",undefined,"[設定]からフォルダ登録してください");
			pal.emptyInfoText.justify="left";
			f_layoutBtn();
			return;
		}

		for(var i=0;i<folDataList.length;i++){
			var item=folDataList[i];

			if(f_isBreaker(item)){
				var breakerSec={};
					breakerSec.type="break";
					breakerSec.index=i;
					breakerSec.cardBounds=[0,0,0,0];
					breakerSec.cardPanel=pal.bgGroup.add("panel",[0,0,100,CARD_breakSizeY],"");
					breakerSec.cardPanel.text="";
					breakerSec.cardPanel.onDraw=function(){};
					breakerSec.label=f_addLabel(pal.bgGroup,100,"--- 区切り ---");
					breakerSec.btnDown=f_addButton(pal.bgGroup,"",DEV_holdBtnSize[0],DEV_holdBtnSize[1],"下へ移動");
				(function(dataIndex,secObj){
					secObj.btnDown.onClick=function(){
						f_moveDownCard(dataIndex);
					};
				})(i,breakerSec);
				pal.folderSections.push(breakerSec);
				continue;
			}

			var sec={};
				sec.type="folder";
				sec.index=i;
				sec.path=item.path;
				sec.forceBreakAfter=!!item.forceBreakAfter;
				sec.lockButtonsPerRow=item.lockButtonsPerRow||0;
				sec.buttons=[];
				sec.cardBounds=[0,0,0,0];
				sec.cardPanel=pal.bgGroup.add("group",[0,0,100,100]);
				sec.cardPanel.forceBreakAfter=sec.forceBreakAfter;
				sec.cardPanel.onDraw=function(){
					if(!this.forceBreakAfter) return;
					var g=this.graphics;
					var w=this.size.width;
					var h=this.size.height;
					var penBreak=g.newPen(g.PenType.SOLID_COLOR,[.5,.5,1,.5],1);
					g.newPath();
					g.moveTo(w-2,2);
					g.lineTo(w-2,h-3);
					g.strokePath(penBreak);
				};

			sec.label=f_addLabel(pal.bgGroup,100,f_get_folName(sec.path));
			sec.label.helpTip="Ctrl/Cmd+クリックでフォルダ設定";

			//フォルダ単体の設定ダイアログを開き、「反映」「削除」を含む操作提供。
			(function(dataIndex,secObj){
				secObj.label.addEventListener("mousedown",function(){
					if(!f_isCtrlOrCmdPressed()) return;

					var dlg=new Window("dialog",secObj.label.text+" 設定");
						dlg.orientation="column";
						dlg.alignChildren=["fill","top"];
						dlg.spacing=10;
						dlg.margins=15;

					var countPnl=dlg.add("panel",undefined,"ボタンの固定折り返し");
						countPnl.orientation="row";
						countPnl.alignChildren=["left","center"];
						countPnl.spacing=8;
						countPnl.margins=10;

					var countInput=countPnl.add("edittext",undefined,String(folDataList[dataIndex].lockButtonsPerRow||0));
						countInput.characters=6;
						countInput.justify="right";
						countPnl.add("statictext",undefined,"(0=固定なし)");
					f_editNumUpDn(countInput,0);

					var breakPnl=dlg.add("panel",undefined,"フォルダカード折り返し");
					breakPnl.orientation="column";
					breakPnl.alignChildren=["left","top"];
					breakPnl.spacing=6;
					breakPnl.margins=10;

					var breakCheckbox=breakPnl.add("checkbox",undefined,"次カードを強制で下に");
					breakCheckbox.value=!!folDataList[dataIndex].forceBreakAfter;

					var iconPnl=dlg.add("panel",undefined,"ボタンのアイコン設定");
					iconPnl.orientation="column";
					iconPnl.alignChildren=["left","top"];
					iconPnl.spacing=6;
					iconPnl.margins=10;

					var disableFolderPngChk=iconPnl.add("checkbox",undefined,"ボタンにpng画像を使わない");
					disableFolderPngChk.value=!!folDataList[dataIndex].disablePng;

					var applyGr=dlg.add("group");
					applyGr.orientation="row";
					applyGr.alignChildren=["center","center"];
					applyGr.alignment=["center","top"];
					applyGr.spacing=8;

					var applyBtn=f_addButton(applyGr,"反映",80,24,"設定を反映");
					applyBtn.onClick=function(){
						var raw=String(countInput.text).replace(/^\s+|\s+$/g,"");
						var value=parseInt(raw,10);
						if(isNaN(value)||value<0){
							alert("0以上の数値を入力してください");
							return;
						}
						folDataList[dataIndex].lockButtonsPerRow=value;
						folDataList[dataIndex].forceBreakAfter=!!breakCheckbox.value;
						folDataList[dataIndex].disablePng=!!disableFolderPngChk.value;
						f_saveAndRebuild(false);
						dlg.close();
					};

					var deleteBtn=f_addButton(applyGr,"カード削除",80,24,"このフォルダを削除");
					deleteBtn.onClick=function(){
						var ok=confirm(""+secObj.label.text+"」を削除しますか?",true,scNameShort);
						if(!ok) return;
						folDataList.splice(dataIndex,1);
						f_saveAndRebuild(false);
						dlg.close();
					};

					dlg.center();
					dlg.show();
				});
			})(i,sec);

			sec.btnDelete=f_addButton(pal.bgGroup,"×",DEV_holdBtnSize[0],DEV_holdBtnSize[1],"削除");
			sec.btnUp=f_addButton(pal.bgGroup,"",DEV_holdBtnSize[0],DEV_holdBtnSize[1],"上へ移動");
			sec.btnDown=f_addButton(pal.bgGroup,"",DEV_holdBtnSize[0],DEV_holdBtnSize[1],"下へ移動");

			(function(dataIndex,secObj){
				secObj.btnDelete.onClick=function(){
					var ok=confirm(secObj.label.text+" を削除しますか?",true,scNameShort);
					if(!ok) return;
					folDataList.splice(dataIndex,1);
					f_saveAndRebuild(false);
				};
				secObj.btnUp.onClick=function(){
					f_moveUpCard(dataIndex);
				};
				secObj.btnDown.onClick=function(){
					f_moveDownCard(dataIndex);
				};
			})(i,sec);

			var folder=new Folder(sec.path);
			var files=f_get_scriptFiles(folder);

			if(!folder.exists&&showAlert){
				alert("フォルダが見つかりません:\n"+folder.fsName);
			}

			for(var j=0;j<files.length;j++){
				sec.buttons.push(f_addScriptButton(pal.bgGroup,files[j],btnSizeX,btnSizeY,sec.path));
			}

			pal.folderSections.push(sec);
		}

		f_layoutBtn();
	}

	// ボタン流し込み
	function f_layoutBtn(){
		if(!pal||!pal.viewareaGroup||!pal.bgGroup||!pal.scrollBarCore) return;

		var panelW=pal.size.width;
		var panelSizeY=pal.size.height;
		if(panelW<=0) panelW=260;
		if(panelSizeY<=0) panelSizeY=200;

		var viewW=panelW-DEV_scrollSizeX;
		if(viewW<60) viewW=60;

		var viewH=panelSizeY-DEV_footerAreaH;
		if(viewH<40) viewH=40;

		var btnSizeX=f_get_btnSizeX();
		var btnSizeY=f_get_btnSizeY();

		var left=DEV_pnlMargin[0];
		var top=DEV_pnlMargin[1];
		var rowMaxH=0;
		var contentBottom=0;

		pal.viewareaGroup.bounds=[0,0,viewW,viewH];
		pal.scrollBarCore.bounds=[viewW,0,viewW+DEV_scrollSizeX,viewH];

		if(pal.emptyInfoText) pal.emptyInfoText.visible=(pal.folderSections.length===0);

		// 全消し状態の専用表示
		if(pal.folderSections.length===0){
			f_layoutFooter(panelSizeY);

			if(pal.emptyInfoText){
				pal.emptyInfoText.bounds=[
					DEV_pnlMargin[0],
					DEV_pnlMargin[1],
					DEV_pnlMargin[0]+180,
					DEV_pnlMargin[1]+DEV_labelSizeY
				];
			}

			pal.contentOffsetY=0;
			pal.bgGroup.bounds=[0,0,viewW,viewH];

			pal.scrollBarCore.minvalue=0;
			pal.scrollBarCore.maxvalue=0;
			pal.scrollBarCore.value=0;
			pal.scrollBarCore.visible=false;

			if(!(pal instanceof Panel)) pal.layout.layout(true);
			return;
		}

		// フォルダカード整列計算
		for(var i=0;i<pal.folderSections.length;i++){
			var sec=pal.folderSections[i];
			if(!sec||!sec.cardPanel) continue;

			var cardSizeX;
			var cardH;

			if(sec.type==="break"){
				cardSizeX=Math.max(CARD_minW,viewW-DEV_pnlMargin[0]-DEV_pnlMargin[0]);
				cardH=CARD_breakSizeY;
			}else{
				cardSizeX=f_cardMaxJustSizeX(sec,btnSizeX);
				if(cardSizeX>viewW-DEV_pnlMargin[0]-DEV_pnlMargin[0]){
					cardSizeX=Math.max(f_cardMinSizeX(sec,btnSizeX),viewW-DEV_pnlMargin[0]-DEV_pnlMargin[0]);
				}
				cardH=f_byWidthCardSizeY(sec,cardSizeX,btnSizeX,btnSizeY);
			}

			sec.cardSizeX=cardSizeX;
			sec.cardHeight=cardH;
		}

		// フォルダカードの配置
		for(var j=0;j<pal.folderSections.length;j++){
			var sec2=pal.folderSections[j];
			if(!sec2||!sec2.cardPanel) continue;

			//「↑」「↓」ボタンは端では無効化
			var lastIndex=pal.folderSections.length-1;
			if(sec2.type==="break"){
				sec2.btnDown.enabled=(j<lastIndex);
			}else{
				sec2.btnUp.enabled=(j>0);
				sec2.btnDown.enabled=(j<lastIndex);
			}

			var cardSizeX2=sec2.cardSizeX;
			var cardSizeY2=sec2.cardHeight;

			var topNeedSizeX=cardSizeX2;
			if(sec2.type!=="break"){
				topNeedSizeX=f_cardMinSizeX(sec2,btnSizeX);
			}

			if(left+topNeedSizeX>viewW-DEV_pnlMargin[0]&&left>DEV_pnlMargin[0]){
				left=DEV_pnlMargin[0];
				top+=rowMaxH+DEV_btnMargin[1];
				rowMaxH=0;
			}

			var placeW=cardSizeX2;

			if(sec2.type!=="break"){
				var remainW=(viewW-DEV_pnlMargin[0])-left;
				var minW=f_cardMinSizeX(sec2,btnSizeX);

				if(remainW<minW){
					remainW=minW;
				}

				if(placeW>remainW){
					placeW=remainW;
				}

				cardSizeY2=f_byWidthCardSizeY(sec2,placeW,btnSizeX,btnSizeY);
			}

			sec2.cardSizeX=placeW;
			sec2.cardHeight=cardSizeY2;
			sec2.cardBounds=[left,top,left+placeW,top+cardSizeY2];

			if(sec2.type==="break"){
				f_layoutBreaker(sec2,left,top,placeW);
				left=DEV_pnlMargin[0];
				top+=cardSizeY2+DEV_btnMargin[1];
				rowMaxH=0;
				if(top>contentBottom) contentBottom=top;
				continue;
			}else{
				f_layoutFolderCard(sec2,left,top,placeW,btnSizeX,btnSizeY);
			}

			if(cardSizeY2>rowMaxH) rowMaxH=cardSizeY2;
			if(top+cardSizeY2>contentBottom) contentBottom=top+cardSizeY2;

			left+=cardSizeX2+f_get_marginR();

			if(sec2.forceBreakAfter){
				left=DEV_pnlMargin[0];
				top+=rowMaxH+DEV_btnMargin[1];
				rowMaxH=0;
			}
		}

		var contentSizeY=contentBottom+DEV_pnlMargin[1];
		if(contentSizeY<0||!isFinite(contentSizeY)) contentSizeY=0;
		pal.contentSizeYeight=contentSizeY;

		var maxScroll=contentSizeY-viewH;
		if(maxScroll<0||!isFinite(maxScroll)) maxScroll=0;

		if(!isFinite(pal.scrollBarCore.value)) pal.scrollBarCore.value=0;
		if(pal.scrollBarCore.value<0) pal.scrollBarCore.value=0;
		if(pal.scrollBarCore.value>maxScroll) pal.scrollBarCore.value=maxScroll;

		pal.contentOffsetY=-Math.round(pal.scrollBarCore.value);
		pal.bgGroup.bounds=[0,pal.contentOffsetY,viewW,pal.contentSizeYeight+pal.contentOffsetY];

		pal.scrollBarCore.minvalue=0;
		pal.scrollBarCore.maxvalue=maxScroll;
		pal.scrollBarCore.visible=(maxScroll>0);

		f_layoutFooter(panelSizeY);

		if(!(pal instanceof Panel)) pal.layout.layout(true);
	}

	//==========
	// 保存系
	//==========

	function f_get_preset(baseKey){
		return baseKey;
	}

	// 設定次々保存
	function f_saveState(){
		if(settingsSuspendSave) return;
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_items),f_serialize(folDataList));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_btnNoPngMap),f_serializeBoolMap(btnNoPngMap));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_btn[0]),String(f_get_btnSizeX()));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_btn[1]),String(f_get_btnSizeY()));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_marginR),String(f_get_marginR()));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_minLabelW),String(f_get_minLabelSizeX()));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_btnLabelMap),f_serializeBtnLabelMap(btnLabelMap));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_showFnBtns),String(f_get_showFnBtns()));
		if(settingsNewPathInput){
			app.settings.saveSetting(scNameShort,f_get_preset(OP_label_newPath),String(settingsNewPathInput.text));
		}else{
			if(app.settings.haveSetting(scNameShort,f_get_preset(OP_label_newPath))){
				app.settings.saveSetting(scNameShort,f_get_preset(OP_label_newPath),app.settings.getSetting(scNameShort,f_get_preset(OP_label_newPath)));
			}else{
				app.settings.saveSetting(scNameShort,f_get_preset(OP_label_newPath),baseFolder.fsName);
			}
		}
		app.preferences.saveToDisk();
	}

	// 初期化として設定に初期値を上書き
	function f_reset_defState(){
		folDataList=[f_makeFolderItem(baseFolder.fsName,false)];
		btnLabelMap={};
		btnNoPngMap={};
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_items),"");
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_btn[0]),String(DEF_btnSize[0]));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_btn[1]),String(DEF_btnSize[1]));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_newPath),baseFolder.fsName);
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_marginR),String(DEF_marginR));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_minLabelW),String(DEF_minLabelSizeX));
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_btnLabelMap),"");
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_showFnBtns),"false");
		app.settings.saveSetting(scNameShort,f_get_preset(OP_label_btnNoPngMap),"");
		app.preferences.saveToDisk();
		if(settingsNewPathInput) settingsNewPathInput.text=baseFolder.fsName;
		if(settingsWidthInput) settingsWidthInput.text=String(DEF_btnSize[0]);
		if(settingsHeightInput) settingsHeightInput.text=String(DEF_btnSize[1]);
		if(settingsRightMarginInput) settingsRightMarginInput.text=String(DEF_marginR);
		if(settingsMinLabelWidthInput) settingsMinLabelWidthInput.text=String(DEF_minLabelSizeX);
	}

	// つづきから遊ぶ
	function f_loadState(){
		folDataList=[];
		btnLabelMap={};
		btnNoPngMap={};
		if(app.settings.haveSetting(scNameShort,f_get_preset(OP_label_items))){
			folDataList=f_deserialize(app.settings.getSetting(scNameShort,f_get_preset(OP_label_items)));
		}
		if(app.settings.haveSetting(scNameShort,f_get_preset(OP_label_btnLabelMap))){
			btnLabelMap=f_deserializeBtnLabelMap(app.settings.getSetting(scNameShort,f_get_preset(OP_label_btnLabelMap)));
		}
		if(app.settings.haveSetting(scNameShort,f_get_preset(OP_label_btnNoPngMap))){
			btnNoPngMap=f_deserializeBoolMap(app.settings.getSetting(scNameShort,f_get_preset(OP_label_btnNoPngMap)));
		}else{
			btnNoPngMap={};
		}
		if(folDataList.length===0&&app.settings.haveSetting(scNameShort,"paths")){
			var legacyRaw=app.settings.getSetting(scNameShort,"paths");
			if(legacyRaw!==""){
				var legacyArr=legacyRaw.split("||");
				for(var i=0;i<legacyArr.length;i++){
					if(legacyArr[i]!=="") folDataList.push(f_makeFolderItem(f_decodeSettingVal(legacyArr[i]),false));
				}
			}
		}
		return true;
	}

	// 保存データがなくてもエラーではなくデフォルトを読み込む
	function f_getSavedInt(settingKey,defaultValue,minValue){
		if(app.settings.haveSetting(scNameShort,f_get_preset(settingKey))){
			var value=parseInt(app.settings.getSetting(scNameShort,f_get_preset(settingKey)),10);
			if(!isNaN(value)&&value>=minValue) return value;
		}
		return defaultValue;
	}

	// こまごまとセーブデータから読み込みするパーツ
	function f_get_savedBtnSizeX(){
		return f_getSavedInt(OP_label_btn[0],DEF_btnSize[0],1);
	}

	function f_get_savedBtnSizeY(){
		return f_getSavedInt(OP_label_btn[1],DEF_btnSize[1],1);
	}

	function f_get_savedMarginR(){
		return f_getSavedInt(OP_label_marginR,DEF_marginR,0);
	}

	function f_get_savedMinLabelSizeX(){
		return f_getSavedInt(OP_label_minLabelW,DEF_minLabelSizeX,0);
	}

	function f_get_savedNewPath(){
		if(app.settings.haveSetting(scNameShort,f_get_preset(OP_label_newPath))) return app.settings.getSetting(scNameShort,f_get_preset(OP_label_newPath));
		return baseFolder.fsName;
	}

	function f_get_settingInt(inputObj,settingKey,defaultValue,minValue){
		var value=defaultValue;
		if(inputObj&&inputObj.text!==""){
			value=parseInt(inputObj.text,10);
		}else if(app.settings.haveSetting(scNameShort,f_get_preset(settingKey))){
			value=parseInt(app.settings.getSetting(scNameShort,f_get_preset(settingKey)),10);
		}
		if(isNaN(value)||value<minValue) value=defaultValue;
		return value;
	}

	// 今から使う数値。入力している数値→未入力なら保存済み→なければ初期値
	function f_get_btnSizeX(){
		return f_get_settingInt(settingsWidthInput,OP_label_btn[0],DEF_btnSize[0],1);
	}

	function f_get_btnSizeY(){
		return f_get_settingInt(settingsHeightInput,OP_label_btn[1],DEF_btnSize[1],1);
	}

	function f_get_marginR(){
		return f_get_settingInt(settingsRightMarginInput,OP_label_marginR,DEF_marginR,0);
	}

	function f_get_minLabelSizeX(){
		return f_get_settingInt(settingsMinLabelWidthInput,OP_label_minLabelW,DEF_minLabelSizeX,0);
	}

	// フォルダけ?
	function f_isFolderItem(item){
		return !item||!item.type||item.type==="folder";
	}

	// そのボタンにはpng不要け?
	function f_isButtonNoPng(fileObj){
		var key=String(fileObj.fsName);
		return !!btnNoPngMap[key];
	}

	// そのフォルダ内のボタンにはpng不要け?
	function f_isFolderNoPng(folderPath){
		for(var i=0;i<folDataList.length;i++){
			var item=folDataList[i];
			if(!f_isFolderItem(item)) continue;
			if(String(item.path)===String(folderPath)){
				return !!item.disablePng;
			}
		}
		return false;
	}

	// 登録フォルダから.jsxか.jsxbinファイルだけ検知
	function f_get_scriptFiles(folder){
		if(!folder||!folder.exists) return [];
		var allItems=folder.getFiles();
		var result=[];
		for(var k=0;k<allItems.length;k++){
			var item=allItems[k];
			if(!(item instanceof File)) continue;
			var name=item.name;
			var dot=name.lastIndexOf(".");
			if(dot<0) continue;
			var ext=name.substring(dot).toLowerCase();
			if(ext===".jsx"||ext===".jsxbin") result.push(item);
		}
		return result;
	}

	// ボタンそのものの描画。
	function f_addScriptButton(parent,file,btnSizeX,btnSizeY,folderPath){
		var forceText=f_isButtonNoPng(file)||f_isFolderNoPng(folderPath);
		var iconFile=forceText?null:f_get_iconFile(file);
		var button;
		var displayLabel=f_nowBtnLabel(file);

		if(iconFile){
			button=parent.add("iconbutton",[0,0,btnSizeX,btnSizeY],iconFile,{style:"toolbutton"});
		}else{
			button=parent.add("button",[0,0,btnSizeX,btnSizeY],displayLabel,{name:file.name});
		}

		button.scriptFile=file.fsName;
		button.folderPath=folderPath;
		button.helpTip=f_pathDecodeName(file)+"\n"+file.fsName;

		button.addEventListener("mousedown",function(){
			if(!f_isCtrlOrCmdPressed()) return;
			f_btnLabelPnlGUI(file);
		});

		button.onClick=function(){
			if(f_isCtrlOrCmdPressed()) return;
			var targetFile=new File(this.scriptFile);
			if(!targetFile.exists){
				alert(this.scriptFile);
				return;
			}
			$.evalFile(targetFile);
		};

		return button;
	}


	//==========
	// フォルダ/アイテム操作
	//==========

	// スクリプトファイルと同名の.png検索
	function f_get_iconFile(scriptFileObj){
		var iconPath=scriptFileObj.fsName.replace(/\.(jsx|jsxbin)$/i,".png");
		var iconFile=new File(iconPath);
		if(iconFile.exists) return iconFile;
		return null;
	}

	// フォルダデータ格納用の箱
	function f_makeFolderItem(path,forceBreakAfter,disablePng){
		return {
			type:"folder",
			path:String(path),
			forceBreakAfter:!!forceBreakAfter,
			lockButtonsPerRow:0,
			disablePng:!!disablePng
		};
	}

	// 折り返しか?
	function f_isBreaker(item){
		return item&&item.type==="break";
	}

	// ファイルパスから末尾のフォルダ名だけちゃんと取得
	function f_get_folName(pathText){
		if(!pathText||pathText==="") return "(未指定)";
		var text=String(pathText);
		try{
			text=Folder.decode(text);
		}catch(e){
			try{
				text=decodeURI(text);
			}catch(e2){}
		}
		text=text.replace(/\\/g,"/");
		text=text.replace(/\/+$/,"");
		text=text.replace(/\s+$/,"");
		var parts=text.split("/");
		if(parts.length===0) return text;
		var last=parts[parts.length-1];
		if(!last||last==="") return text;
		return last;
	}

	function f_encodeSettingVal(text){
		return encodeURIComponent(String(text));
	}

	function f_decodeSettingVal(text){
		try{return decodeURIComponent(String(text));}catch(e){return String(text);}
	}

	// 設定保存用のテキストに変換
	function f_serialize(items){
		var result=[];
		for(var i=0;i<items.length;i++){
			var item=items[i];
			if(f_isBreaker(item)){
				result.push("B");
			}else{
				result.push("F:"+f_encodeSettingVal(item.path)+":"+(item.forceBreakAfter?"1":"0")+":"+(item.lockButtonsPerRow||0)+":"+(item.disablePng?"1":"0"));
			}
		}
		return result.join("||");
	}

	// 設定保存用のテキストから状態復元用に逆変換
	function f_deserialize(raw){
		var result=[];
		if(!raw||raw==="") return result;

		var parts=raw.split("||");
		for(var i=0;i<parts.length;i++){
			var part=parts[i];

			if(part==="B"){
				result.push({type:"break"});
				continue;
			}

			if(part.indexOf("F:")===0){
				var body=part.substring(2);
				var values=body.split(":");

				var pathText=values[0];
				var forceBreak=values[1]==="1";
				var lockPerRow=parseInt(values[2],10);
				var disablePng=values[3]==="1";

				if(isNaN(lockPerRow)||lockPerRow<0) lockPerRow=0;

				var itemObj=f_makeFolderItem(f_decodeSettingVal(pathText),forceBreak,disablePng);
				itemObj.lockButtonsPerRow=lockPerRow;
				result.push(itemObj);
				continue;
			}

			if(part!==""){
				result.push(f_makeFolderItem(f_decodeSettingVal(part),false,false));
			}
		}

		return result;
	}

	// フォルダカードを1つ上と入れ替え
	function f_moveUpCard(dataIndex){
		f_moveCard(dataIndex,-1);
	}

	// フォルダカードを1つ下と入れ替え
	function f_moveDownCard(dataIndex){
		f_moveCard(dataIndex,1);
	}

	// フォルダカードを入れ替える共通処理部分
	function f_moveCard(dataIndex,moveDir){
		var targetIndex=dataIndex+moveDir;
		if(dataIndex<0||dataIndex>=folDataList.length) return;
		if(targetIndex<0||targetIndex>=folDataList.length) return;
		var temp=folDataList[targetIndex];
		folDataList[targetIndex]=folDataList[dataIndex];
		folDataList[dataIndex]=temp;
		f_saveAndRebuild(false);
	}


	//==========
	// 準備したやつ全部表示
	//==========

	pal.onShow=function(){
		f_loadState();
		f_rebuildUI(false);
		if(pal instanceof Window) pal.center();
	};

	pal.onResizing=pal.onResize=function(){
		f_layoutBtn();
	};

	f_loadState();
	f_rebuildUI(false);

	if(pal instanceof Window){
		pal.show();
	}else{
		f_layoutBtn();
	}
})(this);

関数も多く、機能ごとにひとつずつ張っていくのがアレなので、今回はスクリプト内に多めにコメントを書きました。

主にこちらを手がかりにしていただく方式を取ります。

使用上の注意

縦のスクロールバーに関して、マウスホイールが右端のスクロールバーのエリア上でしか反応しません。パネル内にマウスカーソルがあればスクロールして欲しいですが、Script UIの仕様で無理でした。

起動中にPC内で.jsxファイルを移動、追加、削除した際は、プリセット切り替え([Pro版])や、[Fn]クリックなど、あらゆるタイミングで再描画しているのでなにか表示を変えてもらえればOKなはずです。たまにスクロールバーが表示されないなど計算の順番で復帰しないこともあるので、一番効果的なのはパネルを閉じて再度起動が不具合解消にはいい方法かと思います。

考えたこと

ボタンのための.png画像を準備するのが一番面倒くさいところです。私は苦ではなく、むしろ使用するツールは自分好みにカスタマイズできる環境にするためなら時間を支払えます。面倒くさがりで時間を短縮したいのに、結果、効率化のために長時間使うことが多いです。でもそれでいいのです。

まとめ

After Effectsで便利なスクリプトランチャー、いくつかあります。既にあります。まず。

bryful氏の「簡単メニュー.jsx」、老舗の「LauchPad.jsx」が現役で健在です。

これに挑もうと思ったのです。「KBar」はスクリプト以外も登録できるため、このあたりの無料ツールで物足りなくなったり魅力を感じたりしたら購入も検討しましょう。

ツールのデザインは進化しますが、機能性としては大きな核心を必要としないほど、20年前で完成されていることがわかります。

せめて作者である彼らに近づけたらと、ランチャーの制作に挑戦してみました、というのが今回の本筋です。

プログラミング的な考え方を筋トレしたいのと、うまく行けばいいツールが手元に残ります。支払うコストは時間だけで、デメリットが見当たりませんでした。

実際にAfter Effectsを使う側として、彼らのスクリプトに着想を得て機能追加をする過程で、改悪ではなく改善を意識しました。オプション機能へのアクセスが単純なクリックだと、特にフォルダ設定パネルを開くためのフォルダ名クリックは誤クリックの可能性があり、ctrl+クリックに回避したり、ソート用のボタンは普段は隠れて欲しい、など、アイディアは留まることを知らないのでゴタつかないよう試行錯誤を重ねました。

そして毎回ですが、スクリプトを平文で公開しているので、After Effects用のUI設計のクセに苦労しながら、どう実装しているかの一例として学びに役立てていただければと思います。

ダウンロード

速度に応じてシェイプの色を変える前のページ

【限定配布】プロパティをブクマするスクリプト「bkmaProp.jsxbin」次のページ

ピックアップ記事

  1. なぜ?After Effectsの操作を「スクリプト」で効率化

  2. YouTubeで一時停止中のコントローラーを非表示にするブックマークレット

  3. フリーランスの開業届提出は開業freeeでとにかく簡単に

  4. なぜ?After Effectsのレイヤーをエクスプレッションで効率化

  5. amazonのスポンサー商品(広告)を非表示にするブックマークレット「amazO…

関連記事

  1. スクリプト

    【限定配布】プロパティをブクマするスクリプト「bkmaProp.jsxbin」

    プロパティをブックマークしておいて、あとから選択状態を復帰させるスクリ…

  2. スクリプト

    使用エフェクト一覧をテキストファイル出力するスクリプト「exportFxName.jsx」

    使用エフェクト一覧を、開いているaepファイルと同じフォルダにテキスト…

  3. スクリプト

    レイヤー名かフッテージ名末尾に連番を振るスクリプト「sequentialNumRenamer.jsx…

    連番管理したいレイヤーかフッテージを選択した順に連番でリネームするスク…

  4. スクリプト

    H型の定規シェイプを作成するスクリプト「Shape-RulerH.jsx」

    Hの形の定規シェイプレイヤーと追従するラベルのテキストレイヤーを作成す…

  5. スクリプト

    ランダムに配置するスクリプト「posUn-Align.jsx」

    複数のレイヤーを放射状に整列させるエクスプレッションを仕込むスクリプト…

コメント

  1. この記事へのコメントはありません。

  1. この記事へのトラックバックはありません。

CAPTCHA


PAGE TOP