|  | 
|  | 1 | +"""The application/controller class for ABQ Data Entry""" | 
|  | 2 | + | 
|  | 3 | +import tkinter as tk | 
|  | 4 | +from tkinter import ttk | 
|  | 5 | +from tkinter import messagebox | 
|  | 6 | +from tkinter import filedialog | 
|  | 7 | +from tkinter import font | 
|  | 8 | +import platform | 
|  | 9 | + | 
|  | 10 | +from . import views as v | 
|  | 11 | +from . import models as m | 
|  | 12 | +from .mainmenu import get_main_menu_for_os | 
|  | 13 | +from . import images | 
|  | 14 | + | 
|  | 15 | +class Application(tk.Tk): | 
|  | 16 | +  """Application root window""" | 
|  | 17 | + | 
|  | 18 | + | 
|  | 19 | +  def __init__(self, *args, **kwargs): | 
|  | 20 | +    super().__init__(*args, **kwargs) | 
|  | 21 | + | 
|  | 22 | +    # move here for ch12 because we need some settings data to authenticate | 
|  | 23 | +    self.settings_model = m.SettingsModel() | 
|  | 24 | +    self._load_settings() | 
|  | 25 | + | 
|  | 26 | +    # Hide window while GUI is built | 
|  | 27 | +    self.withdraw() | 
|  | 28 | + | 
|  | 29 | +    # Authenticate | 
|  | 30 | +    if not self._show_login(): | 
|  | 31 | +      self.destroy() | 
|  | 32 | +      return | 
|  | 33 | + | 
|  | 34 | +    # show the window | 
|  | 35 | +    self.deiconify() | 
|  | 36 | + | 
|  | 37 | +   # Create model | 
|  | 38 | +   # remove for ch12 | 
|  | 39 | +   # self.model = m.CSVModel() | 
|  | 40 | + | 
|  | 41 | +    # Begin building GUI | 
|  | 42 | +    self.title("ABQ Data Entry Application") | 
|  | 43 | +    self.columnconfigure(0, weight=1) | 
|  | 44 | + | 
|  | 45 | +    # Set taskbar icon | 
|  | 46 | +    self.taskbar_icon = tk.PhotoImage(file=images.ABQ_LOGO_64) | 
|  | 47 | +    self.iconphoto(True, self.taskbar_icon) | 
|  | 48 | + | 
|  | 49 | +    # Create the menu | 
|  | 50 | +    #menu = MainMenu(self, self.settings) | 
|  | 51 | +    menu_class = get_main_menu_for_os(platform.system()) | 
|  | 52 | +    menu = menu_class(self, self.settings) | 
|  | 53 | + | 
|  | 54 | +    self.config(menu=menu) | 
|  | 55 | +    event_callbacks = { | 
|  | 56 | +# remove file capabilities for ch12 | 
|  | 57 | +#      '<<FileSelect>>': self._on_file_select, | 
|  | 58 | +      '<<FileQuit>>': lambda _: self.quit(), | 
|  | 59 | +      '<<ShowRecordlist>>': self._show_recordlist, | 
|  | 60 | +      '<<NewRecord>>': self._new_record, | 
|  | 61 | + | 
|  | 62 | +    } | 
|  | 63 | +    for sequence, callback in event_callbacks.items(): | 
|  | 64 | +      self.bind(sequence, callback) | 
|  | 65 | + | 
|  | 66 | +    # new for ch9 | 
|  | 67 | +    self.logo = tk.PhotoImage(file=images.ABQ_LOGO_32) | 
|  | 68 | +    ttk.Label( | 
|  | 69 | +      self, | 
|  | 70 | +      text="ABQ Data Entry Application", | 
|  | 71 | +      font=("TkDefaultFont", 16), | 
|  | 72 | +      image=self.logo, | 
|  | 73 | +      compound=tk.LEFT | 
|  | 74 | +    ).grid(row=0) | 
|  | 75 | + | 
|  | 76 | +    # The notebook | 
|  | 77 | +    self.notebook = ttk.Notebook(self) | 
|  | 78 | +    self.notebook.enable_traversal() | 
|  | 79 | +    self.notebook.grid(row=1, padx=10, sticky='NSEW') | 
|  | 80 | + | 
|  | 81 | +    # The data record form | 
|  | 82 | +    self.recordform_icon = tk.PhotoImage(file=images.FORM_ICON) | 
|  | 83 | +    self.recordform = v.DataRecordForm(self, self.model, self.settings) | 
|  | 84 | +    self.notebook.add( | 
|  | 85 | +        self.recordform, text='Entry Form', | 
|  | 86 | +        image=self.recordform_icon, compound=tk.LEFT | 
|  | 87 | +    ) | 
|  | 88 | +    self.recordform.bind('<<SaveRecord>>', self._on_save) | 
|  | 89 | + | 
|  | 90 | + | 
|  | 91 | +    # The data record list | 
|  | 92 | +    self.recordlist_icon = tk.PhotoImage(file=images.LIST_ICON) | 
|  | 93 | +    self.recordlist = v.RecordList(self) | 
|  | 94 | +    self.notebook.insert( | 
|  | 95 | +        0, self.recordlist, text='Records', | 
|  | 96 | +        image=self.recordlist_icon, compound=tk.LEFT | 
|  | 97 | +    ) | 
|  | 98 | +    self._populate_recordlist() | 
|  | 99 | +    self.recordlist.bind('<<OpenRecord>>', self._open_record) | 
|  | 100 | + | 
|  | 101 | + | 
|  | 102 | +    self._show_recordlist() | 
|  | 103 | + | 
|  | 104 | +    # status bar | 
|  | 105 | +    self.status = tk.StringVar() | 
|  | 106 | +    self.statusbar = ttk.Label(self, textvariable=self.status) | 
|  | 107 | +    self.statusbar.grid(sticky=(tk.W + tk.E), row=3, padx=10) | 
|  | 108 | + | 
|  | 109 | + | 
|  | 110 | +    self.records_saved = 0 | 
|  | 111 | + | 
|  | 112 | + | 
|  | 113 | +  def _on_save(self, *_): | 
|  | 114 | +    """Handles file-save requests""" | 
|  | 115 | + | 
|  | 116 | +    # Check for errors first | 
|  | 117 | + | 
|  | 118 | +    errors = self.recordform.get_errors() | 
|  | 119 | +    if errors: | 
|  | 120 | +      self.status.set( | 
|  | 121 | +        "Cannot save, error in fields: {}" | 
|  | 122 | +        .format(', '.join(errors.keys())) | 
|  | 123 | +      ) | 
|  | 124 | +      message = "Cannot save record" | 
|  | 125 | +      detail = "The following fields have errors: \n  * {}".format( | 
|  | 126 | +        '\n  * '.join(errors.keys()) | 
|  | 127 | +      ) | 
|  | 128 | +      messagebox.showerror( | 
|  | 129 | +        title='Error', | 
|  | 130 | +        message=message, | 
|  | 131 | +        detail=detail | 
|  | 132 | +      ) | 
|  | 133 | +      return False | 
|  | 134 | + | 
|  | 135 | +    data = self.recordform.get() | 
|  | 136 | +    rowkey = self.recordform.current_record | 
|  | 137 | +    self.model.save_record(data, rowkey) | 
|  | 138 | +    if rowkey is not None: | 
|  | 139 | +      self.recordlist.add_updated_row(rowkey) | 
|  | 140 | +    else: | 
|  | 141 | +      rowkey = (data['Date'], data['Time'], data['Lab'], data['Plot']) | 
|  | 142 | +      self.recordlist.add_inserted_row(rowkey) | 
|  | 143 | +    self.records_saved += 1 | 
|  | 144 | +    self.status.set( | 
|  | 145 | +      "{} records saved this session".format(self.records_saved) | 
|  | 146 | +    ) | 
|  | 147 | +    self.recordform.reset() | 
|  | 148 | +    self._populate_recordlist() | 
|  | 149 | + | 
|  | 150 | +# Remove for ch12 | 
|  | 151 | +#  def _on_file_select(self, *_): | 
|  | 152 | +#    """Handle the file->select action""" | 
|  | 153 | +# | 
|  | 154 | +#    filename = filedialog.asksaveasfilename( | 
|  | 155 | +#      title='Select the target file for saving records', | 
|  | 156 | +#      defaultextension='.csv', | 
|  | 157 | +#      filetypes=[('CSV', '*.csv *.CSV')] | 
|  | 158 | +#    ) | 
|  | 159 | +#    if filename: | 
|  | 160 | +#      self.model = m.CSVModel(filename=filename) | 
|  | 161 | +#      self.inserted_rows.clear() | 
|  | 162 | +#      self.updated_rows.clear() | 
|  | 163 | +#      self._populate_recordlist() | 
|  | 164 | + | 
|  | 165 | +  @staticmethod | 
|  | 166 | +  def _simple_login(username, password): | 
|  | 167 | +    """A basic authentication backend with a hardcoded user and password""" | 
|  | 168 | +    return username == 'abq' and password == 'Flowers' | 
|  | 169 | + | 
|  | 170 | +  # new ch12 | 
|  | 171 | +  def _database_login(self, username, password): | 
|  | 172 | +    """Try to login to the database and create self.data_model""" | 
|  | 173 | +    db_host = self.settings['db_host'].get() | 
|  | 174 | +    db_name = self.settings['db_name'].get() | 
|  | 175 | +    try: | 
|  | 176 | +      self.model = m.SQLModel( | 
|  | 177 | +        db_host, db_name, username, password) | 
|  | 178 | +    except m.pg.OperationalError as e: | 
|  | 179 | +      print(e) | 
|  | 180 | +      return False | 
|  | 181 | +    return True | 
|  | 182 | + | 
|  | 183 | +  def _show_login(self): | 
|  | 184 | +    """Show login dialog and attempt to login""" | 
|  | 185 | +    error = '' | 
|  | 186 | +    title = "Login to ABQ Data Entry" | 
|  | 187 | +    while True: | 
|  | 188 | +      login = v.LoginDialog(self, title, error) | 
|  | 189 | +      if not login.result:  # User canceled | 
|  | 190 | +        return False | 
|  | 191 | +      username, password = login.result | 
|  | 192 | +      if self._database_login(username, password): | 
|  | 193 | +        return True | 
|  | 194 | +      error = 'Login Failed' # loop and redisplay | 
|  | 195 | + | 
|  | 196 | +  def _load_settings(self): | 
|  | 197 | +    """Load settings into our self.settings dict.""" | 
|  | 198 | + | 
|  | 199 | +    vartypes = { | 
|  | 200 | +      'bool': tk.BooleanVar, | 
|  | 201 | +      'str': tk.StringVar, | 
|  | 202 | +      'int': tk.IntVar, | 
|  | 203 | +      'float': tk.DoubleVar | 
|  | 204 | +    } | 
|  | 205 | + | 
|  | 206 | +    # create our dict of settings variables from the model's settings. | 
|  | 207 | +    self.settings = dict() | 
|  | 208 | +    for key, data in self.settings_model.fields.items(): | 
|  | 209 | +      vartype = vartypes.get(data['type'], tk.StringVar) | 
|  | 210 | +      self.settings[key] = vartype(value=data['value']) | 
|  | 211 | + | 
|  | 212 | +    # put a trace on the variables so they get stored when changed. | 
|  | 213 | +    for var in self.settings.values(): | 
|  | 214 | +      var.trace_add('write', self._save_settings) | 
|  | 215 | + | 
|  | 216 | +    # update font settings after loading them | 
|  | 217 | +    self._set_font() | 
|  | 218 | +    self.settings['font size'].trace_add('write', self._set_font) | 
|  | 219 | +    self.settings['font family'].trace_add('write', self._set_font) | 
|  | 220 | + | 
|  | 221 | +    # process theme | 
|  | 222 | +    style = ttk.Style() | 
|  | 223 | +    theme = self.settings.get('theme').get() | 
|  | 224 | +    if theme in style.theme_names(): | 
|  | 225 | +      style.theme_use(theme) | 
|  | 226 | + | 
|  | 227 | +  def _save_settings(self, *_): | 
|  | 228 | +    """Save the current settings to a preferences file""" | 
|  | 229 | + | 
|  | 230 | +    for key, variable in self.settings.items(): | 
|  | 231 | +      self.settings_model.set(key, variable.get()) | 
|  | 232 | +    self.settings_model.save() | 
|  | 233 | + | 
|  | 234 | +  def _show_recordlist(self, *_): | 
|  | 235 | +    """Show the recordform""" | 
|  | 236 | +    self.notebook.select(self.recordlist) | 
|  | 237 | + | 
|  | 238 | +  def _populate_recordlist(self): | 
|  | 239 | +    try: | 
|  | 240 | +      rows = self.model.get_all_records() | 
|  | 241 | +    except Exception as e: | 
|  | 242 | +      messagebox.showerror( | 
|  | 243 | +        title='Error', | 
|  | 244 | +        message='Problem reading file', | 
|  | 245 | +        detail=str(e) | 
|  | 246 | +      ) | 
|  | 247 | +    else: | 
|  | 248 | +      self.recordlist.populate(rows) | 
|  | 249 | + | 
|  | 250 | +  def _new_record(self, *_): | 
|  | 251 | +    """Open the record form with a blank record""" | 
|  | 252 | +    self.recordform.load_record(None, None) | 
|  | 253 | +    self.notebook.select(self.recordform) | 
|  | 254 | + | 
|  | 255 | + | 
|  | 256 | +  def _open_record(self, *_): | 
|  | 257 | +    """Open the Record selected recordlist id in the recordform""" | 
|  | 258 | +    rowkey = self.recordlist.selected_id | 
|  | 259 | +    try: | 
|  | 260 | +      record = self.model.get_record(rowkey) | 
|  | 261 | +    except Exception as e: | 
|  | 262 | +      messagebox.showerror( | 
|  | 263 | +        title='Error', message='Problem reading file', detail=str(e) | 
|  | 264 | +      ) | 
|  | 265 | +      return | 
|  | 266 | +    self.recordform.load_record(rowkey, record) | 
|  | 267 | +    self.notebook.select(self.recordform) | 
|  | 268 | + | 
|  | 269 | +  # new chapter 9 | 
|  | 270 | +  def _set_font(self, *_): | 
|  | 271 | +    """Set the application's font""" | 
|  | 272 | +    font_size = self.settings['font size'].get() | 
|  | 273 | +    font_family = self.settings['font family'].get() | 
|  | 274 | +    font_names = ('TkDefaultFont', 'TkMenuFont', 'TkTextFont') | 
|  | 275 | +    for font_name in font_names: | 
|  | 276 | +      tk_font = font.nametofont(font_name) | 
|  | 277 | +      tk_font.config(size=font_size, family=font_family) | 
0 commit comments