MacVim のインライン変換で、変換対象の文節の範囲とそれ以外との表示に差がない問題を修正する

  • 環境:MacVim release 174 + ATOK (2022-12-28の最新版)
  • 問題点:インラインIM変換対象の文節のアンダーラインが、変換対象でない部分との表示の差がなく、文節長を調節する際などに不便。
    • たとえば下の画像では「吾輩は」までが変換対象の文節になっているのだが、「猫である」と表示に差がない。

MacVim release 174 での変換中の文字列

MacVimとMacVim-KaoriYaの差分 を参考に、問題に関与していると思われる部分を取り込んだパッチを作成した(ページの下部に diff を掲載する)。パッチを当てると、下の画像のように変換対象文字列が他の部分と区別できるようになった。

パッチを当てた後の変換中の文字列

パッチ:

diff --git a/src/MacVim/MMBackend.m b/src/MacVim/MMBackend.m
index 3c3dcaddd..d9d35d8a2 100644
--- a/src/MacVim/MMBackend.m
+++ b/src/MacVim/MMBackend.m
@@ -59,7 +59,7 @@ static id evalExprCocoa(NSString * expr, NSString ** errstr);
 void im_preedit_start_macvim();
 void im_preedit_end_macvim();
 void im_preedit_abandon_macvim();
-void im_preedit_changed_macvim(char *preedit_string, int cursor_index);
+void im_preedit_changed_macvim(char *preedit_string, int start_index, int cursor_index);
 
 enum {
     MMBlinkStateNone = 0,
@@ -3290,21 +3290,22 @@ extern GuiFont gui_mch_retain_font(GuiFont font);
 - (void)handleMarkedText:(NSData *)data
 {
     const void *bytes = [data bytes];
+    unsigned textlen = *((unsigned*)bytes);  bytes += sizeof(unsigned);
     int32_t pos = *((int32_t*)bytes);  bytes += sizeof(int32_t);
     unsigned len = *((unsigned*)bytes);  bytes += sizeof(unsigned);
     char *chars = (char *)bytes;
 
-    ASLogDebug(@"pos=%d len=%d chars=%s", pos, len, chars);
+    ASLogDebug(@"textlen=%d pos=%d len=%d chars=%s", textlen, pos, len, chars);
 
     if (pos < 0) {
         im_preedit_abandon_macvim();
-    } else if (len == 0) {
+    } else if (textlen == 0) {
    im_preedit_end_macvim();
     } else {
         if (!preedit_get_status())
             im_preedit_start_macvim();
 
-  im_preedit_changed_macvim(chars, pos);
+   im_preedit_changed_macvim(chars, pos, pos + len);
     }
 }
 
diff --git a/src/MacVim/MMTextViewHelper.m b/src/MacVim/MMTextViewHelper.m
index 43b718522..63ab3f594 100644
--- a/src/MacVim/MMTextViewHelper.m
+++ b/src/MacVim/MMTextViewHelper.m
@@ -42,7 +42,7 @@ static float MMDragAreaSize = 73.0f;
 - (void)setCursor;
 - (NSRect)trackingRect;
 - (BOOL)inputManagerHandleMouseEvent:(NSEvent *)event;
-- (void)sendMarkedText:(NSString *)text position:(int32_t)pos;
+- (void)sendMarkedText:(NSString *)text position:(int32_t)pos length:(unsigned)len;
 - (void)abandonMarkedText;
 - (void)sendGestureEvent:(int)gesture flags:(int)flags;
 @end
@@ -224,7 +224,7 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
 - (void)insertText:(id)string
 {
     if ([self hasMarkedText]) {
-        [self sendMarkedText:nil position:0];
+        [self sendMarkedText:nil position:0 length:0];
 
         // NOTE: If this call is left out then the marked text isn't properly
         // erased when Return is used to accept the text.
@@ -335,7 +335,7 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
     if ([self hasMarkedText]) {
         // We must clear the marked text since the cursor may move if the
         // marked text moves outside the view as a result of scrolling.
-        [self sendMarkedText:nil position:0];
+        [self sendMarkedText:nil position:0 length:0];
         [self unmarkText];
         [[NSTextInputContext currentInputContext] discardMarkedText];
     }
@@ -659,7 +659,7 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
             imRange = range;
         }
 
-        [self sendMarkedText:text position:range.location];
+        [self sendMarkedText:text position:range.location length:range.length];
         return;
     }
 
@@ -1129,19 +1129,20 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
     return NO;
 }
 
