Skip to content

Commit 58331c7

Browse files
committed
touchy: fit the window to the monitor
Touchy has no scrolling, so when content is larger than the display the window grows past the screen edge and controls become unreachable. The worst offender is structural: a GtkNotebook sizes to its largest page, so the small visible page (the buttons) was forced as wide as the hidden Preferences page, and the tall tool-table listing grew the window on reload. - Wrap each notebook page in a scroller, so the notebook sizes to the current page and oversized pages (Preferences, a long tool table) scroll instead of growing the window. This alone keeps the height in check and cuts the width from 1346 to 1094 px. - Wrap the whole window in a scroller and bound it to the monitor work area, so it can never exceed the screen. - Hide the handwheel column on the Preferences tab (you are not jogging there) so the wide settings page gets the full width. - When the content still does not fit a narrow screen, float a non-modal info bar over the content offering to shrink the fonts: Shrink to fit and save / Not now / Never ask again. Accepting scales to the largest fitting size, rounds to whole points, saves them, and updates the pickers to match. It is only shown when it does not fit and never repeated once it fits; Not now is remembered per screen size (fit_skip_size) so it is not offered again until the screen changes, and Never ask again (fit_fonts) disables it entirely. - Add an 'Offer to shrink fonts to fit the screen' checkbox to Preferences / Display Options to re-enable the offer after Never ask again, and document the behaviour.
1 parent 5f10ab7 commit 58331c7

3 files changed

Lines changed: 271 additions & 4 deletions

File tree

docs/src/gui/touchy.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,27 @@ cut off, reduce the DPI setting.
9696

9797
All other font sizes can be changed on the Preferences tab.
9898

99+
=== Fitting the screen
100+
101+
Touchy bounds its window to the monitor and scrolls any tab whose content
102+
is larger than the screen, so the window never grows past the display
103+
edge (for example after loading a large tool table). The handwheel column
104+
is hidden on the Preferences tab so the settings have the full width.
105+
106+
If the interface is still too large for a small screen, Touchy offers,
107+
once, to shrink the display fonts to fit:
108+
109+
* *Shrink to fit and save* scales the fonts to the largest size that fits
110+
and saves them as your preference (the font selectors update to match).
111+
* *Not now* leaves the fonts unchanged and does not ask again for this
112+
screen size; it is offered again only if you move to a smaller screen.
113+
* *Never ask again* disables the offer on every screen.
114+
115+
To turn the offer back on, tick *Offer to shrink fonts to fit the screen*
116+
in Preferences / Display Options, or set `fit_fonts = ask` in
117+
`~/.touchy_preferences` (the *Never ask again* choice stores
118+
`fit_fonts = never`).
119+
99120
=== Macros
100121

101122
Touchy can invoke O-word macros using the MDI interface. To configure

src/emc/usr_intf/touchy/touchy.glade

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3173,7 +3173,7 @@ F1 S1</property>
31733173
<object class="GtkTable" id="table3">
31743174
<property name="visible">True</property>
31753175
<property name="can-focus">False</property>
3176-
<property name="n-rows">5</property>
3176+
<property name="n-rows">6</property>
31773177
<property name="n-columns">3</property>
31783178
<child>
31793179
<placeholder/>
@@ -3402,6 +3402,27 @@ F1 S1</property>
34023402
<property name="x-padding">10</property>
34033403
</packing>
34043404
</child>
3405+
<child>
3406+
<object class="GtkCheckButton" id="fitfontscheck">
3407+
<property name="label" translatable="yes">Offer to shrink fonts to fit the screen</property>
3408+
<property name="visible">True</property>
3409+
<property name="can-focus">False</property>
3410+
<property name="receives-default">False</property>
3411+
<property name="focus-on-click">False</property>
3412+
<property name="draw-indicator">True</property>
3413+
<signal name="toggled" handler="on_fitfontscheck_toggled" swapped="no"/>
3414+
</object>
3415+
<packing>
3416+
<property name="left-attach">0</property>
3417+
<property name="right-attach">3</property>
3418+
<property name="top-attach">5</property>
3419+
<property name="bottom-attach">6</property>
3420+
<property name="x-options">GTK_FILL</property>
3421+
<property name="y-options"/>
3422+
<property name="x-padding">10</property>
3423+
<property name="y-padding">6</property>
3424+
</packing>
3425+
</child>
34053426
</object>
34063427
</child>
34073428
</object>

