From 3721237e3203696cd42c05000815d7ddd851a499 Mon Sep 17 00:00:00 2001 From: Nigel Date: Thu, 25 Jun 2026 20:34:02 +0100 Subject: [PATCH] Add "horizontal rule" option to images dialog Auto-illustration / HTML images dialogs now have a "Horizontal rule" checkbox, that causes the "illo" to be added as `
` with the illo as a background image. This is intended for where there is an embellished thought break that the PPer wants to preserve instead of just using a plain straight line. Fixes #1882 Rebase/merge work --- src/guiguts/application.py | 1 + src/guiguts/data/html/html_header.txt | 11 +- src/guiguts/html_tools.py | 193 +++++++++++++++++--------- src/guiguts/preferences.py | 1 + tests/expected/htmlconvert1.txt | 11 +- tests/expected/htmlconvert2.txt | 11 +- 6 files changed, 162 insertions(+), 66 deletions(-) diff --git a/src/guiguts/application.py b/src/guiguts/application.py index a470bada..18af2548 100644 --- a/src/guiguts/application.py +++ b/src/guiguts/application.py @@ -640,6 +640,7 @@ def initialize_preferences(self) -> None: preferences.set_default(PrefKey.HTML_IMAGE_ALIGNMENT, "center") preferences.set_default(PrefKey.HTML_IMAGE_CAPTION_P, True) preferences.set_default(PrefKey.HTML_IMAGE_DECORATIVE_ONLY, False) + preferences.set_default(PrefKey.HTML_IMAGE_EMBELLISHED_HR, False) preferences.set_default(PrefKey.HTML_IMAGE_FIND_FIRST, False) preferences.set_default(PrefKey.HTML_LINKS_ALPHABETIC, False) preferences.set_default(PrefKey.HTML_LINKS_HIDE_PAGE, False) diff --git a/src/guiguts/data/html/html_header.txt b/src/guiguts/data/html/html_header.txt index f94bdae4..5af32fdd 100644 --- a/src/guiguts/data/html/html_header.txt +++ b/src/guiguts/data/html/html_header.txt @@ -46,7 +46,16 @@ hr.full {width: 95%; margin-left: 2.5%; margin-right: 2.5%;} hr.r5 {width: 5%; margin-top: 1em; margin-bottom: 1em; margin-left: 47.5%; margin-right: 47.5%;} hr.r65 {width: 65%; margin-top: 3em; margin-bottom: 3em; margin-left: 17.5%; margin-right: 17.5%;} - +hr.hrimage { + background-repeat: no-repeat; + background-position: center; + background-size: contain; + height: 2em; + width: 100%; + border: 0; + margin: 1em 0; +} + div.chapter {page-break-before: always;} h2.nobreak {page-break-before: avoid;} diff --git a/src/guiguts/html_tools.py b/src/guiguts/html_tools.py index 3eacd54d..baf07de1 100644 --- a/src/guiguts/html_tools.py +++ b/src/guiguts/html_tools.py @@ -102,7 +102,9 @@ def __init__(self, dlg_type: ImageDialogType) -> None: for r in (0, 5): self.top_frame.rowconfigure(r, weight=0) - self.top_frame.rowconfigure(3, weight=1) + self.top_frame.rowconfigure( + 3 if dlg_type == ImageDialogType.EDIT_ILLUS else 1, weight=1 + ) # File file_frame = ttk.LabelFrame(self.top_frame, text="File", padding=2) @@ -126,28 +128,47 @@ def __init__(self, dlg_type: ImageDialogType) -> None: # Buttons to see prev/next file & whether decorative-only file_btn_frame = ttk.Frame(file_frame) - file_btn_frame.grid(row=1, column=0, sticky="EW", pady=(3, 0)) - file_btn_frame.columnconfigure(0, weight=1) - ttk.Checkbutton( - file_btn_frame, - text="Decorative only", - variable=PersistentBoolean(PrefKey.HTML_IMAGE_DECORATIVE_ONLY), - ).grid(row=0, column=0, sticky="W") + file_btn_frame.grid(row=1, column=0, pady=(3, 0)) + file_btn_frame.columnconfigure(0, weight=1, uniform="uniimg") + file_btn_frame.columnconfigure(1, weight=1, uniform="uniimg") if dlg_type != ImageDialogType.EDIT_ILLUS: ttk.Button( file_btn_frame, text="Prev File", command=lambda: self.next_file(reverse=True), - ).grid(row=0, column=1, padx=2, sticky="E") + ).grid(row=0, column=0, padx=2) ttk.Button( file_btn_frame, text="Next File", command=lambda: self.next_file(reverse=False), - ).grid(row=0, column=2, padx=(2, 0), sticky="E") + ).grid(row=0, column=1, padx=(2, 0)) + + file_chk_frame = ttk.Frame(file_frame) + file_chk_frame.grid(row=2, column=0, pady=(3, 0)) + deco = ttk.Checkbutton( + file_chk_frame, + text="Decorative only", + variable=PersistentBoolean(PrefKey.HTML_IMAGE_DECORATIVE_ONLY), + ) + deco.grid(row=0, column=0, padx=(0, 10)) + ToolTip( + deco, "Image is decorative and should not be announced by screen readers" + ) + if dlg_type != ImageDialogType.EDIT_ILLUS: + hr = ttk.Checkbutton( + file_chk_frame, + text="Horizontal rule", + variable=PersistentBoolean(PrefKey.HTML_IMAGE_EMBELLISHED_HR), + command=self.hr_update, + ) + hr.grid(row=0, column=1, padx=(10, 0)) + ToolTip(hr, "Use
element rather than for horizontal rule") + file_chk_frame.columnconfigure(0, weight=1, uniform="uniimg") + file_chk_frame.columnconfigure(1, weight=1, uniform="uniimg") # Label to display thumbnail of image - allocate a # square space the same width as the filename frame - frame_width = 300 + frame_width = 280 thumbnail_frame = ttk.LabelFrame(self.top_frame, text="Thumbnail") thumbnail_frame.grid(row=1, column=0, sticky="NSEW") thumbnail_frame.columnconfigure(0, minsize=frame_width, weight=1) @@ -164,15 +185,17 @@ def __init__(self, dlg_type: ImageDialogType) -> None: caption_frame.grid(row=2, column=0, sticky="NSEW") caption_frame.columnconfigure(0, weight=1) self.caption_textvariable = tk.StringVar(self, "") - ttk.Entry( + self.wcaption_text = ttk.Entry( caption_frame, textvariable=self.caption_textvariable, - ).grid(row=0, column=0, sticky="NSEW") - ttk.Checkbutton( + ) + self.wcaption_text.grid(row=0, column=0, sticky="NSEW") + self.wcaption_chk = ttk.Checkbutton( caption_frame, text="Use

