-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLocalMessage.m
More file actions
334 lines (252 loc) · 10.1 KB
/
LocalMessage.m
File metadata and controls
334 lines (252 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
classdef LocalMessage < handle
% Top level manager object for the Local Message application.
properties (Access = private)
udpAgents; % Handles communicating between users.
userInterface; % UI handle
installDir; % Populated upon startup
settingsPath; % Populated upon startup
end
properties (GetAccess = {?LM_Interface,?LM_UnitBase}, SetAccess = private)
logger; % Orchestrates logging and debug messages
end
properties (GetAccess = ?LM_Interface, SetAccess = private)
userName;
userSettings; % Will be populated with struct
end
% Hard coded saved quantities
properties (Access = private, Constant)
settingsFile = '/workingfiles/user_settings.mat'; % Will be relative to installDir
% These will be used to fill missing info for the userSettings
defaultUserSettings = struct(...
... % 'userName' field always exists
'displayName','Unnamed',...
'displayInitials','?',...
'avatarColor',hsv2rgb([rand,1,1]),... % Randomly assigned color (full saturation)
'friendsList',{{}},... % Empty friends list
'visualSettings',LM_Interface.visualSettingsDefault... % Refer to LM_Interface for these defaults
);
end
methods (Access = public)
% Constructor
function this = LocalMessage()
% Create the logger.
this.logger = LM_Logger(this);
this.logger.info('Starting LM manager.');
this.determineUserName();
% Determine the install directory and important sub-paths
this.logger.info('Configuring paths.');
[this.installDir,~,~] = fileparts(mfilename('fullpath'));
this.settingsPath = fullfile(this.installDir,this.settingsFile);
% Perform startup sequence.
this.loadUserSettings();
this.initializeUDPAgents();
this.openUserInterface();
end
% Destructor
function delete(this)
this.logger.info('Initiating shut-down sequence.');
% Delete UDP agents
% Delete user interface
if ~isempty(this.userInterface) && isvalid(this.userInterface)
delete(this.userInterface);
end
this.logger.info('Closing LM manager.');
end
% This is a slightly more useful version of the built-on isvalid()
% for handle objects.
function isValid_ = isValid(this)
isValid_ = isvalid(this) && isValid(this.userInterface);
end
end
methods (Access = private)
% Stores username info inside this.userName
function determineUserName(this)
% Execute commandline call to request username.
timeout_s = 3.0;
[status,userNameRaw,~] = cmd('whoami',timeout_s);
% If something goes wrong, or if the timeout triggers before
% the command returns, status will be nonzero. Otherwise,
% userNameRaw contains the string that was returned.
if status == 0 % If successful
% Clean up some extra spaces around the part we want.
this.userName = userNameRaw(1:end-2);
this.logger.info('Client username: "%s".',this.userName);
else % Something went wrong.
this.logger.severe('The client username could not be extracted.');
end
end
% Safely loads full userSettings map
function userSettingsList = readUserSettings(this)
% If the file doesn't exist, make it, populate it with an empty
% list.
if ~exist(this.settingsPath,'file')
this.logger.info('User settings file not found. Saving empty map to file.');
this.saveUserSettings('empty');
end
% Cautiously read in the file
try % If the file is valid, load in the desired variable.
this.logger.info('Reading saved settings from file.');
loadedSettings = load(this.settingsPath,'userSettingsList');
userSettingsList = loadedSettings.userSettingsList;
% Confirm the variable is setup correctly.
if isa(userSettingsList,'containers.Map') && strcmp(userSettingsList.KeyType,'char') && strcmp(userSettingsList.ValueType,'any')
this.logger.info('Settings read successfully.');
else % Was not the right format map.
this.logger.info('Read data from file, but variable is in the wrong format. Creating from scratch.');
userSettingsList = this.saveUserSettings('empty');
end
catch % Some error happened, almost certainly in the load() call.
this.logger.info('Reading the file failed. Creating from scratch.');
userSettingsList = this.saveUserSettings('empty');
end
end
% Safely saves user settings to file. The mode input defaults to
% 'property' if omitted.
% When mode is 'property' the settings to write come from
% this.userSettings; when mode is 'empty', a new empty map
% container is made and written to file. A backup is made of
% pre-existing saved files.
% The output is a copy of what was written to file.
function userSettingsList = saveUserSettings(this,mode)
% If the mode was unspecified, assign it to its default.
if ~exist('mode','var')
mode = 'property';
end
% Sanitize inputs. Kinda overkill.
if ~any(strcmp(mode,{'property','empty'}))
this.logger.error('The mode specified was invalid. Defaulting to ''property''.');
mode = 'property';
end
% Assemble the file which will be written to file.
switch mode
case 'property'
% Read existing from file
userSettingsList = this.readUserSettings();
% Update this users settings
userSettingsList(this.userName) = this.userSettings;
case 'empty'
% Create empty map container
userSettingsList = this.makeEmptyMap();
% Check for pre-existing file
if exist(this.settingsPath,'file')
% Update the existing name with a (nearly) unique
% string appended to the end.
appendName = datestr(now,' yyyymmdd_HHMMSSFFF.bak');
try % This could fail if there are any read/write permission issues.
movefile(this.settingsPath,[this.settingsPath,appendName]);
this.info('Backup of user settings created: "%s%s".',this.settingsFile,appendName);
catch
this.logger.error('Creating the user settings backup failed.');
end
end
end % No need for otherwise.
% Now try to save the assembled settings to file.
try
save(this.settingsPath,'userSettingsList');
catch
this.logger.severe('Failed in writing user settings to file.');
end
end
% Extract the user's saved settings, if any.
function loadUserSettings(this)
this.logger.info('Importing user settings.');
% Look up the locally stored settings file to get the user
% settings.
userSettingsList = this.readUserSettings();
% Detect whether this is a new user.
if ~userSettingsList.isKey(this.userName) % is new user
userSettingsList(this.userName) = struct(... % Mostly empty struct
'userName',this.userName);
end
% Now that the entry necessarily exists, extract it, and pass
% it off to be applied and propagated.
this.applyUserSettings( userSettingsList(this.userName) ); % containers.Map lookup
end
% Check for changes to the user settings, apply them, and propagate
% those changes to the necessary components. newUserSettings may be
% a partial struct, containing only changes to userSettings.
% Manages assigning defaults to unspecified properties.
% The outputs report whether certain sections of the settings were
% changed.
% Performs saving internally, as necessary.
function [visChanged] = applyUserSettings(this,newUserSettings)
% We need a base for this merging process. If the user settings
% are already defined, use those. Otherwise, use the defaults.
% If the defaults are used, we definitely need to treat all
% settings as changed, since they were effectively undefined
% previously.
if isempty(this.userSettings)
userSettingsBase = this.defaultUserSettings;
allChangedOverride = true;
else
userSettingsBase = this.userSettings;
allChangedOverride = false;
end
% Merge the structs. The new struct overwrites matching fields
% from the base.
this.userSettings = recursiveMergeStruct(userSettingsBase,newUserSettings);
% Compare the base and merged versions, and detect what
% changed.
comparisonStruct = compareStructValues( userSettingsBase, this.userSettings );
visChanged = comparisonStruct.visualSettings || allChangedOverride;
% Call internal updating functions depending on what changed.
if visChanged
this.refreshVisualSettings();
end
end
function initializeUDPAgents(this)
this.logger.info('Initializing connections.');
% Create UDP stuff
this.logger.warning('NOT IMPLEMENTED.');
end
% Makes user interface
function openUserInterface(this)
this.logger.info('Opening GUI.');
this.userInterface = LM_Interface(this);
end
% Makes sure the visual settings are self-consistent.
function refreshVisualSettings(this)
end
end
methods (Access = ?LM_Interface)
% Prompts user about shutting down the application
function requestDelete(this,obj,~)
% If already deleted, just delete caller source
if ~isvalid(this)
delete(obj);
return
end
this.logger.info('LM object requested to shut down. Prompting user.');
% Ask the user about shutting down
response = questdlg('Exit the Local Message app?', ...
'Close Prompt', ...
'Exit','Cancel','Cancel');
if strcmp(response,'Exit')
% Call the hard delete function.
this.delete();
else
this.logger.info('Shut down request rescinded.');
end
end
% Saves the provided settings to the userSettings, and saves it to
% file.
function wrapper_applyUserSettings(this,partialUserSettings)
% Pass on changes to user settings
this.logger.info('Transferring user settings update.');
[visualSettingsChanged] = this.applyUserSettings(partialUserSettings);
% If the visual settings changed, kick off a GUI refresh.
if visualSettingsChanged
this.logger.info('Triggering GUI refresh.');
% GUI REFRESH
end
end
end
methods (Access = private, Static)
% Creates an empty map container with KeyType 'char' and ValueType
% 'any'.
function emptyMap = makeEmptyMap()
emptyMap = containers.Map({'empty'},{'empty'},'UniformValues',false); % Doesn't support being empty from construction.
emptyMap = emptyMap.remove('empty'); % but apparently is okay with this.
end
end
end