Skip to content

Commit

Permalink
Add "goto" and "see img" buttons
Browse files Browse the repository at this point in the history
Status bar labels changed to buttons, to give more modern look,
and because buttons change appearance when mouse hovers
over them, rather than coding it, and because they are buttons!

Improved `get_current_image_name` to cope with cursor being
coincident with one or more page marks. Since typically they
are at the start of a line (at least initially) it can be confusing to
place the cursor at the start of a line and the page number
being reported as the previous page.
  • Loading branch information
windymilla committed Dec 12, 2023
1 parent 636fdfb commit 0373371
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 45 deletions.
86 changes: 76 additions & 10 deletions src/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os.path
import re
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinter import filedialog, messagebox, simpledialog

from mainwindow import maintext

Expand Down Expand Up @@ -250,14 +250,25 @@ def get_current_image_name(self):
insert cursor is.
Returns:
Basename of image file (= name of preceding page mark).
Empty string if no page mark before insert cursor.
Basename of image file. Empty string if none found.
"""
mark = maintext().get_insert_index()
while mark := maintext().mark_previous(mark):
insert = maintext().get_insert_index()
mark = insert
good_mark = ""
# First check for page marks at the current cursor position & return last one
while (mark := maintext().mark_next(mark)) and maintext().compare(
mark, "==", insert
):
if is_page_mark(mark):
return img_from_page_mark(mark)
return ""
good_mark = mark
# If not, then find page mark before current position
if not good_mark:
mark = insert
while mark := maintext().mark_previous(mark):
if is_page_mark(mark):
good_mark = mark
break
return img_from_page_mark(good_mark)

def get_current_image_path(self):
"""Return the path of the image file for the page where the insert
Expand All @@ -274,6 +285,60 @@ def get_current_image_path(self):
else:
return ""

def goto_line(self):
"""Go to the line number the user enters"""
line_num = simpledialog.askinteger(
"Go To Line", "Line number", parent=maintext()
)
if line_num is not None:
maintext().set_insert_index(f"{line_num}.0", see=True)

def goto_page(self):
"""Go to the page the user enters"""
page_num = simpledialog.askstring(
"Go To Page", "Image number", parent=maintext()
)
if page_num is not None:
try:
index = maintext().index(PAGEMARK_PREFIX + page_num)
except tk._tkinter.TclError:
# Bad page number
return
maintext().set_insert_index(index, see=True)

def prev_page(self):
"""Go to the start of the previous page"""
self._next_prev_page(-1)

def next_page(self):
"""Go to the start of the next page"""
self._next_prev_page(1)

def _next_prev_page(self, direction):
"""Go to the page before/after the current one
Always moves backward/forward in file, even if cursor and page mark(s)
are coincident or multiple coincident page marks. Will not remain in
the same location unless no further page marks are found.
Args:
direction: Positive to go to next page; negative for previous page
"""
if direction < 0:
mark_next_previous = maintext().mark_previous
else:
mark_next_previous = maintext().mark_next

insert = maintext().get_insert_index()
cur_page = self.get_current_image_name()
mark = PAGEMARK_PREFIX + cur_page if cur_page else insert
while mark := mark_next_previous(mark):
if is_page_mark(mark) and maintext().compare(mark, "!=", insert):
maintext().set_insert_index(mark, see=True)
return
# TODO: Ring bell or something
return


def is_page_mark(mark):
"""Check whether mark is a page mark, e.g. "Pg027".
Expand All @@ -292,12 +357,13 @@ def img_from_page_mark(mark):
Args:
mark: String containing name of mark whose image is needed.
Does not check if mark is a page mark
Does not check if mark is a page mark. If it is not, the
full string is returned.
Returns:
True if string matches the format of page mark names.
Image name.
"""
return mark[len(PAGEMARK_PREFIX) :]
return mark.removeprefix(PAGEMARK_PREFIX)


def bin_name(basename):
Expand Down
25 changes: 19 additions & 6 deletions src/guiguts.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def init_view_menu(self, parent):
menu_view = Menu(parent, "~View")
menu_view.add_button("~Dock", self.mainwindow.dock_image, "Cmd/Ctrl+D")
menu_view.add_button("~Float", self.mainwindow.float_image, "Cmd/Ctrl+F")
menu_view.add_button("~Load Image", self.load_image, "Cmd/Ctrl+L")
menu_view.add_button("~Load Image", self.load_image)

def init_help_menu(self, parent):
"""Create the Help menu."""
Expand All @@ -190,17 +190,30 @@ def init_os_menu(self, parent):
# Lay out statusbar
def init_statusbar(self, statusbar):
"""Add labels to initialize the statusbar"""
pattern = re.compile(r"(\d+)\.(\d+)")

index_pattern = re.compile(r"(\d+)\.(\d+)")
statusbar.add(
"rowcol",
lambda: pattern.sub(r"L:\1 C:\2", maintext().get_insert_index()),
width=10,
update=lambda: index_pattern.sub(
r"L:\1 C:\2", maintext().get_insert_index()
),
)
statusbar.add_binding("rowcol", "<ButtonRelease-1>", self.file.goto_line)

statusbar.add(
"img",
lambda: "Img: " + self.file.get_current_image_name(),
# width=10,
update=lambda: "Img: " + self.file.get_current_image_name(),
)
statusbar.add_binding("img", "<ButtonRelease-1>", self.file.goto_page)

statusbar.add("prev img", text="<", width=1)
statusbar.add_binding("prev img", "<ButtonRelease-1>", self.file.prev_page)

