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