markup for caption", variable=PersistentBoolean(PrefKey.HTML_IMAGE_CAPTION_P), - ).grid(row=2, column=0, sticky="NS") + ) + self.wcaption_chk.grid(row=2, column=0, sticky="NS") # Alt text alt_frame = ttk.LabelFrame(self.top_frame, text="Alt text", padding=2) @@ -180,7 +203,7 @@ def __init__(self, dlg_type: ImageDialogType) -> None: alt_frame.columnconfigure(0, weight=1) alt_frame.rowconfigure(0, weight=1) scrollbar = ttk.Scrollbar(alt_frame, orient="vertical") - self.alt_text_wgt = tk.Text( + self.walt_text = tk.Text( alt_frame, yscrollcommand=scrollbar.set, height=3, @@ -193,9 +216,8 @@ def __init__(self, dlg_type: ImageDialogType) -> None: insertbackground=maintext().cget("insertbackground"), insertborderwidth=maintext().cget("insertborderwidth"), ) - - scrollbar.config(command=self.alt_text_wgt.yview) - self.alt_text_wgt.grid(row=0, column=0, sticky="NSEW") + scrollbar.config(command=self.walt_text.yview) + self.walt_text.grid(row=0, column=0, sticky="NSEW") scrollbar.grid(row=0, column=1, sticky="NS") # Geometry @@ -216,48 +238,53 @@ def width_updated(new_value: str) -> bool: ttk.Label(width_height_frame, text="Width").grid(row=0, column=0, padx=4) self.width_textvariable = tk.StringVar(self, "") - ttk.Entry( + self.wwidth = ttk.Entry( width_height_frame, textvariable=self.width_textvariable, width=8, validate="all", validatecommand=(self.register(width_updated), "%P"), - ).grid(row=0, column=1, sticky="NSEW", padx=(4, 10)) + ) + self.wwidth.grid(row=0, column=1, sticky="NSEW", padx=(4, 10)) ttk.Label(width_height_frame, text="Height").grid( row=0, column=2, padx=(10, 4) ) self.height_textvariable = tk.StringVar(self, "") - ttk.Entry( + self.wheight = ttk.Entry( width_height_frame, textvariable=self.height_textvariable, width=8, state=tk.DISABLED, - ).grid(row=0, column=3, sticky="NSEW", padx=4) + ) + self.wheight.grid(row=0, column=3, sticky="NSEW", padx=4) unit_frame = ttk.Frame(geom_frame) unit_frame.grid(row=1, column=0, pady=5) unit_textvariable = PersistentString(PrefKey.HTML_IMAGE_UNIT) - ttk.Radiobutton( + self.wrp = ttk.Radiobutton( unit_frame, text="%", variable=unit_textvariable, value="%", command=self.update_geometry_fields, - ).grid(row=0, column=4, sticky="NSEW", padx=10) - ttk.Radiobutton( + ) + self.wrp.grid(row=0, column=4, sticky="NSEW", padx=10) + self.wre = ttk.Radiobutton( unit_frame, text="em", variable=unit_textvariable, value="em", command=self.update_geometry_fields, - ).grid(row=0, column=5, sticky="NSEW", padx=10) - ttk.Radiobutton( + ) + self.wre.grid(row=0, column=5, sticky="NSEW", padx=10) + self.wrx = ttk.Radiobutton( unit_frame, text="px", variable=unit_textvariable, value="px", command=self.update_geometry_fields, - ).grid(row=0, column=6, sticky="NSEW", padx=10) + ) + self.wrx.grid(row=0, column=6, sticky="NSEW", padx=10) self.file_info_textvariable = tk.StringVar(self, "") ttk.Label( @@ -285,24 +312,27 @@ def width_updated(new_value: str) -> bool: align_frame.columnconfigure(1, weight=1) align_frame.columnconfigure(2, weight=1) align_textvariable = PersistentString(PrefKey.HTML_IMAGE_ALIGNMENT) - ttk.Radiobutton( + self.wal = ttk.Radiobutton( align_frame, text="Left", variable=align_textvariable, value="left", - ).grid(row=0, column=0, sticky="NSW", padx=10) - ttk.Radiobutton( + ) + self.wal.grid(row=0, column=0, sticky="NSW", padx=10) + self.wac = ttk.Radiobutton( align_frame, text="Center", variable=align_textvariable, value="center", - ).grid(row=0, column=1, sticky="NSW", padx=10) - ttk.Radiobutton( + ) + self.wac.grid(row=0, column=1, sticky="NSW", padx=10) + self.war = ttk.Radiobutton( align_frame, text="Right", variable=align_textvariable, value="right", - ).grid(row=0, column=2, sticky="NSW", padx=10) + ) + self.war.grid(row=0, column=2, sticky="NSW", padx=10) # Buttons to Find illos and Convert to HTML btn_frame = ttk.Frame(self.top_frame, padding=2) @@ -330,6 +360,8 @@ def width_updated(new_value: str) -> bool: command=self.do_markup, ).grid(row=1, column=0, sticky="NSEW", padx=2) + self.hr_update() + if dlg_type in (ImageDialogType.AUTO_ILLUS, ImageDialogType.EDIT_ILLUS): ttk.Button( btn_frame, @@ -355,6 +387,35 @@ def generic_advance(self) -> None: else: self.choose_file() + def hr_update(self) -> None: + """Update widgets' states based on HR flag.""" + if isinstance(self, HTMLImageEditDialog): + return + state = ( + tk.DISABLED + if preferences.get(PrefKey.HTML_IMAGE_EMBELLISHED_HR) + else tk.ACTIVE + ) + for wgt in ( + self.wcaption_text, + self.wcaption_chk, + self.wwidth, + self.wheight, + self.wrp, + self.wre, + self.wrx, + self.override_checkbutton, + self.wal, + self.wac, + self.war, + ): + wgt["state"] = state + self.walt_text["state"] = ( + tk.DISABLED + if preferences.get(PrefKey.HTML_IMAGE_EMBELLISHED_HR) + else tk.NORMAL + ) + def load_file(self, file_name: str) -> None: """Load given image file.""" assert file_name @@ -514,7 +575,7 @@ def find_illo_markup(self) -> None: caption = re.sub("^ +", "", caption) self.caption_textvariable.set(caption) # Clear alt text, ready for user to type in required string - self.alt_text_wgt.delete("1.0", "end") + self.walt_text.delete("1.0", "end") self.next_file() self.lift() @@ -681,9 +742,9 @@ def find_html_img_markup(self, load: bool) -> None: ) # Display alt text - self.alt_text_wgt.delete("1.0", "end") + self.walt_text.delete("1.0", "end") if alt_value_start >= 0 and alt_value_end >= 0: - self.alt_text_wgt.insert("1.0", img_chunk[alt_value_start:alt_value_end]) + self.walt_text.insert("1.0", img_chunk[alt_value_start:alt_value_end]) # Display presentation flag preferences.set(PrefKey.HTML_IMAGE_DECORATIVE_ONLY, presentation_start >= 0) self.load_file(os.path.join(os.path.dirname(the_file().filename), filename)) @@ -708,7 +769,7 @@ def update_markup(self) -> None: maintext().undo_block_begin() # Update (or insert) alt text - escape any double quotes - alt = self.alt_text_wgt.get("1.0", "end").replace('"', """).strip() + alt = self.walt_text.get("1.0", "end").replace('"', """).strip() try: alt_start = maintext().index(self.mark_alt_start) alt_end = maintext().index(self.mark_alt_end) @@ -790,7 +851,7 @@ def convert_to_html(self) -> None: # If caption already had p markup, e.g. for right justify, avoid double markup caption = caption.replace("

None: ) # Construct HTML - html = f'

\n' - html += f' \n' - html += f"{caption}
" + if preferences.get(PrefKey.HTML_IMAGE_EMBELLISHED_HR): + html = ( + f'
' + ) + else: + html = f'
\n' + html += f' \n' + html += f"{caption}
" maintext().undo_block_begin() # If in Auto-Illustration mode, replace [Illustration...] with HTML @@ -840,26 +906,27 @@ def convert_to_html(self) -> None: self.illo_range = None # Now to insert CSS at end of style block, except for px sizes - insert_point = maintext().search("