|
|
#!/usr/bin/env python3
|
|
|
# -*- coding: utf-8 -*-
|
|
|
"""
|
|
|
GUI主界面模块
|
|
|
FreeNote应用的图形用户界面
|
|
|
"""
|
|
|
|
|
|
import tkinter as tk
|
|
|
from tkinter import ttk, messagebox, filedialog
|
|
|
import datetime
|
|
|
|
|
|
from mod_conf import get_config_manager, get_setting, set_setting
|
|
|
from mod_tpl import get_template_help_text
|
|
|
from mod_file import save_note_content, FileSaveError
|
|
|
|
|
|
|
|
|
class FreeNoteApp:
|
|
|
def __init__(self, root):
|
|
|
self.root = root
|
|
|
self.root.title("FreeNote-随手记")
|
|
|
self.root.geometry("1000x800")
|
|
|
|
|
|
# 文本修改状态标志
|
|
|
self.text_modified = False
|
|
|
|
|
|
# 获取配置管理器
|
|
|
self.config_manager = get_config_manager()
|
|
|
|
|
|
# 创建界面
|
|
|
self.create_widgets()
|
|
|
|
|
|
# 绑定快捷键
|
|
|
self.bind_shortcuts()
|
|
|
|
|
|
def create_widgets(self):
|
|
|
"""创建界面组件"""
|
|
|
# 初始化界面变量
|
|
|
self.init_ui_variables()
|
|
|
|
|
|
# 创建主框架
|
|
|
main_frame = ttk.Frame(self.root)
|
|
|
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
|
|
# 配置Grid布局权重
|
|
|
main_frame.grid_rowconfigure(0, weight=0) # 工具栏:固定高度
|
|
|
main_frame.grid_rowconfigure(1, weight=1) # 文本区域:可伸缩
|
|
|
main_frame.grid_rowconfigure(2, weight=0) # 状态栏:固定高度
|
|
|
main_frame.grid_columnconfigure(0, weight=1) # 列可伸缩
|
|
|
|
|
|
# 工具栏
|
|
|
self.create_toolbar(main_frame)
|
|
|
|
|
|
# 文本编辑区域
|
|
|
self.create_text_area(main_frame)
|
|
|
|
|
|
# 状态栏
|
|
|
self.create_status_bar(main_frame)
|
|
|
|
|
|
def init_ui_variables(self):
|
|
|
"""初始化界面变量"""
|
|
|
# 目录变量
|
|
|
self.folder_var = tk.StringVar(
|
|
|
value=get_setting('folder'))
|
|
|
|
|
|
# 文件名变量
|
|
|
self.filename_var = tk.StringVar(
|
|
|
value=get_setting('filename'))
|
|
|
|
|
|
# 扩展名变量
|
|
|
self.ext_var = tk.StringVar(
|
|
|
value=get_setting('ext'))
|
|
|
|
|
|
def create_toolbar(self, parent):
|
|
|
"""创建工具栏"""
|
|
|
toolbar = ttk.Frame(parent)
|
|
|
toolbar.grid(row=0, column=0, sticky=tk.EW, pady=(0, 5))
|
|
|
|
|
|
# 配置grid权重,使目录框和文件名框可以缩放
|
|
|
toolbar.grid_columnconfigure(0, weight=1) # 目录框列
|
|
|
toolbar.grid_columnconfigure(2, weight=2) # 文件名框列,权重为目录框的2倍
|
|
|
|
|
|
# 目录框
|
|
|
self.dir_entry = ttk.Entry(toolbar, textvariable=self.folder_var)
|
|
|
self.dir_entry.grid(row=0, column=0, sticky=tk.EW, padx=(0, 5))
|
|
|
|
|
|
# 绑定失去焦点事件
|
|
|
self.dir_entry.bind('<FocusOut>', self.on_folder_focus_out)
|
|
|
self.dir_entry.bind('<Return>', self.on_folder_focus_out)
|
|
|
|
|
|
# 选择目录按钮
|
|
|
browse_btn = ttk.Button(toolbar, text="选择", width=4, command=self.browse_folder)
|
|
|
browse_btn.grid(row=0, column=1, padx=(0, 10))
|
|
|
|
|
|
# 文件名框
|
|
|
self.filename_entry = ttk.Entry(
|
|
|
toolbar, textvariable=self.filename_var)
|
|
|
self.filename_entry.grid(row=0, column=2, sticky=tk.EW, padx=(0, 5))
|
|
|
|
|
|
# 绑定失去焦点事件
|
|
|
self.filename_entry.bind('<FocusOut>', self.on_filename_focus_out)
|
|
|
self.filename_entry.bind('<Return>', self.on_filename_focus_out)
|
|
|
|
|
|
# 模板按钮
|
|
|
template_btn = ttk.Button(toolbar, text="模板", width=4, command=self.show_template_help)
|
|
|
template_btn.grid(row=0, column=3, padx=(0, 10))
|
|
|
|
|
|
# 扩展名选择框
|
|
|
ext_combo = ttk.Combobox(toolbar, textvariable=self.ext_var, values=['.txt', '.md'], width=8, state='readonly')
|
|
|
ext_combo.bind('<<ComboboxSelected>>', self.on_ext_change)
|
|
|
ext_combo.grid(row=0, column=4, padx=(0, 10))
|
|
|
|
|
|
# 保存按钮
|
|
|
save_btn = ttk.Button(toolbar, text="保存", width=4, command=self.save_note)
|
|
|
save_btn.grid(row=0, column=5)
|
|
|
|
|
|
def create_text_area(self, parent):
|
|
|
"""创建文本编辑区域"""
|
|
|
text_frame = ttk.Frame(parent)
|
|
|
text_frame.grid(row=1, column=0, sticky=tk.NSEW)
|
|
|
|
|
|
# 配置文本框架的布局权重
|
|
|
text_frame.grid_rowconfigure(0, weight=1)
|
|
|
text_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
|
|
# 文本框
|
|
|
self.text_area = tk.Text(text_frame, wrap=tk.WORD)
|
|
|
self.text_area.grid(row=0, column=0, sticky=tk.NSEW)
|
|
|
|
|
|
# 绑定文本修改事件
|
|
|
self.text_area.bind('<KeyPress>', self.on_text_modified)
|
|
|
self.text_area.bind('<KeyRelease>', self.on_text_modified)
|
|
|
self.text_area.bind('<Button-1>', self.on_text_modified)
|
|
|
self.text_area.bind('<Control-v>', self.on_text_modified) # 粘贴
|
|
|
self.text_area.bind('<Control-x>', self.on_text_modified) # 剪切
|
|
|
|
|
|
# 滚动条
|
|
|
scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.text_area.yview)
|
|
|
scrollbar.grid(row=0, column=1, sticky=tk.NS)
|
|
|
self.text_area.config(yscrollcommand=scrollbar.set)
|
|
|
|
|
|
def create_status_bar(self, parent):
|
|
|
"""创建状态栏"""
|
|
|
status_frame = ttk.Frame(parent)
|
|
|
status_frame.grid(row=2, column=0, sticky=tk.EW, pady=(5, 0))
|
|
|
|
|
|
# 配置状态栏布局权重
|
|
|
status_frame.grid_columnconfigure(0, weight=1) # 左侧状态消息可伸缩
|
|
|
status_frame.grid_columnconfigure(1, weight=0) # 右侧尺寸信息固定
|
|
|
|
|
|
# 主状态消息
|
|
|
self.status_var = tk.StringVar()
|
|
|
status_label = ttk.Label(status_frame, textvariable=self.status_var)
|
|
|
status_label.grid(row=0, column=0, sticky=tk.W)
|
|
|
|
|
|
# 窗口尺寸信息
|
|
|
self.size_var = tk.StringVar()
|
|
|
size_label = ttk.Label(status_frame, textvariable=self.size_var)
|
|
|
size_label.grid(row=0, column=1, sticky=tk.E)
|
|
|
|
|
|
# 初始化窗口尺寸显示
|
|
|
self.update_window_size()
|
|
|
|
|
|
# 绑定窗口resize事件
|
|
|
self.root.bind('<Configure>', self.on_window_resize)
|
|
|
|
|
|
def bind_shortcuts(self):
|
|
|
"""绑定快捷键"""
|
|
|
self.root.bind('<Control-s>', lambda e: self.save_note())
|
|
|
self.root.bind('<Control-n>', lambda e: self.clear_text())
|
|
|
|
|
|
def on_text_modified(self, event=None):
|
|
|
"""文本内容修改时的回调"""
|
|
|
# 使用after方法延迟检查,确保文本已经更新
|
|
|
self.root.after_idle(self.check_text_modified)
|
|
|
|
|
|
def check_text_modified(self):
|
|
|
"""检查文本是否被修改"""
|
|
|
current_content = self.text_area.get("1.0", tk.END).strip()
|
|
|
if current_content and not self.text_modified:
|
|
|
self.text_modified = True
|
|
|
self.update_title()
|
|
|
elif not current_content and self.text_modified:
|
|
|
self.text_modified = False
|
|
|
self.update_title()
|
|
|
|
|
|
def update_title(self):
|
|
|
"""更新窗口标题以显示修改状态"""
|
|
|
base_title = "FreeNote - 随手记录"
|
|
|
if self.text_modified:
|
|
|
self.root.title(f"{base_title} *")
|
|
|
else:
|
|
|
self.root.title(base_title)
|
|
|
|
|
|
def reset_modified_flag(self):
|
|
|
"""重置修改标志"""
|
|
|
self.text_modified = False
|
|
|
self.update_title()
|
|
|
|
|
|
def clear_text(self):
|
|
|
"""清空文本框内容"""
|
|
|
if self.text_modified:
|
|
|
# 如果文本已修改,询问用户是否确认清空
|
|
|
if not messagebox.askyesno("确认", "文本内容已修改但未保存,确定要清空吗?"):
|
|
|
return
|
|
|
|
|
|
# 清空文本框
|
|
|
self.text_area.delete("1.0", tk.END)
|
|
|
|
|
|
# 重置修改标志
|
|
|
self.reset_modified_flag()
|
|
|
|
|
|
# 更新状态栏
|
|
|
self.update_status("文本已清空")
|
|
|
|
|
|
def on_folder_focus_out(self, event=None):
|
|
|
"""目录框失去焦点时保存配置"""
|
|
|
new_val = self.folder_var.get().strip()
|
|
|
old_val = get_setting('folder')
|
|
|
|
|
|
if new_val and new_val != old_val:
|
|
|
# 更新配置
|
|
|
set_setting('folder', new_val)
|
|
|
self.update_status(f"保存文件夹{new_val}")
|
|
|
|
|
|
def on_filename_focus_out(self, event=None):
|
|
|
"""文件名框失去焦点时保存配置"""
|
|
|
new_val = self.filename_var.get().strip()
|
|
|
old_val = get_setting('filename')
|
|
|
|
|
|
if new_val and new_val != old_val:
|
|
|
# 更新配置
|
|
|
set_setting('filename', new_val)
|
|
|
self.update_status(f"文件名{new_val}")
|
|
|
|
|
|
def on_ext_change(self, event=None):
|
|
|
"""扩展名变化时保存配置"""
|
|
|
new_val = self.ext_var.get().strip()
|
|
|
old_val = get_setting('ext')
|
|
|
|
|
|
if new_val and new_val != old_val:
|
|
|
# 更新配置
|
|
|
set_setting('ext', new_val)
|
|
|
self.update_status(f"扩展名已更改为{new_val}")
|
|
|
|
|
|
def browse_folder(self):
|
|
|
"""浏览选择目录"""
|
|
|
folder = filedialog.askdirectory(initialdir=self.folder_var.get())
|
|
|
if folder:
|
|
|
self.folder_var.set(folder)
|
|
|
# 触发失去焦点事件来保存配置
|
|
|
self.on_folder_focus_out()
|
|
|
|
|
|
def save_note(self):
|
|
|
"""保存笔记"""
|
|
|
# 检查文本是否被修改
|
|
|
if not self.text_modified:
|
|
|
messagebox.showinfo("提示", "文本内容未修改,无需保存。")
|
|
|
return
|
|
|
|
|
|
content = self.text_area.get("1.0", tk.END)
|
|
|
|
|
|
try:
|
|
|
# 获取参数
|
|
|
filename_template = self.filename_var.get().strip()
|
|
|
folder = self.folder_var.get().strip()
|
|
|
ext = self.ext_var.get().strip()
|
|
|
|
|
|
# 定义覆盖确认回调
|
|
|
def overwrite_callback(filepath):
|
|
|
return messagebox.askyesno("确认", f"文件 {filepath} 已存在,是否覆盖?")
|
|
|
|
|
|
# 保存文件
|
|
|
filepath = save_note_content(
|
|
|
content, filename_template, folder, ext,
|
|
|
overwrite_callback=overwrite_callback
|
|
|
)
|
|
|
|
|
|
# 更新状态
|
|
|
self.update_status(f"已保存: {filepath}")
|
|
|
|
|
|
# 重置修改标志
|
|
|
self.reset_modified_flag()
|
|
|
|
|
|
except FileSaveError as e:
|
|
|
messagebox.showerror("错误", str(e))
|
|
|
except Exception as e:
|
|
|
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
|
|
|
|
|
def update_status(self, message):
|
|
|
"""更新状态栏"""
|
|
|
status_text = f"{datetime.datetime.now().strftime('%H:%M:%S')} - {message}"
|
|
|
self.status_var.set(status_text)
|
|
|
|
|
|
def update_window_size(self):
|
|
|
"""更新窗口尺寸显示"""
|
|
|
width = self.root.winfo_width()
|
|
|
height = self.root.winfo_height()
|
|
|
self.size_var.set(f"{width} x {height}")
|
|
|
|
|
|
def on_window_resize(self, event=None):
|
|
|
"""窗口尺寸变化事件处理"""
|
|
|
# 只响应主窗口的resize事件,忽略子组件的事件
|
|
|
if event and event.widget == self.root:
|
|
|
self.update_window_size()
|
|
|
|
|
|
def show_template_help(self):
|
|
|
"""显示模板变量帮助"""
|
|
|
help_text = get_template_help_text()
|
|
|
messagebox.showinfo("文件名模板帮助", help_text)
|
|
|
|
|
|
|
|
|
def create_app():
|
|
|
"""创建并返回应用程序实例"""
|
|
|
root = tk.Tk()
|
|
|
app = FreeNoteApp(root)
|
|
|
return root, app
|