この記事を作った動機
2025/10/9 から、2025/10/16 に至るまでの間で OneNote 代替に関して、実装をするなど研究を進めているうちにわかったことなどを簡易的に記録する。
動作の様子 (フロントエンド)
TODOs
2025/10/16 になる間やる項目たち。やりきれなかった分は次に持ち越し。
主な項目
-
ページ作成機能の実装
- linux だけでなく windows 環境において、再帰的にフォルダを作成できるようにする。 -> winpath = someFullAbsolutePath.replace("//","/").replace("/","\")
-
mkdirhelper 関数の実装
-
編集機能、データセーブ(更新)の実装
- バッファーのデータストアを実装
- 別のページを開いても未保存のデータのバッファが管理されるようにする
-
内部構造を説明する図とかドキュメントを作る
- フロントエンド
- 共通のUIについて、使い方の説明について、ドキュメントを書いた。
- overlayWindow
- messagebox
- toolsbar
- page
- drawio で説明する図を作る
- とりあえず図を書くための準備をした。
- 他人が構造の理解に必要な分を記録できたか
- z-index について、どのような割り当てになっているか記録
- 共通のUIについて、使い方の説明について、ドキュメントを書いた。
- バックエンド(更新)
- 新たに追加した、task の仕組みについて、ドキュメントを整備する。
- フロントエンド
-
free ページの実装
- フロントエンド
- オブジェクト変形用に汎用的なコンポーネントを定義する。カーソルがオブジェクトをホバーしているときに出現するようにする。タッチでは、一回タップで出現。(サイズ変更、移動)
- 編集モードを各アイテムの要素の種類ごとに実装。汎用的には、クリックされた場合に編集モードに移行するようにする。タッチではダブルタッチで移行する。
- バックエンド
- フロントエンド
-
selector の改善
- ノートブックはドロップダウンで選択させて、選択されたノートブックの階層構造やページリストを表示するようにする
- Selector に未保存の表示を行う
-
OverlayWindow において、リサイズやサイズ制限をオプションで付ける。デフォルトでは無効にして、とりあえず selector においてそれが機能するようにする。
-
Free ページの要素について、基本要素の項目にアイテムの色をRGBAで設定できるようにする。
-
全体的な未保存、保存、バッファーの管理について考える
-
pageType などは現状のままで、増やさず、free と markdown に専念する
-
tasks -> jobs で定期的に実行されるバックエンドの関数群において、pageCleaner.py を実装する。各ノートにある、参照が消され、deleted.json に記録されていて、一定期間たったページを時期が来たら本当にファイルシステム上から削除する実装をする。コード自体は、tasks.py の設定に依存し、現状では10秒ごとに消す時が来ていないかポーリングする簡易的な実装である。
-
バックエンド割り込みの実装
- バックエンドからフロントエンドの方向の割り込みをすでにドキュメントである程度定義したとおりに実装する。例えば、ページ内容に変更があったら通知したりする。
-
helper.commonとして、sendIntteruptを実装する。 - 割り込みが必要なコマンド群から、割り込みを送るようにする。また各コマンドのドキュメントに割り込みについて説明を書く。これに伴いテンプレートも割り込みについて記述するように、更新を行う。
- フロントエンド側を割り込みを受けつけるように、改修していく。自動的に、フロントエンドが、バックエンドの割り込みによって連動して機能することを確認する。例えば、ページが作成されたとき、内容が変更されたとき、ノートブックが削除された時などに、データサーバが接続しているすべてのフロントエンドに、その通知が行く。
- 割り込みの仕組みについてのドキュメントの記述が見つからない場合、新たに定義し、それをもとに実装を進める。
-
エラーハンドリングにおける共通の
errorResponse関数を helper.common に実装したので、それをなるべく使うように全体のコードを更新していく。特にフロントエンドと関係のあるところで使う。 -
エラーハンドリングにおいて、except の エラーのメッセージを握りつぶさない。
except Exception as e:
print("[commandName] ERROR: error message")
print(e)
- 再接続機能が正しく機能しない場合を検証する
他の事項
- File API 周りの実装
- マルチメディアの対応
- messagebox の実装
やったことの詳細
errorResponse 関数の仕様を変更
helper.commonのerrorResponse関数で、Python のExceptionをメッセージとして表示する仕様に変更した。ドキュメントもそれに応じて書き直し、引数について説明を追加した。なお、exception メッセージは強制ではなく、任意ではある。
「エラーハンドリングにおいて、except の エラーのメッセージを握りつぶさない。」の TODO があったので、より一貫性のある実装としてまとめる意味合いもこの仕様変更にはある。
async def errorResponse(websocket,request:dict,errorMessage:str,variablesList:list,exception = None):
print("{} ERROR: {}".format(request["command"],errorMessage))
variableState = {}
print("{} ERROR: variable state --------------------------".format(request["command"]))
for aVar in variablesList:
# print variable name and data
# https://stackoverflow.com/questions/18425225/getting-the-name-of-a-variable-as-a-string
# f'{aaa=}'.split("=")[0]
varName = f'{aVar=}'.split("=")[0]
print("{}:\n{}\n".format(varName,aVar))
variableState[varName] = str(aVar)
print("{} ERROR: variable state end --------------------------".format(request["command"]))
print("{} ERROR: exception message --------------------------".format(request["command"]))
print(exception)
print("{} ERROR: exception message end --------------------------".format(request["command"]))
responseString = json.dumps({
"status" : "error",
"errorMessage" : errorMessage,
"UUID" : request["UUID"],
"command" : request["command"],
"data" : variableState
})
await websocket.send(responseString)
print(">>> " + responseString)
mkdir 関数の実装
バックエンド内において、共通でフォルダ作成に使える、helper.commonのmkdir関数を実装した。ドキュメントも書いた。
# arg:
# absoluteFolderPath : full folder path to create
# return value
# OK : False
# Error : True will be returned when failed to create folder.
def mkdir(absoluteFolderPath:str):
if(checkTheAbsolutePath(absoluteFolderPath)):
print("mkdir helper: This is malformed path")
print(absoluteFolderPath)
return True
if(platform.system() == "Windows"):
winpath = absoluteFolderPath.replace("//","/").replace("/","\\")
command = ["mkdir",winpath]
print("winpath : " + winpath)
else:
command = ["mkdir -p " + absoluteFolderPath]
result = subprocess.run(command,shell=True,capture_output=True)
print("folder path : " + absoluteFolderPath)
if(result.returncode != 0):
print("mkdir helper: Unable to create the folder.")
print(str(result.stdout))
print(str(result.stderr))
return True
return False
checkTheAbsolutePath 関数の実装
helper.commonのcheckTheAbsolutePath関数を実装した。ドキュメントも書いた。この関数の実装の意図としては、いちいち各 helper 関数ごとにパスをチェックするのは面倒だったので、まとめて処理しようと思ったというのがある。具体的には、mkdir関数内で、deleteDataSafely関数内で行われていることと同じことをいちいち書くのは面倒だというのがあった。
# arg:
# absolutePath : file or folder path
# return value
# OK : False will be returned.
# Error : True will be returned when the path is malformed.
def checkTheAbsolutePath(absolutePath:str):
# check the absoluteDataPath is something malisuous or not.
if(absolutePath == "/" or absolutePath == "C:\\"):
print("checkTheAbsolutePath: Failed. Try to delete root directory.")
return True
# check the absoluteDataPath is relative path or not.
if(absolutePath[0] == "."):
print("checkTheAbsolutePath: Failed. A relative path is specified. Use absolute path.")
return True
# check the absoluteDataPath try to delete outside content of the notebooks or not.
isNotebookPath = False
for aNotebookStoreRoot in loadSettings.settings["NotebookRootFolder"]:
if(aNotebookStoreRoot in absolutePath):
isNotebookPath = True
break
if(not isNotebookPath):
print("checkTheAbsolutePath: Failed. The absoluteDataPath is outside of the notebook store.")
return True
return False
errorResponse 関数の仕様を変更
変数名と変数の内容を表示するという仕様は、現状ではすぐには成り立たせるのが難しいことが分かったため、変数の内容だけ表示するように変更した。
具体的な問題としては、引数としてリストで渡している変数の名前を以下の実装で正しく取得できないことが分かったというのがある。この実装では、for ループで一時的に内容を読みだすために使っているaVarの変数の名前が、引数として渡した変数の名前として現れてしまう。
...
variableState = {}
print("{} ERROR: variable state --------------------------".format(request["command"]))
for aVar in variablesList:
# print variable name and data
# https://stackoverflow.com/questions/18425225/getting-the-name-of-a-variable-as-a-string
# f'{aaa=}'.split("=")[0]
varName = f'{aVar=}'.split("=")[0]
print("{}:\n{}\n".format(varName,aVar))
variableState[varName] = str(aVar)
...
今回は、シンプルにする方向性で修正を考え、結果として変数名は表示せずに、変数の内容だけ表示し、変数の内容をリスト形式にまとめてフロントエンドに送るようにした。
...
variableState = []
print("{} ERROR: variable state --------------------------".format(request["command"]))
for aVar in variablesList:
# print variable name and data
# https://stackoverflow.com/questions/18425225/getting-the-name-of-a-variable-as-a-string
# f'{aaa=}'.split("=")[0]
print("{}\n".format(aVar))
variableState.append(str(aVar))
...
selector の実装を変更
全部のノートブックの情報を表示するのではなく、選択されたノートブックに分だけの表示になるように変更した。
free ページの実装を進めた
-
free ページの基本的なデータ読み込みから、セーブ、終了までのフローができた。ちなみに undo、redo についてはまだ free ページにおいては実装する気がない。
- バックエンドからデータを取得し、free ページが読み込まれる。この時
useFreePageItemsStoreの items 配列にデータが追加されていく。 useFreePageItemsStoreを経由して、テキストを編集した、新たなアイテムを追加した、アイテムを消したなどの変更があると、useFreePageItemStoreEffectにより、自動的にupdatePageコマンドを使って内容をセーブする。- 終了時には、
useFreePageItemsStoreの items の内容が初期化されて、次の表示のために準備される。
- バックエンドからデータを取得し、free ページが読み込まれる。この時
-
編集機能として何が欲しいか書き出してみた。
- add item
- remove item
- resize item
- move item
- range selection
- drop item
-
useFreePageItemStoreEffectを廃止した
useFreePageItemStoreEffectとして hooks をまとめていると、zustand の挙動が怪しくなり、ReactのHooksに準拠していないというエラーになったので、free ページのmain.tsxにとりあえずその中身は移動しておくことにした。 -
右クリック menu の実装
右クリックしたときのメニューを部分的に実装した。
messageBox 向けに画像を作った
これについては、後からいくらでもできるということで、そこまで深く考えて作っておらずとりあえず機能する程度に仕上げた。
ok
error
warning
info
messageBox を実際に動くようにした
バッファーのデータストアを実装
保存されていないページのデータを全体で管理するためのストアを作り、型定義をした。
// TODO: create buffer store for each page
// TODO: make all pages use the buffer store to prevent from losing the unsaved data.
// TODO: if there are unsaved buffers, ask the user to save or throw away the data before the frontend is closed.
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
export type AnUnsavedBuffer = {
notebookName : string,
pageID : string,
pageType : string,
UUID : string, // to identify each buffers.
timestamp : Date,
bufferContentString : string,
}
// NOTE: normally single page will add single buffer and update constantly if there are any modification.
export type unsavedBuffers = {
buffers : AnUnsavedBuffer[],
addBuffer : (unsavedBuffer:AnUnsavedBuffer) => void,
removeBuffer : (unsavedBuffer:AnUnsavedBuffer) => void,
updateBuffer : (unsavedBuffer:AnUnsavedBuffer) => void, // not to create buffer, to update exist buffer
getBuffers : (pageID:string) => AnUnsavedBuffer[],
getUnsavedPageList : () => string[], // for selector compornent.
}
export const useUnsavedBuffersStore = create<unsavedBuffers>((set,get) => ({
buffers: [],
addBuffer: (unsavedBuffer:AnUnsavedBuffer) => {
const oldBuffer = get().buffers
const newBuffer = []
for(const aBuffer of oldBuffer){
// when the buffer has already existed, abort the update
if(aBuffer.UUID == unsavedBuffer.UUID) return
newBuffer.push(aBuffer)
}
newBuffer.push(unsavedBuffer)
set({buffers:newBuffer})
},
removeBuffer: (unsavedBuffer:AnUnsavedBuffer) => {
const oldBuffer = get().buffers
const newBuffer = []
for(const aBuffer of oldBuffer){
if(aBuffer.UUID == unsavedBuffer.UUID) continue
newBuffer.push(aBuffer)
}
newBuffer.push(unsavedBuffer)
set({buffers:newBuffer})
},
getBuffers: (pageID:string) => {
const allBuffer = get().buffers
const buffers = []
for(const aBuffer of allBuffer){
if(aBuffer.pageID == pageID)
buffers.push(aBuffer)
}
return buffers
},
updateBuffer: (unsavedBuffer:AnUnsavedBuffer) => {
const oldBuffer = get().buffers
const newBuffer = []
for(const aBuffer of oldBuffer){
// update
if(aBuffer.UUID == unsavedBuffer.UUID) {
// make sure the timestamp is the time of the last modify.
unsavedBuffer.timestamp = new Date()
newBuffer.push(unsavedBuffer)
continue
}
newBuffer.push(aBuffer)
}
newBuffer.push(unsavedBuffer)
set({buffers:newBuffer})
},
getUnsavedPageList: () => {
const buffers = get().buffers
const pageIDlist:string[] = []
for(const aBuffer of buffers){
let find = false
for(const pageIDname of pageIDlist){
if(pageIDname == aBuffer.pageID){
find = true
break
}
}
if(find) continue
pageIDlist.push(aBuffer.pageID)
}
return pageIDlist
}
}))
フロントエンドの共通のUIの実装に関して、ドキュメントを書き始めた
フロントエンドに存在している、共通のUIパーツは使い方をドキュメントとしてまとめておくことにした。現状では以下の項目について、記録をした。
- OverlayWindow
- toolsBar
- messageBox
以下は記録の例である。
参考にしたサイトとか
- ChatGPT
https://chatgpt.com/ (2025年10月9日) - How can I set the default value for an HTML <select> element? - Stack Overflow
https://stackoverflow.com/questions/3518002/how-can-i-set-the-default-value-for-an-html-select-element (2025年10月13日) - MouseEvent - Web APIs | MDN
https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent (2025年10月14日) - MouseEvent: button property - Web APIs | MDN
https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button (2025年10月14日) - contextmenu - How to disable right-click context-menu in JavaScript - Stack Overflow
https://stackoverflow.com/questions/381795/how-to-disable-right-click-context-menu-in-javascript (2025年10月14日) - Adding Images, Fonts, and Files | Create React App
https://create-react-app.dev/docs/adding-images-fonts-and-files/ (2025年10月16日)