import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
import os
from install_station.system_calls import (
keyboard_dictionary,
keyboard_models,
change_keyboard,
set_keyboard
)
from install_station.data import InstallationData, tmp, get_text
# Ensure temp directory exists
if not os.path.exists(tmp):
os.makedirs(tmp)
layout = f'{tmp}layout'
variant = f'{tmp}variant'
KBFile = f'{tmp}keyboard'
kb_dictionary = keyboard_dictionary()
kbm_dictionary = keyboard_models()
cssProvider = Gtk.CssProvider()
cssProvider.load_from_path('/usr/local/lib/install-station/ghostbsd-style.css')
screen = Gdk.Screen.get_default()
styleContext = Gtk.StyleContext()
styleContext.add_provider_for_screen(
screen,
cssProvider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
class PlaceHolderEntry(Gtk.Entry):
"""
GTK Entry widget with placeholder text functionality.
This class extends Gtk.Entry to provide placeholder text that disappears
when the widget gains focus and returns when focus is lost if empty.
"""
def __init__(self, *args, **kwds) -> None:
Gtk.Entry.__init__(self, *args, **kwds)
self.placeholder = get_text('Type here to test your keyboard')
self.set_text(self.placeholder)
self._default = True
self.connect('focus-in-event', self._focus_in_event)
self.connect('focus-out-event', self._focus_out_event)
def _focus_in_event(self, _widget: Gtk.Widget, _event) -> None:
if self._default:
self.set_text('')
def _focus_out_event(self, _widget: Gtk.Widget, _event) -> None:
if Gtk.Entry.get_text(self) == '':
self.set_text(self.placeholder)
self._default = True
else:
self._default = False
def get_text(self) -> str:
if self._default:
return ''
return Gtk.Entry.get_text(self)
class Keyboard:
"""
Utility class for the keyboard configuration screen following the utility class pattern.
This class provides a GTK+ interface for keyboard layout and model selection including:
- Keyboard layout selection from available system layouts
- Keyboard model selection from available models
- Real-time keyboard testing with preview text entry
- Integration with InstallationData for persistent configuration
The class follows a utility pattern with class methods and variables for state management,
designed to integrate with the Interface controller for navigation flow.
"""
# Class variables instead of instance variables
kb_layout: str | None = None
kb_variant: str | None = None
kb_model: str | None = None
vbox1: Gtk.Box | None = None
treeView: Gtk.TreeView | None = None
test_entry: PlaceHolderEntry | None = None
@classmethod
def layout_columns(cls, treeview: Gtk.TreeView) -> None:
"""
Configure the keyboard layout treeview with appropriate columns.
Creates a single column with a "Keyboard Layout" header for displaying
available keyboard layouts in the tree view.
Args:
treeview: TreeView widget to configure with layout column
"""
cell = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(None, cell, text=0)
column_header = Gtk.Label(label=f'{get_text("Keyboard Layout")}')
column_header.set_use_markup(True)
column_header.show()
column.set_widget(column_header)
column.set_sort_column_id(0)
treeview.append_column(column)
@classmethod
def variant_columns(cls, treeview: Gtk.TreeView) -> None:
"""
Configure the keyboard model treeview with appropriate columns.
Creates a single column with a "Keyboard Models" header for displaying
available keyboard models in the tree view.
Args:
treeview: TreeView widget to configure with model column
"""
cell = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(None, cell, text=0)
column_header = Gtk.Label(label=f'{get_text("Keyboard Models")}')
column_header.set_use_markup(True)
column_header.show()
column.set_widget(column_header)
column.set_sort_column_id(0)
treeview.append_column(column)
@classmethod
def layout_selection(cls, tree_selection: Gtk.TreeSelection) -> None:
"""
Handle keyboard layout selection from the treeview.
Extracts the selected layout from the tree view and updates both
class variables and InstallationData with the layout information.
Also applies the keyboard layout change immediately for testing.
Args:
tree_selection: TreeSelection widget containing the user's layout choice
"""
model, treeiter = tree_selection.get_selected()
if treeiter is not None:
value = model[treeiter][0]
kb_lv = kb_dictionary[value]
cls.kb_layout = kb_lv['layout']
cls.kb_variant = kb_lv['variant']
# Save to InstallationData
InstallationData.keyboard_layout = value
InstallationData.keyboard_layout_code = cls.kb_layout
InstallationData.keyboard_variant = cls.kb_variant
change_keyboard(cls.kb_layout, cls.kb_variant)
print(f"Keyboard layout selected: {value} ({cls.kb_layout}/{cls.kb_variant})")
@classmethod
def model_selection(cls, tree_selection: Gtk.TreeSelection) -> None:
"""
Handle keyboard model selection from the treeview.
Extracts the selected model from the tree view and updates both
class variables and InstallationData with the model information.
Also applies the keyboard model change immediately for testing.
Args:
tree_selection: TreeSelection widget containing the user's model choice
"""
model, treeiter = tree_selection.get_selected()
if treeiter is not None:
value = model[treeiter][0]
cls.kb_model = kbm_dictionary[value]
# Save to InstallationData
InstallationData.keyboard_model = value
InstallationData.keyboard_model_code = cls.kb_model
if cls.kb_layout and cls.kb_variant:
change_keyboard(cls.kb_layout, cls.kb_variant, cls.kb_model)
print(f"Keyboard model selected: {value} ({cls.kb_model})")
@classmethod
def save_selection(cls) -> None:
"""
Save the current keyboard selection.
This method saves keyboard configuration to both InstallationData
(for the installer) and temporary files (for compatibility).
"""
# Data is now saved in InstallationData automatically
# Keep file writing for compatibility
if cls.kb_layout and cls.kb_variant and cls.kb_model:
with open(KBFile, 'w') as file:
file.write(f"{cls.kb_layout}\\n")
file.write(f"{cls.kb_variant}\\n")
file.write(f"{cls.kb_model}\\n")
@classmethod
def save_keyboard(cls) -> None:
"""
Apply the keyboard configuration to the system.
This method applies the selected keyboard layout, variant, and model
to the current system for immediate use.
"""
if cls.kb_layout and cls.kb_variant and cls.kb_model:
set_keyboard(cls.kb_layout, cls.kb_variant, cls.kb_model)
@classmethod
def initialize(cls) -> None:
"""
Initialize the keyboard configuration UI following the utility class pattern.
Creates the main interface including:
- Keyboard layout selection tree view on the left side
- Keyboard model selection tree view on the right side
- Test entry field at the bottom for keyboard testing
- Grid-based layout with proper spacing and margins
This method is called automatically by get_model() when the interface is first accessed.
"""
cls.vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
cls.vbox1.show()
main_grid = Gtk.Grid()
cls.vbox1.pack_start(main_grid, True, True, 0)
# Create two scrolled windows side by side for layout and model selection
# Left side - Keyboard layouts
sw_layouts = Gtk.ScrolledWindow()
sw_layouts.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
sw_layouts.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
layout_store = Gtk.TreeStore(str)
layout_store.append(None, [get_text('English (US)')])
layout_store.append(None, [get_text('English (Canada)')])
layout_store.append(None, [get_text('French (Canada)')])
for line in sorted(kb_dictionary):
layout_store.append(None, [line.rstrip()])
cls.treeView = Gtk.TreeView()
cls.treeView.set_model(layout_store)
cls.treeView.set_rules_hint(True)
cls.layout_columns(cls.treeView)
layout_selection = cls.treeView.get_selection()
layout_selection.set_mode(Gtk.SelectionMode.SINGLE)
layout_selection.connect("changed", cls.layout_selection)
sw_layouts.add(cls.treeView)
sw_layouts.show()
# Right side - Keyboard models
sw_models = Gtk.ScrolledWindow()
sw_models.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
sw_models.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
model_store = Gtk.TreeStore(str)
for line in sorted(kbm_dictionary):
model_store.append(None, [line.rstrip()])
model_treeview = Gtk.TreeView()
model_treeview.set_model(model_store)
model_treeview.set_rules_hint(True)
cls.variant_columns(model_treeview)
model_selection = model_treeview.get_selection()
model_selection.set_mode(Gtk.SelectionMode.SINGLE)
model_selection.connect("changed", cls.model_selection)
sw_models.add(model_treeview)
sw_models.show()
# Bottom - Test entry
cls.test_entry = PlaceHolderEntry()
# Layout everything in grid
main_grid.set_row_spacing(5)
main_grid.set_column_spacing(10)
main_grid.set_column_homogeneous(True)
main_grid.set_row_homogeneous(True)
main_grid.set_margin_left(10)
main_grid.set_margin_right(10)
main_grid.set_margin_top(10)
main_grid.set_margin_bottom(10)
main_grid.attach(sw_layouts, 0, 0, 1, 8)
main_grid.attach(sw_models, 1, 0, 1, 8)
main_grid.attach(cls.test_entry, 0, 9, 2, 1)
main_grid.show()
# Set default selection
cls.treeView.set_cursor(0)
@classmethod
def get_model(cls) -> Gtk.Box:
"""
Return the GTK widget model for the keyboard configuration interface.
Returns the main container widget that was created during initialization.
Returns:
Gtk.Box: The main container widget for the keyboard configuration interface
"""
if cls.vbox1 is None:
cls.initialize()
return cls.vbox1
@classmethod
def get_keyboard_info(cls) -> dict[str, str | None]:
"""
Get the current keyboard configuration information.
Returns:
dict: Dictionary containing keyboard layout, variant, and model information
"""
return {
'layout': InstallationData.keyboard_layout or cls.kb_layout,
'layout_code': InstallationData.keyboard_layout_code or cls.kb_layout,
'variant': InstallationData.keyboard_variant or cls.kb_variant,
'model': InstallationData.keyboard_model or cls.kb_model,
'model_code': InstallationData.keyboard_model_code or cls.kb_model
}