Skip to content

Commit 59e3f19

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 888cb94 commit 59e3f19

3 files changed

Lines changed: 272 additions & 5 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: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3002,7 +3002,7 @@ F1 S1</property>
30023002
<child>
30033003
<object class="GtkTable" id="table3">
30043004
<property name="visible">True</property>
3005-
<property name="n_rows">5</property>
3005+
<property name="n_rows">6</property>
30063006
<property name="n_columns">3</property>
30073007
<child>
30083008
<object class="GtkLabel" id="controlfont">
@@ -3218,7 +3218,25 @@ F1 S1</property>
32183218
</packing>
32193219
</child>
32203220
<child>
3221-
<placeholder/>
3221+
<object class="GtkCheckButton" id="fitfontscheck">
3222+
<property name="label" translatable="yes">Offer to shrink fonts to fit the screen</property>
3223+
<property name="visible">True</property>
3224+
<property name="can_focus">False</property>
3225+
<property name="receives_default">False</property>
3226+
<property name="focus_on_click">False</property>
3227+
<property name="draw_indicator">True</property>
3228+
<signal name="toggled" handler="on_fitfontscheck_toggled"/>
3229+
</object>
3230+
<packing>
3231+
<property name="left_attach">0</property>
3232+
<property name="right_attach">3</property>
3233+
<property name="top_attach">5</property>
3234+
<property name="bottom_attach">6</property>
3235+
<property name="x_options">GTK_FILL</property>
3236+
<property name="y_options"/>
3237+
<property name="x_padding">10</property>
3238+
<property name="y_padding">6</property>
3239+
</packing>
32223240
</child>
32233241
</object>
32243242
</child>

src/emc/usr_intf/touchy/touchy.py

Lines changed: 231 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ def __init__(self, inifile):
107107
self.wTree.get_object('MainWindow').set_can_focus(True)
108108
self.wTree.get_object('MainWindow').grab_focus()
109109

110+
# Stop the window growing past the screen (see method).
111+
self._constrain_to_monitor()
112+
110113
self.num_mdi_labels = 11
111114
self.num_filechooser_labels = 11
112115
self.num_listing_labels = 20
@@ -136,6 +139,13 @@ def __init__(self, inifile):
136139
self.err_textcolor = self.prefs.getpref('err_textcolor', 'default', str)
137140
self.window_geometry = self.prefs.getpref('window_geometry', 'default', str)
138141
self.window_max = self.prefs.getpref('window_force_max', 'false', bool)
142+
self.fit_fonts = self.prefs.getpref('fit_fonts', 'ask', str)
143+
self.fit_skip_size = self.prefs.getpref('fit_skip_size', '', str)
144+
check = self.wTree.get_object("fitfontscheck")
145+
if check:
146+
self._setting_fit_check = True
147+
check.set_active(self.fit_fonts != 'never')
148+
self._setting_fit_check = False
139149

140150
# initial screen setup
141151
if os.path.exists(themedir):
@@ -335,6 +345,7 @@ def __init__(self, inifile):
335345
"on_dro_mm_clicked" : self.dro_mm,
336346
"on_errorfontbutton_font_set" : self.change_error_font,
337347
"on_listingfontbutton_font_set" : self.change_listing_font,
348+
"on_fitfontscheck_toggled" : self.fit_fonts_toggled,
338349
"on_estop_clicked" : self.linuxcnc.estop,
339350
"on_estop_reset_clicked" : self.linuxcnc.estop_reset,
340351
"on_machine_off_clicked" : self.linuxcnc.machine_off,
@@ -427,9 +438,15 @@ def quit(self, unused):
427438

428439

429440
def tabselect(self, notebook, b, tab):
430-
# new_tab=notebook.get_nth_page(tab)
431-
# old_tab=notebook.get_nth_page(self.tab)
432441
self.tab = tab
442+
# The handwheel is for jogging, not setup: hide it on the
443+
# Preferences tab so the wide settings page gets the full width.
444+
wheel = self.wTree.get_object("wheel")
445+
if wheel is not None and getattr(self, "_prefs_index", -1) >= 0:
446+
if tab == self._prefs_index:
447+
wheel.hide()
448+
else:
449+
wheel.show()
433450
# for c in self._dynamic_childs:
434451
# if new_tab.__gtype__.name =='GtkSocket':
435452
# w= new_tab.get_plug_window()
@@ -616,6 +633,21 @@ def change_listing_font(self, fontbutton):
616633
self.listing_font = Pango.FontDescription(self.listing_font_name)
617634
self.setfont()
618635

636+
def fit_fonts_toggled(self, button):
637+
# Re-enable or disable the offer to shrink the fonts to fit a
638+
# small screen. Checking it also forgets any per-screen decline
639+
# so the offer can appear again.
640+
if getattr(self, "_setting_fit_check", False):
641+
return
642+
if button.get_active():
643+
self.fit_fonts = 'ask'
644+
self.prefs.putpref('fit_fonts', 'ask', str)
645+
self.fit_skip_size = ''
646+
self.prefs.putpref('fit_skip_size', '', str)
647+
else:
648+
self.fit_fonts = 'never'
649+
self.prefs.putpref('fit_fonts', 'never', str)
650+
619651
def change_theme(self, b):
620652
tree_iter = b.get_active_iter()
621653
if tree_iter is not None:
@@ -627,6 +659,201 @@ def change_theme(self, b):
627659
settings = Gtk.Settings.get_default()
628660
settings.set_string_property("gtk-theme-name", theme, "")
629661

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

0 commit comments

Comments
 (0)