2022-06-23 18:43:24 +00:00
|
|
|
/* Copyright 2017, 2022 Joseph Wasson, Vladislav Kucheriavykh
|
2017-07-27 18:56:50 +00:00
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* 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. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
#include "process_steno.h"
|
2017-07-26 21:41:39 +00:00
|
|
|
#include "quantum_keycodes.h"
|
2023-07-11 07:07:24 +00:00
|
|
|
#include "eeconfig.h"
|
Improve state/chord handling and clean up namespace
Some values that can never, ever, change were held in local
variables, rather than in PROGMEM. Fixed.
Change "pressed" to a signed int so the test for < 0 makes
sense, and to avoid possible weird failure modes in the
case where a key release comes in when pressed is already
zero. (Shouldn't happen, sure, but computers are weird.)
A lot of things in process_steno had external linkage for no
particular reason. They've been marked static. Stuff still
builds.
Distinguish between currently-held keys and keys that have
been held, and expose these values through a nicely-named API
so other code could, say, check on the current set of steno
chording in order to make displays. Also in passing fix up the
"state" value having external linkage so it could clash with
other people's variable declarations.
The API also provides hooks for key processing and steno chord
events, so you can monitor those events without having to
run in matrix_scan_user and recheck the values directly. Also
document these.
There is no path through processing a key that doesn't
end with a return false, so the nested return foo() are
gone and we just return false.
2017-11-18 15:38:15 +00:00
|
|
|
#include <string.h>
|
2022-06-23 18:43:24 +00:00
|
|
|
#ifdef VIRTSER_ENABLE
|
|
|
|
# include "virtser.h"
|
|
|
|
#endif
|
|
|
|
#ifdef STENO_ENABLE_ALL
|
|
|
|
# include "eeprom.h"
|
|
|
|
#endif
|
2017-07-26 21:41:39 +00:00
|
|
|
|
2022-06-23 18:43:24 +00:00
|
|
|
// All steno keys that have been pressed to form this chord,
|
|
|
|
// stored in MAX_STROKE_SIZE groups of 8-bit arrays.
|
|
|
|
static uint8_t chord[MAX_STROKE_SIZE] = {0};
|
|
|
|
// The number of physical keys actually being held down.
|
|
|
|
// This is not always equal to the number of 1 bits in `chord` because it is possible to
|
|
|
|
// simultaneously press down four keys, then release three of those four keys and then press yet
|
|
|
|
// another key while the fourth finger is still holding down its key.
|
|
|
|
// At the end of this scenario given as an example, `chord` would have five bits set to 1 but
|
|
|
|
// `n_pressed_keys` would be set to 2 because there are only two keys currently being pressed down.
|
|
|
|
static int8_t n_pressed_keys = 0;
|
|
|
|
|
|
|
|
#ifdef STENO_ENABLE_ALL
|
Improve state/chord handling and clean up namespace
Some values that can never, ever, change were held in local
variables, rather than in PROGMEM. Fixed.
Change "pressed" to a signed int so the test for < 0 makes
sense, and to avoid possible weird failure modes in the
case where a key release comes in when pressed is already
zero. (Shouldn't happen, sure, but computers are weird.)
A lot of things in process_steno had external linkage for no
particular reason. They've been marked static. Stuff still
builds.
Distinguish between currently-held keys and keys that have
been held, and expose these values through a nicely-named API
so other code could, say, check on the current set of steno
chording in order to make displays. Also in passing fix up the
"state" value having external linkage so it could clash with
other people's variable declarations.
The API also provides hooks for key processing and steno chord
events, so you can monitor those events without having to
run in matrix_scan_user and recheck the values directly. Also
document these.
There is no path through processing a key that doesn't
end with a return false, so the nested return foo() are
gone and we just return false.
2017-11-18 15:38:15 +00:00
|
|
|
static steno_mode_t mode;
|
2022-06-23 18:43:24 +00:00
|
|
|
#elif defined(STENO_ENABLE_GEMINI)
|
|
|
|
static const steno_mode_t mode = STENO_MODE_GEMINI;
|
|
|
|
#elif defined(STENO_ENABLE_BOLT)
|
|
|
|
static const steno_mode_t mode = STENO_MODE_BOLT;
|
2021-08-17 18:48:00 +00:00
|
|
|
#endif
|
|
|
|
|
2022-06-23 18:43:24 +00:00
|
|
|
static inline void steno_clear_chord(void) {
|
2019-08-30 18:19:03 +00:00
|
|
|
memset(chord, 0, sizeof(chord));
|
Improve state/chord handling and clean up namespace
Some values that can never, ever, change were held in local
variables, rather than in PROGMEM. Fixed.
Change "pressed" to a signed int so the test for < 0 makes
sense, and to avoid possible weird failure modes in the
case where a key release comes in when pressed is already
zero. (Shouldn't happen, sure, but computers are weird.)
A lot of things in process_steno had external linkage for no
particular reason. They've been marked static. Stuff still
builds.
Distinguish between currently-held keys and keys that have
been held, and expose these values through a nicely-named API
so other code could, say, check on the current set of steno
chording in order to make displays. Also in passing fix up the
"state" value having external linkage so it could clash with
other people's variable declarations.
The API also provides hooks for key processing and steno chord
events, so you can monitor those events without having to
run in matrix_scan_user and recheck the values directly. Also
document these.
There is no path through processing a key that doesn't
end with a return false, so the nested return foo() are
gone and we just return false.
2017-11-18 15:38:15 +00:00
|
|
|
}
|
|
|
|
|
2022-06-23 18:43:24 +00:00
|
|
|
#ifdef STENO_ENABLE_GEMINI
|
|
|
|
|
|
|
|
# ifdef VIRTSER_ENABLE
|
|
|
|
void send_steno_chord_gemini(void) {
|
|
|
|
// Set MSB to 1 to indicate the start of packet
|
|
|
|
chord[0] |= 0x80;
|
|
|
|
for (uint8_t i = 0; i < GEMINI_STROKE_SIZE; ++i) {
|
|
|
|
virtser_send(chord[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
# else
|
|
|
|
# pragma message "VIRTSER_ENABLE = yes is required for Gemini PR to work properly out of the box!"
|
|
|
|
# endif // VIRTSER_ENABLE
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @precondition: `key` is pressed
|
|
|
|
*/
|
|
|
|
bool add_gemini_key_to_chord(uint8_t key) {
|
|
|
|
// Although each group of the packet is 8 bits long, the MSB is reserved
|
|
|
|
// to indicate whether that byte is the first byte of the packet (MSB=1)
|
|
|
|
// or one of the remaining five bytes of the packet (MSB=0).
|
|
|
|
// As a consequence, only 7 out of the 8 bits are left to be used as a bit array
|
|
|
|
// for the steno keys of that group.
|
|
|
|
const int group_idx = key / 7;
|
|
|
|
const int intra_group_idx = key - group_idx * 7;
|
|
|
|
// The 0th steno key of the group has bit=0b01000000, the 1st has bit=0b00100000, etc.
|
|
|
|
const uint8_t bit = 1 << (6 - intra_group_idx);
|
|
|
|
chord[group_idx] |= bit;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
#endif // STENO_ENABLE_GEMINI
|
|
|
|
|
|
|
|
#ifdef STENO_ENABLE_BOLT
|
|
|
|
|
|
|
|
# define TXB_GRP0 0b00000000
|
|
|
|
# define TXB_GRP1 0b01000000
|
|
|
|
# define TXB_GRP2 0b10000000
|
|
|
|
# define TXB_GRP3 0b11000000
|
|
|
|
# define TXB_GRPMASK 0b11000000
|
|
|
|
|
|
|
|
# define TXB_GET_GROUP(code) ((code & TXB_GRPMASK) >> 6)
|
|
|
|
|
|
|
|
static const uint8_t boltmap[64] PROGMEM = {TXB_NUL, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_S_L, TXB_S_L, TXB_T_L, TXB_K_L, TXB_P_L, TXB_W_L, TXB_H_L, TXB_R_L, TXB_A_L, TXB_O_L, TXB_STR, TXB_STR, TXB_NUL, TXB_NUL, TXB_NUL, TXB_STR, TXB_STR, TXB_E_R, TXB_U_R, TXB_F_R, TXB_R_R, TXB_P_R, TXB_B_R, TXB_L_R, TXB_G_R, TXB_T_R, TXB_S_R, TXB_D_R, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_NUM, TXB_Z_R};
|
|
|
|
|
|
|
|
# ifdef VIRTSER_ENABLE
|
|
|
|
static void send_steno_chord_bolt(void) {
|
|
|
|
for (uint8_t i = 0; i < BOLT_STROKE_SIZE; ++i) {
|
|
|
|
// TX Bolt uses variable length packets where each byte corresponds to a bit array of certain keys.
|
|
|
|
// If a user chorded the keys of the first group with keys of the last group, for example, there
|
|
|
|
// would be bytes of 0x00 in `chord` for the middle groups which we mustn't send.
|
|
|
|
if (chord[i]) {
|
2019-08-30 18:19:03 +00:00
|
|
|
virtser_send(chord[i]);
|
|
|
|
}
|
Improve state/chord handling and clean up namespace
Some values that can never, ever, change were held in local
variables, rather than in PROGMEM. Fixed.
Change "pressed" to a signed int so the test for < 0 makes
sense, and to avoid possible weird failure modes in the
case where a key release comes in when pressed is already
zero. (Shouldn't happen, sure, but computers are weird.)
A lot of things in process_steno had external linkage for no
particular reason. They've been marked static. Stuff still
builds.
Distinguish between currently-held keys and keys that have
been held, and expose these values through a nicely-named API
so other code could, say, check on the current set of steno
chording in order to make displays. Also in passing fix up the
"state" value having external linkage so it could clash with
other people's variable declarations.
The API also provides hooks for key processing and steno chord
events, so you can monitor those events without having to
run in matrix_scan_user and recheck the values directly. Also
document these.
There is no path through processing a key that doesn't
end with a return false, so the nested return foo() are
gone and we just return false.
2017-11-18 15:38:15 +00:00
|
|
|
}
|
2022-06-23 18:43:24 +00:00
|
|
|
// Sending a null packet is not always necessary, but it is simpler and more reliable
|
|
|
|
// to unconditionally send it every time instead of keeping track of more states and
|
|
|
|
// creating more branches in the execution of the program.
|
|
|
|
virtser_send(0);
|
2017-07-27 04:51:41 +00:00
|
|
|
}
|
2022-06-23 18:43:24 +00:00
|
|
|
# else
|
|
|
|
# pragma message "VIRTSER_ENABLE = yes is required for TX Bolt to work properly out of the box!"
|
|
|
|
# endif // VIRTSER_ENABLE
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @precondition: `key` is pressed
|
|
|
|
*/
|
|
|
|
static bool add_bolt_key_to_chord(uint8_t key) {
|
|
|
|
uint8_t boltcode = pgm_read_byte(boltmap + key);
|
|
|
|
chord[TXB_GET_GROUP(boltcode)] |= boltcode;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
#endif // STENO_ENABLE_BOLT
|
|
|
|
|
|
|
|
#ifdef STENO_COMBINEDMAP
|
|
|
|
/* Used to look up when pressing the middle row key to combine two consonant or vowel keys */
|
|
|
|
static const uint16_t combinedmap_first[] PROGMEM = {STN_S1, STN_TL, STN_PL, STN_HL, STN_FR, STN_PR, STN_LR, STN_TR, STN_DR, STN_A, STN_E};
|
|
|
|
static const uint16_t combinedmap_second[] PROGMEM = {STN_S2, STN_KL, STN_WL, STN_RL, STN_RR, STN_BR, STN_GR, STN_SR, STN_ZR, STN_O, STN_U};
|
|
|
|
#endif
|
2017-07-27 04:51:41 +00:00
|
|
|
|
2022-06-23 18:43:24 +00:00
|
|
|
#ifdef STENO_ENABLE_ALL
|
2023-01-20 16:21:17 +00:00
|
|
|
void steno_init(void) {
|
2019-08-30 18:19:03 +00:00
|
|
|
if (!eeconfig_is_enabled()) {
|
|
|
|
eeconfig_init();
|
|
|
|
}
|
|
|
|
mode = eeprom_read_byte(EECONFIG_STENOMODE);
|
2017-07-27 04:51:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void steno_set_mode(steno_mode_t new_mode) {
|
2022-06-23 18:43:24 +00:00
|
|
|
steno_clear_chord();
|
2019-08-30 18:19:03 +00:00
|
|
|
mode = new_mode;
|
|
|
|
eeprom_update_byte(EECONFIG_STENOMODE, mode);
|
2017-07-27 04:51:41 +00:00
|
|
|
}
|
2022-06-23 18:43:24 +00:00
|
|
|
#endif // STENO_ENABLE_ALL
|
2017-07-27 04:51:41 +00:00
|
|
|
|
Improve state/chord handling and clean up namespace
Some values that can never, ever, change were held in local
variables, rather than in PROGMEM. Fixed.
Change "pressed" to a signed int so the test for < 0 makes
sense, and to avoid possible weird failure modes in the
case where a key release comes in when pressed is already
zero. (Shouldn't happen, sure, but computers are weird.)
A lot of things in process_steno had external linkage for no
particular reason. They've been marked static. Stuff still
builds.
Distinguish between currently-held keys and keys that have
been held, and expose these values through a nicely-named API
so other code could, say, check on the current set of steno
chording in order to make displays. Also in passing fix up the
"state" value having external linkage so it could clash with
other people's variable declarations.
The API also provides hooks for key processing and steno chord
events, so you can monitor those events without having to
run in matrix_scan_user and recheck the values directly. Also
document these.
There is no path through processing a key that doesn't
end with a return false, so the nested return foo() are
gone and we just return false.
2017-11-18 15:38:15 +00:00
|
|
|
/* override to intercept chords right before they get sent.
|
|
|
|
* return zero to suppress normal sending behavior.
|
|
|
|
*/
|
2022-06-23 18:43:24 +00:00
|
|
|
__attribute__((weak)) bool send_steno_chord_user(steno_mode_t mode, uint8_t chord[MAX_STROKE_SIZE]) {
|
2022-02-12 18:29:31 +00:00
|
|
|
return true;
|
|
|
|
}
|
Improve state/chord handling and clean up namespace
Some values that can never, ever, change were held in local
variables, rather than in PROGMEM. Fixed.
Change "pressed" to a signed int so the test for < 0 makes
sense, and to avoid possible weird failure modes in the
case where a key release comes in when pressed is already
zero. (Shouldn't happen, sure, but computers are weird.)
A lot of things in process_steno had external linkage for no
particular reason. They've been marked static. Stuff still
builds.
Distinguish between currently-held keys and keys that have
been held, and expose these values through a nicely-named API
so other code could, say, check on the current set of steno
chording in order to make displays. Also in passing fix up the
"state" value having external linkage so it could clash with
other people's variable declarations.
The API also provides hooks for key processing and steno chord
events, so you can monitor those events without having to
run in matrix_scan_user and recheck the values directly. Also
document these.
There is no path through processing a key that doesn't
end with a return false, so the nested return foo() are
gone and we just return false.
2017-11-18 15:38:15 +00:00
|
|
|
|
2022-07-29 04:51:01 +00:00
|
|
|
__attribute__((weak)) bool post_process_steno_user(uint16_t keycode, keyrecord_t *record, steno_mode_t mode, uint8_t chord[MAX_STROKE_SIZE], int8_t n_pressed_keys) {
|
2022-02-12 18:29:31 +00:00
|
|
|
return true;
|
|
|
|
}
|
Improve state/chord handling and clean up namespace
Some values that can never, ever, change were held in local
variables, rather than in PROGMEM. Fixed.
Change "pressed" to a signed int so the test for < 0 makes
sense, and to avoid possible weird failure modes in the
case where a key release comes in when pressed is already
zero. (Shouldn't happen, sure, but computers are weird.)
A lot of things in process_steno had external linkage for no
particular reason. They've been marked static. Stuff still
builds.
Distinguish between currently-held keys and keys that have
been held, and expose these values through a nicely-named API
so other code could, say, check on the current set of steno
chording in order to make displays. Also in passing fix up the
"state" value having external linkage so it could clash with
other people's variable declarations.
The API also provides hooks for key processing and steno chord
events, so you can monitor those events without having to
run in matrix_scan_user and recheck the values directly. Also
document these.
There is no path through processing a key that doesn't
end with a return false, so the nested return foo() are
gone and we just return false.
2017-11-18 15:38:15 +00:00
|
|
|
|
2022-02-12 18:29:31 +00:00
|
|
|
__attribute__((weak)) bool process_steno_user(uint16_t keycode, keyrecord_t *record) {
|
|
|
|
return true;
|
|
|
|
}
|
Improve state/chord handling and clean up namespace
Some values that can never, ever, change were held in local
variables, rather than in PROGMEM. Fixed.
Change "pressed" to a signed int so the test for < 0 makes
sense, and to avoid possible weird failure modes in the
case where a key release comes in when pressed is already
zero. (Shouldn't happen, sure, but computers are weird.)
A lot of things in process_steno had external linkage for no
particular reason. They've been marked static. Stuff still
builds.
Distinguish between currently-held keys and keys that have
been held, and expose these values through a nicely-named API
so other code could, say, check on the current set of steno
chording in order to make displays. Also in passing fix up the
"state" value having external linkage so it could clash with
other people's variable declarations.
The API also provides hooks for key processing and steno chord
events, so you can monitor those events without having to
run in matrix_scan_user and recheck the values directly. Also
document these.
There is no path through processing a key that doesn't
end with a return false, so the nested return foo() are
gone and we just return false.
2017-11-18 15:38:15 +00:00
|
|
|
|
2022-06-23 18:43:24 +00:00
|
|
|
bool process_steno(uint16_t keycode, keyrecord_t *record) {
|
|
|
|
if (keycode < QK_STENO || keycode > QK_STENO_MAX) {
|
|
|
|
return true; // Not a steno key, pass it further along the chain
|
|
|
|
/*
|
|
|
|
* Clearing or sending the chord state is not necessary as we intentionally ignore whatever
|
|
|
|
* normal keyboard keys the user may have tapped while chording steno keys.
|
|
|
|
*/
|
2017-07-26 21:41:39 +00:00
|
|
|
}
|
2022-06-23 18:43:24 +00:00
|
|
|
if (IS_NOEVENT(record->event)) {
|
|
|
|
return true;
|
2019-08-30 18:19:03 +00:00
|
|
|
}
|
2022-06-23 18:43:24 +00:00
|
|
|
if (!process_steno_user(keycode, record)) {
|
|
|
|
return false; // User fully processed the steno key themselves
|
2019-08-30 18:19:03 +00:00
|
|
|
}
|
|
|
|
switch (keycode) {
|
2022-06-23 18:43:24 +00:00
|
|
|
#ifdef STENO_ENABLE_ALL
|
2019-08-30 18:19:03 +00:00
|
|
|
case QK_STENO_BOLT:
|
2023-04-03 08:33:45 +00:00
|
|
|
if (record->event.pressed) {
|
2019-08-30 18:19:03 +00:00
|
|
|
steno_set_mode(STENO_MODE_BOLT);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
|
|
|
|
case QK_STENO_GEMINI:
|
2023-04-03 08:33:45 +00:00
|
|
|
if (record->event.pressed) {
|
2019-08-30 18:19:03 +00:00
|
|
|
steno_set_mode(STENO_MODE_GEMINI);
|
|
|
|
}
|
|
|
|
return false;
|
2022-06-23 18:43:24 +00:00
|
|
|
#endif // STENO_ENABLE_ALL
|
2019-08-30 18:19:03 +00:00
|
|
|
|
2021-08-17 18:48:00 +00:00
|
|
|
#ifdef STENO_COMBINEDMAP
|
2021-11-01 19:18:33 +00:00
|
|
|
case QK_STENO_COMB ... QK_STENO_COMB_MAX: {
|
2022-06-23 18:43:24 +00:00
|
|
|
bool first_result = process_steno(combinedmap_first[keycode - QK_STENO_COMB], record);
|
|
|
|
bool second_result = process_steno(combinedmap_second[keycode - QK_STENO_COMB], record);
|
|
|
|
return first_result && second_result;
|
2021-08-17 18:48:00 +00:00
|
|
|
}
|
2022-06-23 18:43:24 +00:00
|
|
|
#endif // STENO_COMBINEDMAP
|
2019-08-30 18:19:03 +00:00
|
|
|
case STN__MIN ... STN__MAX:
|
2023-04-03 08:33:45 +00:00
|
|
|
if (record->event.pressed) {
|
2022-06-23 18:43:24 +00:00
|
|
|
n_pressed_keys++;
|
|
|
|
switch (mode) {
|
|
|
|
#ifdef STENO_ENABLE_BOLT
|
|
|
|
case STENO_MODE_BOLT:
|
|
|
|
add_bolt_key_to_chord(keycode - QK_STENO);
|
|
|
|
break;
|
|
|
|
#endif // STENO_ENABLE_BOLT
|
|
|
|
#ifdef STENO_ENABLE_GEMINI
|
|
|
|
case STENO_MODE_GEMINI:
|
|
|
|
add_gemini_key_to_chord(keycode - QK_STENO);
|
|
|
|
break;
|
|
|
|
#endif // STENO_ENABLE_GEMINI
|
|
|
|
default:
|
|
|
|
return false;
|
2019-08-30 18:19:03 +00:00
|
|
|
}
|
2022-07-29 04:51:01 +00:00
|
|
|
if (!post_process_steno_user(keycode, record, mode, chord, n_pressed_keys)) {
|
2022-06-23 18:43:24 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else { // is released
|
|
|
|
n_pressed_keys--;
|
2022-07-29 04:51:01 +00:00
|
|
|
if (!post_process_steno_user(keycode, record, mode, chord, n_pressed_keys)) {
|
2022-06-23 18:43:24 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (n_pressed_keys > 0) {
|
|
|
|
// User hasn't released all keys yet,
|
|
|
|
// so the chord cannot be sent
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
n_pressed_keys = 0;
|
|
|
|
if (!send_steno_chord_user(mode, chord)) {
|
|
|
|
steno_clear_chord();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
switch (mode) {
|
|
|
|
#if defined(STENO_ENABLE_BOLT) && defined(VIRTSER_ENABLE)
|
|
|
|
case STENO_MODE_BOLT:
|
|
|
|
send_steno_chord_bolt();
|
|
|
|
break;
|
|
|
|
#endif // STENO_ENABLE_BOLT && VIRTSER_ENABLE
|
|
|
|
#if defined(STENO_ENABLE_GEMINI) && defined(VIRTSER_ENABLE)
|
|
|
|
case STENO_MODE_GEMINI:
|
|
|
|
send_steno_chord_gemini();
|
|
|
|
break;
|
|
|
|
#endif // STENO_ENABLE_GEMINI && VIRTSER_ENABLE
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
steno_clear_chord();
|
2019-08-30 18:19:03 +00:00
|
|
|
}
|
2022-06-23 18:43:24 +00:00
|
|
|
break;
|
2019-08-30 18:19:03 +00:00
|
|
|
}
|
2022-06-23 18:43:24 +00:00
|
|
|
return false;
|
2017-07-26 21:41:39 +00:00
|
|
|
}
|