You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/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