statusbar.add("see img", text="See Img", width=7)
statusbar.add_binding("see img", "<ButtonRelease-1>", self.load_image)

statusbar.add("next img", text=">", width=1)
statusbar.add_binding("next img", "<ButtonRelease-1>", self.file.next_page)


if __name__ == "__main__":
Expand Down
89 changes: 60 additions & 29 deletions src/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

TEXTIMAGE_WINDOW_ROW = 0
TEXTIMAGE_WINDOW_COL = 0
STATUSBAR_ROW = 1
SEPARATOR_ROW = 1
SEPARATOR_COL = 0
STATUSBAR_ROW = 2
STATUSBAR_COL = 0
MIN_PANE_WIDTH = 20

Expand Down Expand Up @@ -55,6 +57,12 @@ def __init__(self):
sticky="NSEW",
)

ttk.Separator(root()).grid(
column=SEPARATOR_COL,
row=SEPARATOR_ROW,
sticky="NSEW",
)

self.paned_window = tk.PanedWindow(
root(), orient=tk.HORIZONTAL, sashwidth=4, sashrelief=tk.GROOVE
)
Expand All @@ -68,16 +76,11 @@ def __init__(self):
wrap="none",
autoseparators=True,
maxundo=-1,
highlightthickness=0,
)
self.paned_window.add(maintext().frame, minsize=MIN_PANE_WIDTH)

MainWindow.mainimage = MainImage(self.paned_window)
# self.paned_window.add(mainimage(), minsize=MIN_PANE_WIDTH)
return
if preferences.get("ImageWindow") == "Docked":
self.dock_image()
else:
self.float_image()

def float_image(self, *args):
"""Float the image into a separate window"""
Expand Down Expand Up @@ -340,6 +343,7 @@ def set_insert_index(self, index, see=False):
self.mark_set(tk.INSERT, index)
if see:
self.see(tk.INSERT)
self.focus_set()


class MainImage(tk.Frame):
Expand Down Expand Up @@ -528,8 +532,7 @@ def load_image(self, filename=None):
"""Load or clear the given image file.
Args:
filename: Optional name of image file. If none given, clear image
and display "No image" label.
filename: Optional name of image file. If none given, clear image.
"""
if os.path.isfile(filename):
self.image = Image.open(filename)
Expand All @@ -554,7 +557,7 @@ def is_image_loaded(self):
class StatusBar(ttk.Frame):
"""Statusbar at the bottom of the screen.
Labels in statusbar can be automatically or manually updated.
Fields in statusbar can be automatically or manually updated.
"""

def __init__(self, parent):
Expand All @@ -563,45 +566,73 @@ def __init__(self, parent):
Args:
parent: Frame to contain status bar.
"""
super().__init__(parent, borderwidth=1, relief=tk.SUNKEN)
self.labels = {}
super().__init__(parent)
self.fields = {}
self.callbacks = {}
self._update()

def add(self, key, callback=None, **kwargs):
"""Add label to status bar
def add(self, key, update=None, **kwargs):
"""Add field to status bar
Args:
key - Key to use to refer to label
callback - Optional callback function that returns a string.
If supplied, label will be regularly updated automatically with
the string returned by ``callback()``. If argument not given,
key: Key to use to refer to field.
update: Optional callback function that returns a string.
If supplied, field will be regularly updated automatically with
the string returned by ``update()``. If argument not given,
application is responsible for updating, using ``set(key)``.
"""
kwargs["borderwidth"] = 1
kwargs["relief"] = tk.RIDGE
self.labels[key] = tk.Label(self, kwargs)
self.callbacks[key] = callback
self.labels[key].grid(column=len(self.labels), row=0)
self.fields[key] = ttk.Button(self, takefocus=0, **kwargs)
self.callbacks[key] = update
self.fields[key].grid(column=len(self.fields), row=0)

def set(self, key, value):
"""Set label in statusbar to given value.
"""Set field in statusbar to given value.
Args:
key - Key to refer to label.
value - String to use to update label.
key: Key to refer to field.
value: String to use to update field.
"""
self.labels[key].config(text=value)
self.fields[key].config(text=value)

def _update(self):
"""Update labels in statusbar that have callbacks. Updates every
"""Update fields in statusbar that have callbacks. Updates every
200 milliseconds.
"""
for key in self.labels:
for key in self.fields:
if self.callbacks[key]:
self.set(key, self.callbacks[key]())
self.after(200, self._update)

def add_binding(self, key, event, callback):
"""Add an action to be executed when the given event occurs
Args:
key: Key to refer to field.
callback: Function to be called when event occurs.
event: Event to trigger action. Use button release to avoid
clash with button activate appearance behavior.
"""
mouse_bind(self.fields[key], event, lambda *args: callback())


def mouse_bind(widget, event, callback):
"""Bind mouse button callback to event on widget.
If binding is to mouse button 2 or 3, also bind the other button
to support all platforms and 2-button mice.
Args:
widget: Widget to bind to
event: Event string to trigger callback
callback: Function to be called when event occurs
"""
widget.bind(event, callback)

if match := re.match(r"(<.*Button.*)([23])(>)", event):
other_button = "2" if match.group(2) == "3" else "3"
other_event = match.group(1) + other_button + match.group(3)
widget.bind(other_event, callback)


def root():
"""Return the single instance of Root"""
Expand Down

0 comments on commit 0373371

Please sign in to comment.