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