Skip to content

Commit 3a31a9b

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, offer to shrink the fonts rather than doing it silently: a dialog with 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. The dialog is only shown when it does not fit and is never repeated once it fits; Not now is remembered per screen size (fit_skip_size) so it is not asked 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 3a31a9b

3 files changed

Lines changed: 262 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: 221 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,191 @@ 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+
win.add(scroller)
709+
scroller.show_all()
710+
# Fit on map, once the monitor is known.
711+
self._fitted = False
712+
win.connect("map-event", self._fit_to_monitor, scroller, child)
713+
except Exception:
714+
pass
715+
716+
def _fit_to_monitor(self, win, event, scroller, child):
717+
if self._fitted:
718+
return False
719+
self._fitted = True
720+
# Bound the window to the work area and let the scroller fill it.
721+
# Page wrapping already keeps the height in check; only the position
722+
# readout plus handwheel can exceed a narrow screen's width, and the
723+
# scroller handles that until (and unless) the user opts to shrink.
724+
try:
725+
display = win.get_display()
726+
gdkwin = win.get_window()
727+
if gdkwin is not None:
728+
monitor = display.get_monitor_at_window(gdkwin)
729+
else:
730+
monitor = display.get_primary_monitor()
731+
if monitor is None:
732+
monitor = display.get_monitor(0)
733+
area = monitor.get_workarea()
734+
hints = Gdk.Geometry()
735+
hints.max_width = area.width
736+
hints.max_height = area.height
737+
win.set_geometry_hints(None, hints, Gdk.WindowHints.MAX_SIZE)
738+
scroller.set_min_content_width(area.width)
739+
scroller.set_max_content_width(area.width)
740+
scroller.set_min_content_height(area.height)
741+
scroller.set_max_content_height(area.height)
742+
self._fit_win = win
743+
GLib.idle_add(self._offer_fit, child, area)
744+
except Exception:
745+
pass
746+
return False
747+
748+
def _offer_fit(self, child, area):
749+
# Only ask when the interface does not fit, and only change anything
750+
# with the user's consent (shrinking the fonts edits their saved
751+
# preference, and they may just be on the wrong monitor for a
752+
# moment). To avoid nagging, never ask when it already fits, when
753+
# the user opted out, or when they already declined for this same
754+
# screen size; a different (smaller) screen is a new situation and
755+
# is offered again.
756+
try:
757+
size = child.get_preferred_size()[0]
758+
if size.width <= area.width and size.height <= area.height:
759+
return False
760+
if self.fit_fonts == 'never':
761+
return False
762+
screen = "%dx%d" % (area.width, area.height)
763+
if self.fit_skip_size == screen:
764+
return False
765+
dialog = Gtk.MessageDialog(
766+
transient_for=self._fit_win, modal=True,
767+
message_type=Gtk.MessageType.QUESTION,
768+
buttons=Gtk.ButtonsType.NONE,
769+
text=_("The screen is too small for the current fonts."))
770+
dialog.format_secondary_text(
771+
_("Shrink the fonts so the whole interface fits this screen "
772+
"and save that as your preference?"))
773+
dialog.add_button(_("Not now"), 1)
774+
dialog.add_button(_("Never ask again"), 2)
775+
dialog.add_button(_("Shrink to fit and save"), 3)
776+
dialog.set_default_response(3)
777+
response = dialog.run()
778+
dialog.destroy()
779+
if response == 3:
780+
self.prefs.putpref('fit_skip_size', '', str)
781+
self.fit_skip_size = ''
782+
self._fit_floor = max(1, int(self.control_font.get_size() * 0.5))
783+
GLib.timeout_add(60, self._fit_pass, child, area)
784+
elif response == 1:
785+
# Not now: leave the fonts alone and do not ask again for
786+
# this screen size (offer again only if the screen changes).
787+
self.prefs.putpref('fit_skip_size', screen, str)
788+
self.fit_skip_size = screen
789+
elif response == 2:
790+
self.prefs.putpref('fit_fonts', 'never', str)
791+
self.fit_fonts = 'never'
792+
except Exception:
793+
pass
794+
return False
795+
796+
def _fit_pass(self, child, area):
797+
# One shrink step per relayout; stop on the first fit so the result
798+
# is the largest font that fits, then save the new fonts.
799+
try:
800+
size = child.get_preferred_size()[0]
801+
fits = size.width <= area.width and size.height <= area.height
802+
at_floor = self.control_font.get_size() <= self._fit_floor
803+
if not fits and not at_floor:
804+
factor = max(min(area.width / float(size.width),
805+
area.height / float(size.height)) * 0.99, 0.90)
806+
self._scale_fonts(factor)
807+
child.queue_resize()
808+
GLib.timeout_add(60, self._fit_pass, child, area)
809+
return False
810+
self._persist_fonts()
811+
except Exception:
812+
pass
813+
return False
814+
815+
def _persist_fonts(self):
816+
# Round down to whole points (keeps the fit), save as the new
817+
# preference, and show the values in the pickers so the displayed
818+
# sizes match what is drawn.
819+
for name, fd, button in (
820+
('control_font', self.control_font, 'controlfontbutton'),
821+
('dro_font', self.dro_font, 'drofontbutton'),
822+
('error_font', self.error_font, 'errorfontbutton'),
823+
('listing_font', self.listing_font, 'listingfontbutton')):
824+
points = fd.get_size() // Pango.SCALE
825+
if points > 0:
826+
fd.set_size(points * Pango.SCALE)
827+
text = fd.to_string()
828+
self.prefs.putpref(name, text, str)
829+
widget = self.wTree.get_object(button)
830+
if widget:
831+
widget.set_font(text)
832+
self.control_font_name = self.control_font.to_string()
833+
self.dro_font_name = self.dro_font.to_string()
834+
self.error_font_name = self.error_font.to_string()
835+
self.listing_font_name = self.listing_font.to_string()
836+
self.setfont()
837+
838+
def _scale_fonts(self, factor):
839+
# Scale every display font in place; the saved prefs are untouched.
840+
for fd in (self.control_font, self.dro_font,
841+
self.error_font, self.listing_font):
842+
size = fd.get_size()
843+
if size > 0:
844+
fd.set_size(max(1, int(size * factor)))
845+
self.setfont()
846+
630847
def setfont(self):
631848
# buttons
632849
for i in ["1", "2", "3", "4", "5", "6", "7",
@@ -645,7 +862,8 @@ def setfont(self):
645862
"dro_commanded", "dro_actual", "dro_inch", "dro_mm",
646863
"reload_tooltable", "opstop_on", "opstop_off",
647864
"blockdel_on", "blockdel_off", "pointer_hide", "pointer_show",
648-
"toolset_workpiece", "toolset_fixture","change_theme"]:
865+
"toolset_workpiece", "toolset_fixture","change_theme",
866+
"fitfontscheck"]:
649867
w = self.wTree.get_object(i)
650868
if w:
651869
w.override_font(self.control_font)

0 commit comments

Comments
 (0)