src/emc/usr_intf/touchy/touchy.py

Lines changed: 228 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ def __init__(self, inifile):
119119
self.wTree.get_object('MainWindow').set_can_focus(True)
120120
self.wTree.get_object('MainWindow').grab_focus()
121121

122+
# Stop the window growing past the screen (see method).
123+
self._constrain_to_monitor()
124+
122125
self.num_mdi_labels = 11
123126
self.num_filechooser_labels = 11
124127
self.num_listing_labels = 20
@@ -148,6 +151,13 @@ def __init__(self, inifile):
148151
self.err_textcolor = self.prefs.getpref('err_textcolor', 'default', str)
149152
self.window_geometry = self.prefs.getpref('window_geometry', 'default', str)
150153
self.window_max = self.prefs.getpref('window_force_max', 'false', bool)
154+
self.fit_fonts = self.prefs.getpref('fit_fonts', 'ask', str)
155+
self.fit_skip_size = self.prefs.getpref('fit_skip_size', '', str)
156+
check = self.wTree.get_object("fitfontscheck")
157+
if check:
158+
self._setting_fit_check = True
159+
check.set_active(self.fit_fonts != 'never')
160+
self._setting_fit_check = False
151161

152162
# initial screen setup
153163
if os.path.exists(themedir):
@@ -347,6 +357,7 @@ def __init__(self, inifile):
347357
"on_dro_mm_clicked" : self.dro_mm,
348358
"on_errorfontbutton_font_set" : self.change_error_font,
349359
"on_listingfontbutton_font_set" : self.change_listing_font,
360+
"on_fitfontscheck_toggled" : self.fit_fonts_toggled,
350361
"on_estop_clicked" : self.linuxcnc.estop,
351362
"on_estop_reset_clicked" : self.linuxcnc.estop_reset,
352363
"on_machine_off_clicked" : self.linuxcnc.machine_off,
@@ -439,9 +450,15 @@ def quit(self, unused):
439450

440451

441452
def tabselect(self, notebook, b, tab):
442-
# new_tab=notebook.get_nth_page(tab)
443-
# old_tab=notebook.get_nth_page(self.tab)
444453
self.tab = tab
454+
# The handwheel is for jogging, not setup: hide it on the
455+
# Preferences tab so the wide settings page gets the full width.
456+
wheel = self.wTree.get_object("wheel")
457+
if wheel is not None and getattr(self, "_prefs_index", -1) >= 0:
458+
if tab == self._prefs_index:
459+
wheel.hide()
460+
else:
461+
wheel.show()
445462
# for c in self._dynamic_childs:
446463
# if new_tab.__gtype__.name =='GtkSocket':
447464
# w= new_tab.get_plug_window()
@@ -628,6 +645,21 @@ def change_listing_font(self, fontbutton):
628645
self.listing_font = Pango.FontDescription(self.listing_font_name)
629646
self.setfont()
630647

648+
def fit_fonts_toggled(self, button):
649+
# Re-enable or disable the offer to shrink the fonts to fit a
650+
# small screen. Checking it also forgets any per-screen decline
651+
# so the offer can appear again.
652+
if getattr(self, "_setting_fit_check", False):
653+
return
654+
if button.get_active():
655+
self.fit_fonts = 'ask'
656+
self.prefs.putpref('fit_fonts', 'ask', str)
657+
self.fit_skip_size = ''
658+
self.prefs.putpref('fit_skip_size', '', str)
659+
else:
660+
self.fit_fonts = 'never'
661+
self.prefs.putpref('fit_fonts', 'never', str)
662+
631663
def change_theme(self, b):
632664
tree_iter = b.get_active_iter()
633665
if tree_iter is not None:
@@ -639,6 +671,198 @@ def change_theme(self, b):
639671
settings = Gtk.Settings.get_default()
640672
settings.set_string_property("gtk-theme-name", theme, "")
641673

