@@ -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