-- (void)sendMarkedText:(NSString *)text position:(int32_t)pos
+- (void)sendMarkedText:(NSString *)text position:(int32_t)pos length:(unsigned)len
 {
     if (![self useInlineIm])
         return;
 
     NSMutableData *data = [NSMutableData data];
-    unsigned len = text == nil ? 0
+    unsigned textlen = text == nil ? 0
                     : [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
 
+    [data appendBytes:&textlen length:sizeof(unsigned)];
     [data appendBytes:&pos length:sizeof(int32_t)];
     [data appendBytes:&len length:sizeof(unsigned)];
-    if (len > 0) {
-        [data appendBytes:[text UTF8String] length:len];
+    if (textlen > 0) {
+        [data appendBytes:[text UTF8String] length:textlen];
         [data appendBytes:"\x00" length:1];
     }
 
@@ -1155,7 +1156,7 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
     // Send an empty marked text message with position set to -1 to indicate
     // that the marked text should be abandoned.  (If pos is set to 0 Vim will
     // send backspace sequences to delete the old marked text.)
-    [self sendMarkedText:nil position:-1];
+    [self sendMarkedText:nil position:-1 length:0];
     [[NSTextInputContext currentInputContext] discardMarkedText];
 }
 
diff --git a/src/gui_xim.c b/src/gui_xim.c
index ce2e79c95..fdcf29aaa 100644
--- a/src/gui_xim.c
+++ b/src/gui_xim.c
@@ -184,6 +184,7 @@ init_preedit_start_col(void)
 
 static int im_is_active           = FALSE; // IM is enabled for current mode
 static int preedit_is_active   = FALSE;
+static int im_preedit_start    = 0;    /* start offset in characters        */
 static int im_preedit_cursor   = 0;    // cursor offset in characters
 static int im_preedit_trailing = 0;    // number of characters after cursor
 
@@ -715,7 +716,7 @@ im_preedit_abandon_macvim()
 im_preedit_changed_cb(GtkIMContext *context, gpointer data UNUSED)
 # else
     void
-im_preedit_changed_macvim(char *preedit_string, int cursor_index)
+im_preedit_changed_macvim(char *preedit_string, int start_index, int cursor_index)
 # endif
 {
 # ifndef FEAT_GUI_MACVIM
@@ -736,6 +737,8 @@ im_preedit_changed_macvim(char *preedit_string, int cursor_index)
    gtk_im_context_get_preedit_string(context,
                      &preedit_string, NULL,
                      NULL);
+# else
+    im_preedit_start = start_index;
 # endif
 
 #ifdef XIM_DEBUG
@@ -924,7 +927,10 @@ im_get_feedback_attr(int col UNUSED)
 
     return char_attr;
 # else
-    return HL_UNDERLINE;
+    if (col >= im_preedit_start && col < im_preedit_cursor)
+   return HL_UNDERCURL;
+    else
+   return HL_UNDERLINE;
 # endif
 }

Excel で、複数の条件に該当するセルのデータを横方向に抽出する

Excel を用いて、複数の条件をもとにセルのデータを横方向に抽出する必要が生じた。

要は、Sheet1 にある次のような表を

f:id:KainokiKaede:20211016121511p:plain
元表

公開フラグが立っているもののみ、次のように Sheet2 に整理(?)したい。

f:id:KainokiKaede:20211016121529p:plain
抽出表

Yahoo! 知恵袋 などには COUNTIF, MATCH 等を使って実現する方法が載っているが、上の表のように公開フラグなどの条件が入ってくると扱いづらそうに感じた。

最近の Office には FILTER, UNIQUE などの配列関数が実装された ようで、これらを用いれば複数条件に該当する値のリストを取得できる。

まず Sheet2 の A2 に次の式を入力する。(Excel の最大行数は 1048576 行である

=UNIQUE(Sheet1!$A$2:$A$1048576)

これで Sheet1 の A 列のうち重複を含まないものが Sheet2 のA列に表示される。

続いて Sheet2 の B2 に次の式を入力する。

=TRANSPOSE(FILTER(Sheet1!$C$2:$C$1048576, (Sheet1!$A$2:$A$1048576=$A2) * (Sheet1!$B$2:$B$1048576="公開")))

ここでは、まず FILTER 関数で第2引数の条件に合う行のC列を取得したうえで、TRANSPOSE で横向きに並べている。

最後にB2からB5までオートフィルすれば完成である。

式2つで表が作成できてオトクだと思うし既存の手法よりも式の見通しがよいと思う。難点は新しい Office が入っていないとこれらの関数が使えず、Excel のみでデータを弄る必要があるときには往々にして新しい Office は入っていないという点か。

Mac の PlatformIO IDE で #include エラーが出る問題を解決する

MacVSCode で PlatformIO を使って M5Stack の開発をしていると、#include <M5EPD.h> の行に

#include エラーが検出されました。includePath を更新してください。...
ソース ファイルを開けません "AvailabilityMacros.h" (dependency of "M5EPD.h")

というエラーが出現する。英語版では

#include errors detected. Please update your includePath. ...
cannot open source file "AvailabilityMacros.h"

であろう。

この状態でもプロジェクトのコンパイルは可能だが、VSCode が常にエラーの存在を訴えてきて annoying である。

このエラーが発生する原因は、最近の macOS には /usr/include が存在しないのにもかかわらず VSCode (もしくは PlatformIO もしくは C/C++ extension)が /usr/include を読みに行っているからだと思われる。

% xcrun --show-sdk-path
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk

で正しい path を確認してやり、platformio.inibuild_flags 内に次の行を追加すればエラーは消える。

build_flags = 
    ; ...
    -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/**

M5Paper で BLE Current Time Service を利用して Real Time Clock を設定する

M5Stack シリーズの一つ M5Paper には real time clock (RTC) BM8563 が搭載されており、M5.RTC ライブラリを利用して時刻の書き込み・読み込みを行うことができる。

この RTC の時刻を合わせる方法として、Wi-Fi を用いて適当な NTP サーバーへ接続し現在時刻を取得する例が Web 上に多く存在するが、Web 接続可能な Wi-Fi が常に使用可能とは限らない。

M5Paper には Bluetooth 機能も付属している。Bluetooth 規格の一種である Bluetooth Low Energy (BLE) には Current Time Service (CTS, リンク先はPDF) という現在時刻を取得できるサービスが存在する。Accessory Design Guidelines for Apple Devices (PDF) には、iOS 7.0 以降は Current Time Service が利用可能であると記載されている。すなわち、iPhone の時計が携帯電話回線経由で常にほぼ正確な時刻に合わせられていることを前提とするならば、M5Paper を BLE で iPhone に接続し、Current Time Service を用いて時刻を取得することで、M5Paper を Wi-Fi に接続することなしにほぼ正確な時刻を取得することが可能となる。

M5StickC を用いて Current Time Service より現在時刻を取得する試みはすでに存在する が、この手法では BLE デバイス検索アプリを別途用意する必要があり、また接続時に iPhone からのペアリング操作が毎回必要であり手間が大きかった。

今回、接続のための専用アプリが不要で、かつ一度ペアリングした以降は再接続時も iPhone 側での操作が不要である ESP32 用 Current Time Service クライアントライブラリ ddlsmurf/ESP32-ANCS-AMS-Notifications を見つけたので、これを紹介するとともに、M5Paper での利用テストプログラムを制作したので公開する。

早速であるが以下がそのプログラムである。公式の RTC 利用例前述のライブラリ を合体させた。初回起動時にはペアリング待ち状態となり、その間に iPhone 側の設定アプリからペアリングを行う。一度ペアリングされた以降は再度同一のプログラムを実行すれば自動で M5Paper が iPhone へペアリングされ時刻情報が取得される。ペアリング情報はおそらく内部的に利用されている ESP32 のライブラリがうまく管理してくれており、スケッチを書き換えても維持されるようだ。

#include <M5EPD.h>

//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//+++++ Get Current Time from BLE Current Time Service +++++
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#include <esp32notifications.h>

void setupTime(int year, int month, int date, int hour, int minute, int second);

void checkCTSAndUpdateRTC() {
  BLENotifications notifications;
  notifications.begin("CTS_test");  // BLE begin.
  while (1) {
    if (notifications.clientCTS->ready()) break;
    delay(100);
  }
  ble_cts_current_time_char_t *time = notifications.clientCTS->readTime();
  if (time->exact_time_256.seconds < 59) {  // Inclimentating time is complicated ... so forget about seconds = 59.
    delay(int(1000. - float(time->exact_time_256.fractions256) * 1000. / 256.));  // wait until next second.
    time->exact_time_256.seconds += 1;  // incliment.
  }
  setupTime(time->exact_time_256.year, time->exact_time_256.month, time->exact_time_256.day,
            time->exact_time_256.hours, time->exact_time_256.minutes, time->exact_time_256.seconds);
  notifications.stop();  // BLE stop.
}
//-----------------------------------------------------------


//++++++++++++++++++++++++
//+++++ RTC settings +++++
//++++++++++++++++++++++++
M5EPD_Canvas canvas(&M5.EPD);

rtc_time_t RTCtime;
rtc_date_t RTCDate;

char timeStrbuff[64];

void flushTime(){
    M5.RTC.getTime(&RTCtime);
    M5.RTC.getDate(&RTCDate);

    sprintf(timeStrbuff,"%d/%02d/%02d %02d:%02d:%02d",
                        RTCDate.year,RTCDate.mon,RTCDate.day,
                        RTCtime.hour,RTCtime.min,RTCtime.sec);

    canvas.drawString(timeStrbuff, 0, 0);
    canvas.pushCanvas(100,200,UPDATE_MODE_DU4);
}

void setupTime(int year, int month, int date, int hour, int minute, int second){
  RTCtime.hour = hour;
  RTCtime.min = minute;
  RTCtime.sec = second;
  M5.RTC.setTime(&RTCtime);

  RTCDate.year = year;
  RTCDate.mon = month;
  RTCDate.day = date;
  M5.RTC.setDate(&RTCDate);
}
//---------------------------


void setup(void) {
  M5.begin();
  M5.EPD.SetRotation(90);
  M5.EPD.Clear(true);
  M5.RTC.begin();
  canvas.createCanvas(400, 300);
  canvas.setTextSize(3);

  checkCTSAndUpdateRTC();
}

void loop() {
    M5.update();
    if( M5.BtnL.pressedFor(2000))  // shutdown.
    {
        canvas.drawString("Shutdown...", 0, 0);
        canvas.pushCanvas(100,200,UPDATE_MODE_DU4);
        delay(600);
        M5.disableEPDPower();
        M5.disableEXTPower();
        M5.disableMainPower();
        esp_deep_sleep_start();
        while(1);
    }
    flushTime();
    delay(500);
}

ぜひこのコードをご活用いただき、NTP にアクセスできない場所でもほぼ正確な時刻を M5Paper に教えてあげてほしい。

追記: M5Paper_FactoryTest にこの機能を実装した。GitHub の diff を載せる。Setting - Sync Time を押すと同期が実行される。

参考

コードの一部などを以下のサイトから拝借した。

M5Paper の FactoryTest に HelloWorld アプリを追加する

M5PaperM5Stack より発売されたスマホ型 e-Ink 搭載 IoT デバイスである。

M5Paper には非常にリッチなサンプルプログラムである FactoryTest が存在する。

FactoryTest の GUI は EPDGUI, Frame というフレームワークからなる。これらは簡単に拡張できる構造となっており、実際に他のサンプルプログラムである Todo, Calculator でも利用されているが、非常に残念なことに Documentation が存在せず、Web で検索してもこれらを活用している例は少なく、各人が独自の UI を制作して利用している状態である。

この記事の目的は、そのような状態に一石を投じるべく、FactoryTest に HelloWorld アプリを実装する方法を示すことで、EPDGUI, Frame の使用方法を解説することである。

実際に動いているところの写真を載せる。

f:id:KainokiKaede:20210417175519j:plain f:id:KainokiKaede:20210417175527j:plain

実装後のプロジェクトは GitHub に掲載したFactoryTest からの diff を見ることで、新規機能の実装の際に必要な要素を概観することができる。ある程度プログラミングに慣れている人であれば記事を読むまでもなくこの diff を見ればよいだろう。

フレームワークの構造

EPDGUI フレームワークにおいて一つ一つの画面は Frame という名で管理される。新たな画面はこの Frame_Base クラスを継承して作成する。

EPDGUI フレームワークにおいて、画面に表示されるボタンや画像などの要素は EPDGUI_Base を継承して作成される。このクラスには位置・サイズ・タッチの判定などの機能が含まれている。たとえばこれを継承して作成された EPDGUI_Button クラスには、タッチされたときの挙動などが定義されている。

フレームの起動から終了までの流れ

このフレームワークがどのように用いられるかを、FactoryTest のコードを追うことによって見ていく。

起動

起動後、まず setup() 内から SysInit_start() が呼ばれる。

ここでは、はじめに表示する Frame である Frame_Main のインスタンスを作成し、それを EPDGUI_PushFrame を用いて frame_stack へ push している。

    // Frame_Main の新規インスタンスを作成。
    Frame_Main *frame_main = new Frame_Main();
    // EPDGUI_PushFrame でそのインスタンスを frame_stack へ Push。
    EPDGUI_PushFrame(frame_main);

このように、EPDGUI では Frame を frame_stack に push することで実行することができる。

続いて実行予定のフレームの全てを EPDGUI_AddFrame を用いて frame_map へ追加していく。これにより、名称を指定することで EPDGUI_GetFrame を用いて Frame を得ることができるようになる。ぶっちゃけこの構造はいらないんじゃないかと思う(実行したい Frame をその都度 frame_stack に push するのではだめなのか?)が、FactoryTest で用いられている方法なのでそれに従っておく。

    // 実行予定のフレームのインスタンスを作成。
    Frame_FactoryTest *frame_factorytest = new Frame_FactoryTest();
    // EPDGUI_AddFrame でそのインスタンスを名前付きで frame_map へ Add。
    EPDGUI_AddFrame("Frame_FactoryTest", frame_factorytest);
    // ...
    // ほかにも実行予定のフレームを frame_map へ登録していく。
    /// ...

実行

setup() が終了すると、loop() 内の EPDGUI_MainLoop() が実行される。

この中では、frame_stack の一番上にある Frame を frame_stack.top() で取ってきたのちに、そのフレームを初期化(frame->init(args) を実行)し、EPDGUI_Run(frame) を実行している。この EPDGUI_Run は frame が終了する際に終了する。つまり各 Frame が実行〜終了のプロセスを終えるごとに EPDGUI_MainLoop() は1回実行されるということになる。

EPDGUI_MainLoop() から実行された EPDGUI_Run の中にも while loop がある。

ここでは、まずその Frame が実行状態にあることを確認したのちに、frame->fun() を1回実行する。ゆえに各 Frame でループ実行したい処理がある場合は1ループぶんを frame->run() 内に実装すればよい。run() の戻り値が 0 のときその Frame は終了させられるので、終了したいとき以外は 0 以外を返さなければならない。

frame->run() が実行されたのち、タッチ状態が判定され、変化があれば EPDGUI_Process(x,y) が実行される。

EPDGUI_Process(x,y) では、epdgui_object_list (表示されているボタンなどの要素が全て入っている list)内の全てに対して UpdateState(x,y) を実行する。ゆえに、タッチイベントに反応させたい要素を作るには、UpdateState を実装したのちに要素のインスタンスを EPDGUI_AddObject を用いて epdgui_object_list 内に登録すればよい。

ここまでが EPDGUI_Run のループである。ループが終了するとまたループの頭に戻る。

終了

EPDGUI_Run のループ内で、PopFrame() を実行したのちに _is_run を 0 にすると、次の EPDGUI_Run 内のループ中に frame->exit() が呼ばれてフレームは終了される。ゆえにフレーム終了時に行いたい処理は frame->exit() 内に実装すればよい。

フレーム終了後は EPDGUI_MainLoop() が再度頭から実行され、frame_stack.top() にある次のフレームが EPDGUI_Run(frame) される。

HelloWorld アプリを実装する

いよいよアプリの実装に入る。

まずは座標などを決めるために、何らかのお絵かきアプリを用いて目標とする画面を作ってみる。私は下のような画面を作成した。

f:id:KainokiKaede:20210417183601j:plain

続いて frame_helloworld.cpp, frame_helloworld.h を作る。ヘッダファイルでは変数定義のみを行っている。frame_helloworld.cpp について解説する。

前述したように、新しい Frame を作成するにあたって実装する必要があるのは、コンストラクタ・デストラクタ・init・run・exit である。ただし run, exit の際に何もしなくていいなら、空の関数が基底クラスで定義されているので再定義は不要である。

#include "frame_helloworld.h"

Frame_HelloWorld::Frame_HelloWorld(void)
{
    // コンストラクタ。

    // _frame_name を定義する。
    _frame_name = "Frame_HelloWorld";

    // Frame(アプリ)名を上段に表示する。
    _canvas_title->drawString("HelloWorld", 270, 34);

    // 左上に表示される、ホーム画面に戻るボタンを作成する。
    // exitbtn 関数 は Frame_Base に定義されている。実行すると _key_exit 変数に戻るボタンのインスタンスが作成される。
    exitbtn("Home");
    // ボタンから指が離れたときに、コールバック関数に渡す 0 番目の変数を定義する。ここでは _is_run を渡している。
    _key_exit->AddArgs(EPDGUI_Button::EVENT_RELEASED, 0, (void*)(&_is_run));
    // ボタンから指が離れたときに実行されるコールバック関数 exit_cb(Frame_Base で定義されている)をバインドしている。
    _key_exit->Bind(EPDGUI_Button::EVENT_RELEASED, &Frame_Base::exit_cb);

    // Hello World と表示するボタンのインスタンスを作成している。位置は前述のお絵かきアプリで作成した画面から算出する。
    _key_helloworld = new EPDGUI_Button("Hello M5Paper World!", 30, 137, 480, 190, EPDGUI_Button::STYLE_DEFAULT);
}

Frame_HelloWorld::~Frame_HelloWorld(void)
{
    // デストラクタ。とくに何もしない。
}

int Frame_HelloWorld::init(epdgui_args_vector_t &args)
{
    // init.

    // まずこの Frame が実行中であるフラグを立てる。_is_run = 0 の状態で次のループが始まると Frame は終了される。
    _is_run = 1;
    // 画面を全消しする。
    M5.EPD.Clear();
    // アプリ名を上段に表示する。
    _canvas_title->pushCanvas(0, 8, UPDATE_MODE_NONE);
    // epdgui_object_list に Hello World を表示しているボタンを登録している。こうしなければボタンは表示されない。
    EPDGUI_AddObject(_key_helloworld);
    // 同様に、epdgui_object_list に戻るボタンを登録している。
    EPDGUI_AddObject(_key_exit);
    // FactoryTest の Frame は 3 とか 6 とか 9 とかを返している。この値を参照するコードはないのでテキトーでよさそう。
    return 3;
}

// run, exit のときには何もしなくてよいので定義していない。

ここまでで HelloWorld フレームは作成できた。

続いて FactoryTest からこのフレームを実行できるようにする。変更点は GitHub の diff のほうが見やすいのでそちらを見ていただいてもよい。

frame/frame.h に frame_helloworld.h を追加。これはもしかしたら不要かもしれない。

#include "frame_helloworld.h"

systeminit.cpp に次を追加。これで frame_map に "Frame_HelloWorld" のキーで HelloWorld フレームのインスタンスを作成できた。

        Frame_HelloWorld *frame_helloworld = new Frame_HelloWorld();
        EPDGUI_AddFrame("Frame_HelloWorld", frame_helloworld);

92x92 のアイコンを作成して、

f:id:KainokiKaede:20210417183532p:plain

M5Paper のリポジトリに付属しているツールである image2gray.py でアイコンを  ImageResource_HelloWorld.h に変換。変換先のファイルを一度開いて、ifndef, define の行を書き換えないとファイルが読み込まれないので注意。

#ifndef IMAGERESOURCE_HELLOWORLD_H
#define IMAGERESOURCE_HELLOWORLD_H
// ...

frame_main.cpp に新しいボタンを追加して、それを押すと HelloWorld フレーム(アプリ)が開くように変更。

#include "frame_helloworld.h"
#include "../resources/ImageResource_HelloWorld.h"

enum{
    kKeyHelloWorld
}

// HelloWorld アプリのボタンを押したときに呼ばれるコールバック関数。
// Frame_HelloWorld のインスタンスを frame_stack へ push している。
void key_helloworld_cb(epdgui_args_vector_t &args)
{
    Frame_Base *frame = EPDGUI_GetFrame("Frame_HelloWorld");
    if(frame == NULL)
    {
        frame = new Frame_HelloWorld();
        EPDGUI_AddFrame("Frame_HelloWorld", frame);
    }
    EPDGUI_PushFrame(frame);
    *((int*)(args[0])) = 0;
}

// ...

_key[8] = new EPDGUI_Button("HelloWorld", 20, 390, KEY_W, KEY_H);

// ...
    _key[kKeyHelloWorld]->CanvasNormal()->pushImage(0, 0, 92, 92, ImageResource_helloworld_icon_92x92);
    *(_key[kKeyHelloWorld]->CanvasPressed()) = *(_key[kKeyHelloWorld]->CanvasNormal());
    _key[kKeyHelloWorld]->CanvasPressed()->ReverseColor();
    _key[kKeyHelloWorld]->AddArgs(EPDGUI_Button::EVENT_RELEASED, 0, (void*)(&_is_run));
    _key[kKeyHelloWorld]->Bind(EPDGUI_Button::EVENT_RELEASED, key_helloworld_cb);

    _names->fillCanvas(0);
    _names->drawString("HelloWorld", 20 + 46, 16);
    _names->pushCanvas(0, 337+151, mode);

int Frame_Main::init(epdgui_args_vector_t &args)
    for(int i = 0; i < 9; i++)
    {
        EPDGUI_AddObject(_key[i]);
    }

これにて終了。コンパイルして M5Paper に送ると HelloWorld アプリが出現しているはずである。

結語

この記事によって M5Paper の GUI 開発がさかんになれば幸いである。

MacBook Pro (Retina, 13-inch, Late 2013) を macOS Big Sur にアップデートできない

自分が持っている古い MacBook Pro (Retina, 13-inch, Late 2013) を Big Sur にアップデートしようとしたら、システム環境設定のソフトウェアアップデート画面で「必要なバージョンの macOS がありません」(英語だと "The requested version of macOS is not available." )と表示されてアップデートできなかった。この型式のコンピュータは macOS Big Sur と互換性のあるコンピュータ のリストに入っているので本来ならばアップデートできるはずである。

少し調べてみたところ、このモデルの MacBook Pro に Big Sur を導入すると文鎮化する問題 があるようで、一時的にアップデート対象から外された ようだ。

同時に Apple も文鎮化した場合の対応について公表している。アップデート対象に戻るかどうかは現段階では不明なようである。

Twitter でもこの現象は複数の人が経験していた (link1, link2 )。日本語で情報が見つからなかったので取り急ぎブログを更新する。

2021-02-06 追記:再度システム環境設定を確認したところ Big Sur へのアップデートが表示され、正常にアップデートできた。問題は解消したものと思われる。