From 7f00045b2910a1c789088802596c8f133e41ecb9 Mon Sep 17 00:00:00 2001 From: Subin Siby Date: Sun, 28 Nov 2021 01:39:43 +0530 Subject: [PATCH] Learn words (#7) * Can select candidates by number press, word break commit support * Implement word learning * Use CTRL or CMD +DEL to unlearn word * Added recently learned words in settings * Improved vstDir setting, RLW page has languages toggler --- Application/AppDelegate.swift | 4 +- Application/MainView.swift | 16 ++- Application/RLWTable.swift | 127 +++++++++++++++++++++ Application/RecentlyLearnedWordsView.swift | 111 ++++++++++++++++++ Application/VarnamApp.entitlements | 2 +- Common/VarnamConfig.swift | 26 ++++- GoVarnam/Varnam.swift | 79 +++++++++---- Input Source/AppDelegate.swift | 11 ++ Input Source/ClientManager.swift | 11 +- Input Source/VarnamController.swift | 124 +++++++++++++++----- VarnamIME.xcodeproj/project.pbxproj | 8 ++ 11 files changed, 455 insertions(+), 64 deletions(-) create mode 100644 Application/RLWTable.swift create mode 100644 Application/RecentlyLearnedWordsView.swift diff --git a/Application/AppDelegate.swift b/Application/AppDelegate.swift index 45476cc..9026a99 100644 --- a/Application/AppDelegate.swift +++ b/Application/AppDelegate.swift @@ -39,10 +39,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { } @IBAction func showReleaseNotes(_ sender: NSMenuItem) { - NSWorkspace.shared.open(URL(string: "https://github.com/ratreya/varnam-ime/releases")!) + NSWorkspace.shared.open(URL(string: "https://github.com/varnamproject/varnam-macOS/releases")!) } @IBAction func reportIssue(_ sender: NSMenuItem) { - NSWorkspace.shared.open(URL(string: "https://github.com/ratreya/varnam-ime/issues/new")!) + NSWorkspace.shared.open(URL(string: "https://github.com/varnamproject/varnam-macOS/issues/new")!) } } diff --git a/Application/MainView.swift b/Application/MainView.swift index 9b75a67..657c4c7 100644 --- a/Application/MainView.swift +++ b/Application/MainView.swift @@ -16,17 +16,23 @@ struct MainView: View { TabView(selection: $currentTab) { SettingsView().tabItem { Text("Settings") - }.tag(2) + }.tag(0) .onAppear() { - self.currentTab = 2 + self.currentTab = 0 } LanguageView().tabItem { Text("Languages") - }.tag(3) + }.tag(1) + .onAppear() { + self.currentTab = 1 + } + RecentlyLearnedWordsView().tabItem { + Text("Recently Learned Words") + }.tag(2) .onAppear() { - self.currentTab = 3 + self.currentTab = 2 } - }.padding(20) + }.padding(20) } } diff --git a/Application/RLWTable.swift b/Application/RLWTable.swift new file mode 100644 index 0000000..42cc201 --- /dev/null +++ b/Application/RLWTable.swift @@ -0,0 +1,127 @@ +// +// RLWTable.swift +// VarnamApp +// +// Copyright © 2021 Subin Siby +// + +import Foundation + +import SwiftUI + +extension Double { + func getDateTimeStringFromUTC() -> String { + let date = Date(timeIntervalSince1970: self) + let dateFormatter = DateFormatter() + dateFormatter.timeStyle = DateFormatter.Style.medium //Set time style + dateFormatter.dateStyle = DateFormatter.Style.medium //Set date style + dateFormatter.timeZone = .current + return dateFormatter.string(from: date) + } +} + +struct RLWTable: NSViewControllerRepresentable { + var words: [Suggestion] + var unlearn: (String)->() + typealias NSViewControllerType = RLWTableController + + func makeNSViewController(context: Context) -> RLWTableController { + return RLWTableController(self) + } + + func updateNSViewController(_ nsViewController: RLWTableController, context: Context) { + nsViewController.words = words + nsViewController.table.reloadData() + } +} + +class RLWTableController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate { + private var wrapper: RLWTable + + var table = NSTableView() + var words: [Suggestion]; + + init(_ wrapper: RLWTable) { + self.wrapper = wrapper + self.words = wrapper.words + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + self.view = NSView() + view.autoresizesSubviews = true + + let columns = [ + (title: "Learned On", width: 200.0, tooltip: "Factory name for language"), + (title: "Word", width: 250.0, tooltip: "Custom name for language"), + (title: "", width: 10.0, tooltip: "Shortcut to select language"), + ] + for column in columns { + let tableColumn = NSTableColumn() + tableColumn.headerCell.title = column.title + tableColumn.headerCell.alignment = .center + tableColumn.identifier = NSUserInterfaceItemIdentifier(rawValue: column.title) + tableColumn.width = CGFloat(column.width) + tableColumn.headerToolTip = column.tooltip + table.addTableColumn(tableColumn) + } + table.allowsColumnResizing = true + table.allowsColumnSelection = false + table.allowsMultipleSelection = false + table.allowsColumnReordering = false + table.allowsEmptySelection = true + table.allowsTypeSelect = false + table.usesAlternatingRowBackgroundColors = true + table.intercellSpacing = NSSize(width: 15, height: 7) + + let scroll = NSScrollView() + scroll.documentView = table + scroll.hasVerticalScroller = true + scroll.autoresizingMask = [.height, .width] + scroll.borderType = .bezelBorder + view.addSubview(scroll) + } + + override func viewDidLoad() { + super.viewDidLoad() + + table.delegate = self + table.dataSource = self + } + + // NSTableViewDataSource + func numberOfRows(in table: NSTableView) -> Int { + return words.count + } + + // NSTableViewDelegate + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + switch tableColumn!.title { + case "Learned On": + return NSTextField(string: Double(words[row].LearnedOn).getDateTimeStringFromUTC()) + case "Word": + return NSTextField(string: words[row].Word) + case "": + let btn = NSButton(title: "Unlearn", target: self, action: #selector(self.onChange(receiver:))) + btn.identifier = tableColumn!.identifier + return btn + default: + Logger.log.fatal("Unknown column title \(tableColumn!.title)") + fatalError() + } + } + + @objc func onChange(receiver: Any) { + let row = table.row(for: receiver as! NSView) + if row == -1 { + // The view has changed under us + return + } + wrapper.unlearn(words[row].Word) + } +} diff --git a/Application/RecentlyLearnedWordsView.swift b/Application/RecentlyLearnedWordsView.swift new file mode 100644 index 0000000..4f55598 --- /dev/null +++ b/Application/RecentlyLearnedWordsView.swift @@ -0,0 +1,111 @@ +/* +* VarnamApp is companion application for VarnamIME. +* Copyright (C) 2021 Subin Siby - VarnamIME +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +import Foundation + +import SwiftUI + +class RLWModel: ObservableObject { + @Published public var words: [Suggestion] = [Suggestion](); + + @Published var languages: [LanguageConfig]; + @Published var schemeID: String = "ml"; + @Published var schemeLangName: String = "Malayalam"; + + let config = VarnamConfig() + private (set) var varnam: Varnam! = nil; + + private func closeVarnam() { + varnam.close() + varnam = nil + } + + private func initVarnam() -> Bool { + if (varnam != nil) { + closeVarnam() + } + do { + varnam = try Varnam(schemeID) + } catch let error { + Logger.log.error(error.localizedDescription) + return false + } + return true + } + + init() { + Varnam.setVSTLookupDir(config.vstDir) + + // One language = One dictionary + // Same language can have multiple schemes + schemeID = config.schemeID + languages = config.languageConfig + + if initVarnam() { + refreshWords() + } + } + + func refreshWords() { + do { + words = try varnam.getRecentlyLearnedWords() + } catch let error { + Logger.log.error(error.localizedDescription) + } + } + + func unlearn(_ word: String) { + do { + try varnam.unlearn(word) + } catch let error { + Logger.log.error(error.localizedDescription) + } + refreshWords() + } + + func changeScheme(_ id: String) { + schemeID = id + schemeLangName = languages.first(where: { $0.identifier == schemeID })?.DisplayName ?? "" + initVarnam() + refreshWords() + } +} + +struct RecentlyLearnedWordsView: View { + // Changes in model will automatically reload the table view + @ObservedObject var model = RLWModel() + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text("Language: ") + MenuButton(model.schemeLangName) { + ForEach(model.languages, id: \.self) { (lang) in + Button(lang.DisplayName) { + self.model.changeScheme(lang.identifier) + } + } + } + .fixedSize() + .padding(0) + } + Spacer(minLength: 5) + RLWTable( + words: model.words, + unlearn: model.unlearn + ) + }.padding(16) + } +} + +struct RecentlyLearnedWordsView_Previews: PreviewProvider { + static var previews: some View { + RecentlyLearnedWordsView() + } +} diff --git a/Application/VarnamApp.entitlements b/Application/VarnamApp.entitlements index 307c4f4..d05569b 100644 --- a/Application/VarnamApp.entitlements +++ b/Application/VarnamApp.entitlements @@ -3,7 +3,7 @@ com.apple.security.app-sandbox - + com.apple.security.application-groups group.varnamproject.Varnam diff --git a/Common/VarnamConfig.swift b/Common/VarnamConfig.swift index 405b9c9..3fa6540 100644 --- a/Common/VarnamConfig.swift +++ b/Common/VarnamConfig.swift @@ -16,6 +16,11 @@ struct LanguageConfig: Codable, Equatable, Hashable { var isEnabled: Bool var shortcutKey: String? var shortcutModifiers: UInt? + + // TODO make this struct same as SchemeDetails + var Identifier: String + var DisplayName: String + var LangCode: String } class VarnamConfig: Config { @@ -96,16 +101,27 @@ class VarnamConfig: Config { var languageConfig: [LanguageConfig] { get { + var langConfigs = factoryLanguageConfig if let encoded = userDefaults.data(forKey: #function) { do { - return try JSONDecoder().decode(Array.self, from: encoded) + let savedLangConfigs = try JSONDecoder().decode(Array.self, from: encoded) + for slc in savedLangConfigs { + if let row = langConfigs.firstIndex(where: {$0.identifier == slc.identifier}) { + // Only changing the setting values + // Other properties such as display name are constant, + // They are obtained from VST + langConfigs[row].isEnabled = slc.isEnabled + langConfigs[row].shortcutKey = slc.shortcutKey + langConfigs[row].shortcutModifiers = slc.shortcutModifiers + } + } } catch { Logger.log.error("Exception while trying to decode languageConfig: \(error)") resetLanguageConfig() } } - return factoryLanguageConfig + return langConfigs } set(value) { let encodedData: Data = try! JSONEncoder().encode(value) @@ -121,7 +137,11 @@ class VarnamConfig: Config { configs.append(LanguageConfig( identifier: scheme.Identifier, language: scheme.DisplayName, - isEnabled: true + isEnabled: true, + + Identifier: scheme.Identifier, + DisplayName: scheme.DisplayName, + LangCode: scheme.LangCode )) } return configs diff --git a/GoVarnam/Varnam.swift b/GoVarnam/Varnam.swift index e2873d3..fe1973d 100644 --- a/GoVarnam/Varnam.swift +++ b/GoVarnam/Varnam.swift @@ -8,15 +8,18 @@ import Foundation +// Thank you Martin R +// https://stackoverflow.com/a/44548174/1372424 public struct VarnamException: Error { - let message: String - - init(_ message: String) { - self.message = message + let msg: String + init(_ msg: String) { + self.msg = msg } +} - public var localizedDescription: String { - return message +extension VarnamException: LocalizedError { + public var errorDescription: String? { + return NSLocalizedString(msg, comment: "") } } @@ -29,6 +32,12 @@ public struct SchemeDetails { var IsStable: Bool } +public struct Suggestion { + var Word: String + var Weight: Int + var LearnedOn: Int +} + extension String { func toCStr() -> UnsafeMutablePointer? { return UnsafeMutablePointer(mutating: (self as NSString).utf8String) @@ -38,7 +47,15 @@ extension String { public class Varnam { private var varnamHandle: Int32 = 0; + // VSTs are stored in VarnamIME.app's assets. + // VarnamApp.app will know this path by a config value + // set by the IME app. Kind of weird, yes. + // Setting the lookup dir to assetsFolderPath only + // works for VarnamIME.app. For VarnamApp, the VST + // lookup path should be set from the config value. + static let assetsFolderPath = Bundle.main.resourceURL!.appendingPathComponent("assets").path + static func importAllVLFInAssets() { // TODO import only necessary ones let fm = FileManager.default @@ -60,25 +77,11 @@ public class Varnam { } static func setVSTLookupDir(_ path: String) { - varnam_set_vst_lookup_dir(assetsFolderPath.toCStr()) - } - - // This will only run once - struct VarnamInit { - static let once = VarnamInit() - init() { - print(assetsFolderPath) - Varnam.setVSTLookupDir(assetsFolderPath) - } + varnam_set_vst_lookup_dir(path.toCStr()) } internal init(_ schemeID: String = "ml") throws { - _ = VarnamInit.once - - schemeID.withCString { - let rc = varnam_init_from_id(UnsafeMutablePointer(mutating: $0), &varnamHandle) - try! checkError(rc) - } + try checkError(varnam_init_from_id(schemeID.toCStr(), &varnamHandle)) } public func getLastError() -> String { @@ -106,7 +109,7 @@ public class Varnam { var results = [String]() for i in (0.. [SchemeDetails] { - _ = VarnamInit.once + public func getRecentlyLearnedWords() throws -> [Suggestion] { + var arr: UnsafeMutablePointer? = varray_init() + try checkError(varnam_get_recently_learned_words(varnamHandle, 1, 0, 30, &arr)) + var results = [Suggestion]() + for i in (0.. [SchemeDetails] { var schemes = [SchemeDetails]() let arr = varnam_get_all_scheme_details() diff --git a/Input Source/AppDelegate.swift b/Input Source/AppDelegate.swift index 8407a1d..e46c296 100644 --- a/Input Source/AppDelegate.swift +++ b/Input Source/AppDelegate.swift @@ -41,6 +41,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { }} func applicationDidFinishLaunching(_ aNotification: Notification) { + setVarnamPaths() + for arg in CommandLine.arguments { if arg == "-import" { importVLF() @@ -74,6 +76,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { server.commitComposition(self) } + func setVarnamPaths() { + // This is being set because VarnamApp doesn't know + // the location to look for VST files. + // See comments inside Varnam.swift + let config = VarnamConfig() + config.vstDir = Varnam.assetsFolderPath + Varnam.setVSTLookupDir(config.vstDir) + } + func importVLF() { Varnam.importAllVLFInAssets() } diff --git a/Input Source/ClientManager.swift b/Input Source/ClientManager.swift index 5820dee..47eff7e 100644 --- a/Input Source/ClientManager.swift +++ b/Input Source/ClientManager.swift @@ -82,6 +82,15 @@ class ClientManager: CustomStringConvertible { candidatesWindow.interpretKeyEvents([event]) } + func getCandidateAt(_ index: Int) -> String? { + if candidates.count < index { + return nil + } else { + return candidates[index] + } + } + + // Get candidate at current highlighted position func getCandidate() -> String? { if candidates.count == 0 { return nil @@ -90,7 +99,7 @@ class ClientManager: CustomStringConvertible { } } - func finalize(_ output: String) { + func commitText(_ output: String) { Logger.log.debug("Finalizing with: \(output)") client.insertText(output, replacementRange: notFoundRange) candidatesWindow.hide() diff --git a/Input Source/VarnamController.swift b/Input Source/VarnamController.swift index 546c3ee..a47c945 100644 --- a/Input Source/VarnamController.swift +++ b/Input Source/VarnamController.swift @@ -15,8 +15,6 @@ import Carbon.HIToolbox @objc(VarnamController) public class VarnamController: IMKInputController { - static let validInputs = CharacterSet.alphanumerics.union(CharacterSet.whitespaces).union(CharacterSet.punctuationCharacters).union(.symbols) - let config = VarnamConfig() let dispatch = AsyncDispatcher() private let clientManager: ClientManager @@ -28,7 +26,10 @@ public class VarnamController: IMKInputController { private var schemeID = "ml" private (set) var varnam: Varnam! = nil - private func initVarnam() { + private (set) var validInputs: CharacterSet; + private (set) var wordBreakChars: CharacterSet; + + private func initVarnam() -> Bool { if (varnam != nil) { closeVarnam() } @@ -37,13 +38,9 @@ public class VarnamController: IMKInputController { varnam = try Varnam(schemeID) } catch let error { Logger.log.error(error.localizedDescription) + return false } - - // This is being set because VarnamApp doesn't know - // the location who also access govarnam - if config.vstDir == "" { - config.vstDir = Varnam.assetsFolderPath - } + return true } private func closeVarnam() { @@ -61,9 +58,24 @@ public class VarnamController: IMKInputController { return nil } self.clientManager = clientManager + + validInputs = CharacterSet.letters + wordBreakChars = CharacterSet.punctuationCharacters + + // TODO get special characters from varnam via SearchSymbolTable + let validSpecialInputs = [ + "_", // Used for ZWJ + "~" // Used usually for virama + ] + for char in validSpecialInputs { + let charScalar = char.unicodeScalars.first! + validInputs.insert(charScalar) + wordBreakChars.remove(charScalar) + } + super.init(server: server, delegate: delegate, client: inputClient) - - initVarnam() + + _ = initVarnam() Logger.log.debug("Initialized Controller for Client: \(clientManager)") } @@ -74,15 +86,42 @@ public class VarnamController: IMKInputController { } func commitText(_ text: String) { - clientManager.finalize(text) + clientManager.commitText(text) clearState() + + if config.learnWords { + Logger.log.debug("Learning \(text)") + do { + try varnam.learn(text) + } catch let error { + Logger.log.warning(error.localizedDescription) + } + } + } + + func commitCandidateAt(_ position: Int) { + if position == 0 { + commitText(preedit) + } else if let text = clientManager.getCandidateAt(position-1) { + commitText(text) + } + } + + func commitPreedit() -> Bool { + if preedit.isEmpty { + return false + } + commitText(preedit) + return true } // Commits the first candidate if available - func commit() { + func commit() -> Bool { if let text = clientManager.getCandidate() { commitText(text) + return true } + return false } private func insertAtIndex(_ source: inout String, _ location: String.IndexDistance, _ char: String!) { @@ -106,6 +145,19 @@ public class VarnamController: IMKInputController { let keyCode = Int(event.keyCode) + if event.modifierFlags.contains(.command) || event.modifierFlags.contains(.control) { + if preedit.count == 0 { + return false + } + if keyCode == kVK_Delete || keyCode == kVK_ForwardDelete { + Logger.log.debug("CMD + DEL = Unlearn word") + if let text = clientManager.getCandidate() { + try! varnam.unlearn(text) + updateLookupTable() + } + } + } + switch keyCode { case kVK_Space: let text = clientManager.getCandidate() @@ -118,18 +170,13 @@ public class VarnamController: IMKInputController { case kVK_Return: let text = clientManager.getCandidate() if text == nil { - commitText(preedit) - return false + return commitPreedit() } else { commitText(text!) } return true case kVK_Escape: - if preedit.isEmpty { - return false - } - commitText(preedit) - return true + return commitPreedit() case kVK_LeftArrow: if preedit.isEmpty { return false @@ -184,11 +231,32 @@ public class VarnamController: IMKInputController { } return true default: - if let chars = event.characters, chars.unicodeScalars.count == 1, event.modifierFlags.isSubset(of: [.capsLock, .shift]), VarnamController.validInputs.contains(chars.unicodeScalars.first!) { - NSLog("character event: \(chars)") - return processInput(chars) + if let chars = event.characters, chars.unicodeScalars.count == 1 { + let numericKey: Int = Int(chars) ?? 10 + + if numericKey >= 0 && numericKey <= 9 { + // Numeric key press + commitCandidateAt(numericKey) + return true + } + + let charScalar = chars.unicodeScalars.first! + + if wordBreakChars.contains(charScalar) { + if let text = clientManager.getCandidate() { + commitText(text + chars) + return true + } + return false + } + + if event.modifierFlags.isSubset(of: [.capsLock, .shift]), validInputs.contains(charScalar) { + Logger.log.debug("character event: \(chars)") + return processInput(chars) + } } } + return false } @@ -231,10 +299,10 @@ public class VarnamController: IMKInputController { // (b) could have changed while we were in background - converge (a) -> (b) if global script selection is configured if schemeID != config.schemeID { Logger.log.debug("Initializing varnam: \(schemeID) to: \(config.schemeID)") - initVarnam() + _ = initVarnam() } if (varnam == nil) { - initVarnam() + _ = initVarnam() } } @@ -259,7 +327,9 @@ public class VarnamController: IMKInputController { public override func commitComposition(_ sender: Any!) { Logger.log.debug("Commit Composition called by: \((sender as? IMKTextInput)?.bundleIdentifier() ?? "unknown")") - commit() + // This is usually called when current input method is changed. + // Some apps also call to commit + _ = commitPreedit() } @objc public func menuItemSelected(sender: NSDictionary) { @@ -268,6 +338,6 @@ public class VarnamController: IMKInputController { // Converge (b) -> (c) config.schemeID = item.representedObject as! String // Converge (a) -> (b) - initVarnam() + _ = initVarnam() } } diff --git a/VarnamIME.xcodeproj/project.pbxproj b/VarnamIME.xcodeproj/project.pbxproj index 6032647..c994ad0 100644 --- a/VarnamIME.xcodeproj/project.pbxproj +++ b/VarnamIME.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 040A907E27523D2F008E365B /* RLWTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040A907D27523D2F008E365B /* RLWTable.swift */; }; 043C818D274139D900E6832E /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D0A69827411F47006C3B54 /* Config.swift */; }; 043C818E274139DC00E6832E /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D0A69A27411F6D006C3B54 /* Logger.swift */; }; 043C818F274139DE00E6832E /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D0A69C27411FB1006C3B54 /* Common.swift */; }; @@ -14,6 +15,7 @@ 043C819B27413CB500E6832E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043C819A27413CB500E6832E /* SettingsView.swift */; }; 043C819C27413D8000E6832E /* libgovarnam.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 04D0A67F273FC2BC006C3B54 /* libgovarnam.dylib */; }; 043C819D27413D8000E6832E /* libgovarnam.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 04D0A67F273FC2BC006C3B54 /* libgovarnam.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 0460D233274D668B005B91DB /* RecentlyLearnedWordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0460D232274D668B005B91DB /* RecentlyLearnedWordsView.swift */; }; 04D0A680273FC2BC006C3B54 /* libgovarnam.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 04D0A67F273FC2BC006C3B54 /* libgovarnam.dylib */; }; 04D0A681273FC2C0006C3B54 /* libgovarnam.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 04D0A67F273FC2BC006C3B54 /* libgovarnam.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 04D0A683273FC367006C3B54 /* Varnam.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D0A682273FC367006C3B54 /* Varnam.swift */; }; @@ -97,9 +99,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 040A907D27523D2F008E365B /* RLWTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RLWTable.swift; sourceTree = ""; }; 043C819027413A2400E6832E /* LipikaEngine_OSX.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LipikaEngine_OSX.framework; path = "../lipika-engine/build/Release/LipikaEngine_OSX.framework"; sourceTree = ""; }; 043C819527413C1E00E6832E /* govarnam-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "govarnam-Bridging-Header.h"; sourceTree = ""; }; 043C819A27413CB500E6832E /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 0460D232274D668B005B91DB /* RecentlyLearnedWordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyLearnedWordsView.swift; sourceTree = ""; }; 04D0A67F273FC2BC006C3B54 /* libgovarnam.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libgovarnam.dylib; sourceTree = ""; }; 04D0A682273FC367006C3B54 /* Varnam.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Varnam.swift; sourceTree = ""; }; 04D0A696274112BE006C3B54 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = ""; }; @@ -252,9 +256,11 @@ A52905DC234EC67900A8D95E /* MainView.swift */, A5E9827F2480658900D7B9A9 /* SettingsModel.swift */, A59E32552495A93300736334 /* LanguageTable.swift */, + 0460D232274D668B005B91DB /* RecentlyLearnedWordsView.swift */, A5E20D8324944B6E00EF7B1C /* LanguageView.swift */, A585A17C24AC28FC00816D1E /* PersistenceView.swift */, 043C819A27413CB500E6832E /* SettingsView.swift */, + 040A907D27523D2F008E365B /* RLWTable.swift */, ); path = Application; sourceTree = ""; @@ -492,7 +498,9 @@ A5698AE9247EFA8E00443158 /* MainView.swift in Sources */, A59E32562495A93300736334 /* LanguageTable.swift in Sources */, 043C818D274139D900E6832E /* Config.swift in Sources */, + 0460D233274D668B005B91DB /* RecentlyLearnedWordsView.swift in Sources */, 043C818F274139DE00E6832E /* Common.swift in Sources */, + 040A907E27523D2F008E365B /* RLWTable.swift in Sources */, A5E20D8424944B6E00EF7B1C /* LanguageView.swift in Sources */, A585A17D24AC28FC00816D1E /* PersistenceView.swift in Sources */, 043C818E274139DC00E6832E /* Logger.swift in Sources */,