674+
def _wrap_notebook_pages(self):
675+
# A notebook sizes to its widest/tallest page, so the small visible
676+
# page (e.g. the buttons) is forced as large as the hidden settings
677+
# page. Wrap each page in a scroller so the notebook sizes to the
678+
# current page; oversized pages scroll instead of growing the window.
679+
nb = self.wTree.get_object("notebook1")
680+
if nb is None:
681+
return
682+
self._prefs_index = -1
683+
for i in range(nb.get_n_pages()):
684+
page = nb.get_nth_page(i)
685+
if isinstance(page, (Gtk.ScrolledWindow, Gtk.Socket)):
686+
continue
687+
if (nb.get_tab_label_text(page) or "").strip() == "Preferences":
688+
self._prefs_index = i
689+
label = nb.get_tab_label(page)
690+
scroller = Gtk.ScrolledWindow()
691+
scroller.set_policy(Gtk.PolicyType.AUTOMATIC,
692+
Gtk.PolicyType.AUTOMATIC)
693+
scroller.set_min_content_width(0)
694+
scroller.set_min_content_height(0)
695+
nb.remove_page(i)
696+
scroller.add(page)
697+
viewport = scroller.get_child()
698+
if isinstance(viewport, Gtk.Viewport):
699+
viewport.set_shadow_type(Gtk.ShadowType.NONE)
700+
scroller.show_all()
701+
nb.insert_page(scroller, label, i)
702+
703+
def _constrain_to_monitor(self):
704+
# Wrap the content in a scroller so the window can be bounded to the
705+
# monitor; touchy has no scrolling otherwise and grows past the screen.
706+
try:
707+
self._wrap_notebook_pages()
708+
win = self.wTree.get_object("MainWindow")
709+
child = win.get_child()
710+
if child is None or isinstance(child, Gtk.ScrolledWindow):
711+
return
712+
scroller = Gtk.ScrolledWindow()
713+
scroller.set_policy(Gtk.PolicyType.AUTOMATIC,
714+
Gtk.PolicyType.AUTOMATIC)
715+
win.remove(child)
716+
scroller.add(child)
717+
viewport = scroller.get_child()
718+
if isinstance(viewport, Gtk.Viewport):
719+
viewport.set_shadow_type(Gtk.ShadowType.NONE)
720+
# Float the fit offer over the content (no modal pop-up, and it
721+
# does not add to the window's minimum size).
722+
overlay = Gtk.Overlay()
723+
overlay.add(scroller)
724+
self._infobar = self._build_fit_infobar()
725+
self._infobar.set_halign(Gtk.Align.FILL)
726+
self._infobar.set_valign(Gtk.Align.START)
727+
overlay.add_overlay(self._infobar)
728+
win.add(overlay)
729+
overlay.show_all()
730+
self._infobar.hide()
731+
# Fit on map, once the monitor is known.
732+
self._fitted = False
733+
win.connect("map-event", self._fit_to_monitor, scroller, child)
734+
except Exception:
735+
pass
736+
737+
def _build_fit_infobar(self):
738+
bar = Gtk.InfoBar()
739+
bar.set_message_type(Gtk.MessageType.QUESTION)
740+
label = Gtk.Label(label=_("The interface is larger than this screen."))
741+
bar.get_content_area().add(label)
742+
bar.add_button(_("Shrink to fit and save"), 3)
743+
bar.add_button(_("Not now"), 1)
744+
bar.add_button(_("Never ask again"), 2)
745+
bar.connect("response", self._fit_infobar_response)
746+
return bar
747+
748+
def _fit_to_monitor(self, win, event, scroller, child):
749+
if self._fitted:
750+
return False
751+
self._fitted = True
752+
# Bound the window to the work area by sizing the scroller to it
753+
# (no max-size hint, so the window keeps its normal controls).
754+
# Page wrapping keeps the height in check; only the position readout
755+
# plus handwheel can exceed a narrow screen's width, and the scroller
756+
# handles that until (and unless) the user opts to shrink.
757+
try:
758+
display = win.get_display()
759+
gdkwin = win.get_window()
760+
if gdkwin is not None:
761+
monitor = display.get_monitor_at_window(gdkwin)
762+
else:
763+
monitor = display.get_primary_monitor()
764+
if monitor is None:
765+
monitor = display.get_monitor(0)
766+
area = monitor.get_workarea()
767+
scroller.set_min_content_width(area.width)
768+
scroller.set_max_content_width(area.width)
769+
scroller.set_min_content_height(area.height)
770+
scroller.set_max_content_height(area.height)
771+
GLib.idle_add(self._offer_fit, child, area)
772+
except Exception:
773+
pass
774+
return False
775+
776+
def _offer_fit(self, child, area):
777+
# Offer to shrink only when it does not fit and the user has not
778+
# opted out or already declined for this screen size; a smaller
779+
# screen is a new situation and is offered again.
780+
try:
781+
size = child.get_preferred_size()[0]
782+
if size.width <= area.width and size.height <= area.height:
783+
return False
784+
if self.fit_fonts == 'never':
785+
return False
786+
screen = "%dx%d" % (area.width, area.height)
787+
if self.fit_skip_size == screen:
788+
return False
789+
self._fit_child = child
790+
self._fit_area = area
791+
self._fit_screen = screen
792+
self._infobar.show()
793+
except Exception:
794+
pass
795+
return False
796+
797+
def _fit_infobar_response(self, infobar, response):
798+
infobar.hide()
799+
try:
800+
if response == 3:
801+
self.prefs.putpref('fit_skip_size', '', str)
802+
self.fit_skip_size = ''
803+
self._fit_floor = max(1, int(self.control_font.get_size() * 0.5))
804+
GLib.timeout_add(60, self._fit_pass, self._fit_child, self._fit_area)
805+
elif response == 1:
806+
# Not now: do not offer again until the screen changes.
807+
self.prefs.putpref('fit_skip_size', self._fit_screen, str)
808+
self.fit_skip_size = self._fit_screen
809+
elif response == 2:
810+
self.prefs.putpref('fit_fonts', 'never', str)
811+
self.fit_fonts = 'never'
812+
except Exception:
813+
pass
814+
815+
def _fit_pass(self, child, area):
816+
# One shrink step per relayout; stop on the first fit so the result
817+
# is the largest font that fits, then save the new fonts.
818+
try:
819+
size = child.get_preferred_size()[0]
820+
fits = size.width <= area.width and size.height <= area.height
821+
at_floor = self.control_font.get_size() <= self._fit_floor
822+
if not fits and not at_floor:
823+
factor = max(min(area.width / float(size.width),
824+
area.height / float(size.height)) * 0.99, 0.90)
825+
self._scale_fonts(factor)
826+
child.queue_resize()
827+
GLib.timeout_add(60, self._fit_pass, child, area)
828+
return False
829+
self._persist_fonts()
830+
except Exception:
831+
pass
832+
return False
833+
834+
def _persist_fonts(self):
835+
# Round down to whole points (keeps the fit), save as the new
836+
# preference, and show the values in the pickers so the displayed
837+
# sizes match what is drawn.
838+
for name, fd, button in (
839+
('control_font', self.control_font, 'controlfontbutton'),
840+
('dro_font', self.dro_font, 'drofontbutton'),
841+
('error_font', self.error_font, 'errorfontbutton'),
842+
('listing_font', self.listing_font, 'listingfontbutton')):
843+
points = fd.get_size() // Pango.SCALE
844+
if points > 0:
845+
fd.set_size(points * Pango.SCALE)
846+
text = fd.to_string()
847+
self.prefs.putpref(name, text, str)
848+
widget = self.wTree.get_object(button)
849+
if widget:
850+
widget.set_font(text)
851+
self.control_font_name = self.control_font.to_string()
852+
self.dro_font_name = self.dro_font.to_string()
853+
self.error_font_name = self.error_font.to_string()
854+
self.listing_font_name = self.listing_font.to_string()
855+
self.setfont()
856+
857+
def _scale_fonts(self, factor):
858+
# Scale every display font in place; the saved prefs are untouched.
859+
for fd in (self.control_font, self.dro_font,
860+
self.error_font, self.listing_font):
861+
size = fd.get_size()
862+
if size > 0:
863+
fd.set_size(max(1, int(size * factor)))
864+
self.setfont()
865+
642866
def setfont(self):
643867
# buttons
644868
for i in ["1", "2", "3", "4", "5", "6", "7",
@@ -657,7 +881,8 @@ def setfont(self):
657881
"dro_commanded", "dro_actual", "dro_inch", "dro_mm",
658882
"reload_tooltable", "opstop_on", "opstop_off",
659883
"blockdel_on", "blockdel_off", "pointer_hide", "pointer_show",
660-
"toolset_workpiece", "toolset_fixture","change_theme"]:
884+
"toolset_workpiece", "toolset_fixture","change_theme",
885+
"fitfontscheck"]:
661886
w = self.wTree.get_object(i)
662887
if w:
663888
w.override_font(self.control_font)

0 commit comments

Comments
 (0)