h-1.flet.3/components/text_editor.py

386 lines
13 KiB
Python

"""
テキストエディタコンポーネント
再利用可能なテキスト編集機能を提供
"""
import flet as ft
import sqlite3
import logging
from datetime import datetime
from typing import List, Dict, Optional, Callable
class TextEditor:
"""再利用可能なテキストエディタコンポーネント"""
def __init__(
self,
page: ft.Page,
title: str = "テキストエディタ",
placeholder: str = "テキストを入力してください",
min_lines: int = 10,
max_lines: int = 50,
width: int = 600,
height: int = 400,
on_save: Optional[Callable] = None,
on_load: Optional[Callable] = None,
on_delete: Optional[Callable] = None
):
self.page = page
self.title_text = title
self.placeholder = placeholder
self.min_lines = min_lines
self.max_lines = max_lines
self.width = width
self.height = height
self.on_save = on_save
self.on_load = on_load
self.on_delete = on_delete
# UI部品
self.title = ft.Text(title, size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900)
# テキストエリア
self.text_area = ft.TextField(
label=placeholder,
multiline=True,
min_lines=min_lines,
max_lines=max_lines,
value="",
autofocus=True,
width=width,
height=height
)
# ボタン群
self.save_btn = ft.Button(
"保存",
on_click=self.save_text,
bgcolor=ft.Colors.GREEN,
color=ft.Colors.WHITE
)
self.clear_btn = ft.Button(
"クリア",
on_click=self.clear_text,
bgcolor=ft.Colors.ORANGE,
color=ft.Colors.WHITE
)
self.load_btn = ft.Button(
"読込",
on_click=self.load_latest,
bgcolor=ft.Colors.BLUE,
color=ft.Colors.WHITE
)
self.delete_btn = ft.Button(
"削除",
on_click=self.delete_latest,
bgcolor=ft.Colors.RED,
color=ft.Colors.WHITE
)
# テキストリスト
self.text_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=200)
# 操作説明
self.instructions = ft.Text(
"操作方法: TABでフォーカス移動、SPACE/ENTERで保存、マウスクリックでもボタン操作可能",
size=12,
color=ft.Colors.GREY_600
)
# データ読み込み
self.load_texts()
def build(self):
"""UIを構築して返す"""
return ft.Column([
self.title,
ft.Divider(),
ft.Row([self.save_btn, self.clear_btn, self.load_btn, self.delete_btn], spacing=10),
ft.Divider(),
self.text_area,
ft.Divider(),
ft.Text("保存済みテキスト", size=18, weight=ft.FontWeight.BOLD),
self.instructions,
self.text_list
], expand=True, spacing=15)
def save_text(self, e):
"""テキストを保存"""
if self.text_area.value.strip():
try:
# データベースに保存
conn = sqlite3.connect('sales.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS text_storage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
INSERT INTO text_storage (category, title, content)
VALUES (?, ?, ?)
''', (self.title_text, f"{self.title_text}_{datetime.now().strftime('%Y%m%d_%H%M%S')}", self.text_area.value))
conn.commit()
conn.close()
# コールバック実行
if self.on_save:
self.on_save(self.text_area.value)
# 成功メッセージ
self._show_snackbar("テキストを保存しました", ft.Colors.GREEN)
logging.info(f"テキスト保存: {len(self.text_area.value)} 文字")
# リスト更新
self.load_texts()
except Exception as ex:
logging.error(f"テキスト保存エラー: {ex}")
self._show_snackbar("保存エラー", ft.Colors.RED)
def clear_text(self, e):
"""テキストをクリア"""
self.text_area.value = ""
self.page.update()
logging.info("テキストをクリア")
def load_latest(self, e):
"""最新のテキストを読み込み"""
try:
conn = sqlite3.connect('sales.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS text_storage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
SELECT id, title, content, created_at
FROM text_storage
WHERE category = ?
ORDER BY created_at DESC
LIMIT 1
''', (self.title_text,))
result = cursor.fetchone()
conn.close()
if result:
text_id, title, content, created_at = result
self.text_area.value = content
# コールバック実行
if self.on_load:
self.on_load(content)
self._show_snackbar(f"テキストを読み込みました (ID: {text_id})", ft.Colors.GREEN)
logging.info(f"テキスト読込: ID={text_id}")
else:
self._show_snackbar("保存済みテキストがありません", ft.Colors.ORANGE)
except Exception as ex:
logging.error(f"テキスト読込エラー: {ex}")
self._show_snackbar("読込エラー", ft.Colors.RED)
def delete_latest(self, e):
"""最新のテキストを削除"""
try:
conn = sqlite3.connect('sales.db')
cursor = conn.cursor()
cursor.execute('''
DELETE FROM text_storage
WHERE id = (SELECT id FROM text_storage WHERE category = ? ORDER BY created_at DESC LIMIT 1)
''', (self.title_text,))
conn.commit()
conn.close()
# コールバック実行
if self.on_delete:
self.on_delete()
# リスト更新
self.load_texts()
self._show_snackbar("テキストを削除しました", ft.Colors.GREEN)
logging.info("テキスト削除完了")
except Exception as ex:
logging.error(f"テキスト削除エラー: {ex}")
self._show_snackbar("削除エラー", ft.Colors.RED)
def load_texts(self):
"""テキストリストを読み込む"""
try:
conn = sqlite3.connect('sales.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS text_storage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
SELECT id, title, content, created_at
FROM text_storage
WHERE category = ?
ORDER BY created_at DESC
LIMIT 10
''', (self.title_text,))
texts = cursor.fetchall()
conn.close()
# リストを更新
self.text_list.controls.clear()
for text in texts:
text_id, title, content, created_at = text
# テキストカード
text_card = ft.Card(
content=ft.Container(
content=ft.Column([
ft.Row([
ft.Text(f"ID: {text_id}", size=12, color=ft.Colors.GREY_600),
ft.Text(f"作成: {created_at}", size=12, color=ft.Colors.GREY_600)
], spacing=5),
ft.Text(title, weight=ft.FontWeight.BOLD, size=14),
ft.Text(content[:100] + "..." if len(content) > 100 else content, size=12),
]),
padding=10,
width=400
),
margin=ft.margin.only(bottom=5)
)
# 読込ボタン
load_btn = ft.Button(
"読込",
on_click=lambda _, tid=text_id: self.load_specific_text(text_id),
bgcolor=ft.Colors.BLUE,
color=ft.Colors.WHITE,
width=80
)
self.text_list.controls.append(
ft.Row([text_card, load_btn], alignment=ft.MainAxisAlignment.SPACE_BETWEEN)
)
self.page.update()
except Exception as e:
logging.error(f"テキストリスト読込エラー: {e}")
def load_specific_text(self, text_id):
"""特定のテキストを読み込む"""
try:
conn = sqlite3.connect('sales.db')
cursor = conn.cursor()
cursor.execute('''
SELECT content
FROM text_storage
WHERE id = ?
''', (text_id,))
result = cursor.fetchone()
conn.close()
if result:
self.text_area.value = result[0]
# コールバック実行
if self.on_load:
self.on_load(result[0])
self._show_snackbar(f"テキスト #{text_id} を読み込みました", ft.Colors.GREEN)
logging.info(f"テキスト読込: ID={text_id}")
else:
self._show_snackbar("テキストが見つかりません", ft.Colors.RED)
except Exception as ex:
logging.error(f"テキスト読込エラー: {ex}")
self._show_snackbar("読込エラー", ft.Colors.RED)
def get_text(self) -> str:
"""現在のテキストを取得"""
return self.text_area.value
def set_text(self, text: str):
"""テキストを設定"""
self.text_area.value = text
self.page.update()
def _show_snackbar(self, message: str, color: ft.Colors):
"""SnackBarを表示"""
try:
self.page.snack_bar = ft.SnackBar(
content=ft.Text(message),
bgcolor=color
)
self.page.snack_bar.open = True
self.page.update()
except:
pass
# 使用例
def create_draft_editor(page: ft.Page) -> TextEditor:
"""下書きエディタを作成"""
return TextEditor(
page=page,
title="下書きエディタ",
placeholder="下書き内容を入力してください",
min_lines=15,
max_lines=50,
width=600,
height=400
)
def create_memo_editor(page: ft.Page) -> TextEditor:
"""メモエディタを作成"""
return TextEditor(
page=page,
title="メモ",
placeholder="メモを入力してください",
min_lines=10,
max_lines=30,
width=500,
height=300
)
def create_note_editor(page: ft.Page) -> TextEditor:
"""ノートエディタを作成"""
return TextEditor(
page=page,
title="ノート",
placeholder="ノートを入力してください",
min_lines=20,
max_lines=100,
width=700,
height=500
)