diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9050a5b..292ffee 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: '' -labels: enhancement +labels: bug assignees: '' --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dac55a..22a7850 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Change Log +## v3.4.0-a1 - 2024-01-28 + +### New Features: + +* Discord webhooks completely re-worked. Now, instead of a single, fixed webhook for each type of Discord post, there is a fully flexible table of webhooks which you can set up any way you like - a single webhook for all Discord posts; a webhook for each type of Discord post; multiple webhooks for each type, or any combination of these. As one example, this would allow you to send your BGS reports to multiple Discord servers if you wish. +* The system title and a link to the Inara page for the system are now shown at the top of every activity panel. + +### Changes: + +* Heading styles have been standardised across all windows. And headings are now purple, yay! +* URL link styles have been standardised across all windows. +* When posting CMDR info to Discord, now include how you interacted with them, colour coded. + +### Bug Fixes: + +* Thargoid vessel types in mission reports were still showing if they were 0. These are now omitted. +* Fix error when fetching carrier data when carrier has no sell orders. + + ## v3.3.0 - 2023-12-09 ### New Features: @@ -20,7 +39,7 @@ * Fix (another) crash in code that detects drop from supercruise at megaships. -### API Changes ([v1.3](https://studio-ws.apicur.io/sharing/281a84ad-dca9-42da-a08b-84e4b9af1b7e)): +### API Changes ([v1.3](https://studio-ws.apicur.io/sharing/d352797e-c40e-4f91-bcd8-773a14f40fc0)): * `/events` endpoint: All localised fields are now stripped before sending. i.e. fields who's name ends with `_Localised`. * `/activities` endpoint: Added `banshee` to `systems/[system]/twkills`. diff --git a/bgstally/activity.py b/bgstally/activity.py index bd1c5ee..43f3213 100644 --- a/bgstally/activity.py +++ b/bgstally/activity.py @@ -89,7 +89,7 @@ class Activity: factions with their activity """ - def __init__(self, bgstally, tick: Tick = None, discord_bgs_messageid: str = None): + def __init__(self, bgstally, tick: Tick = None): """ Instantiate using a given Tick """ @@ -100,8 +100,7 @@ def __init__(self, bgstally, tick: Tick = None, discord_bgs_messageid: str = Non self.tick_id: str = tick.tick_id self.tick_time: datetime = tick.tick_time self.tick_forced: bool = False - self.discord_bgs_messageid: str = discord_bgs_messageid - self.discord_tw_messageid: str = None + self.discord_webhook_data:dict = {} # key = webhook uuid, value = dict containing webhook data self.discord_notes: str = "" self.dirty: bool = False self.systems: dict = {} @@ -1289,7 +1288,7 @@ def _generate_tw_system_text(self, system: dict, discord: bool): if (system_station['passengers']['sum'] > 0): system_text += f" ๐Ÿง x {green(system_station['passengers']['sum'], fp=fp)} - {green(system_station['passengers']['count'], fp=fp)} missions\n" if (sum(x['sum'] for x in system_station['massacre'].values())) > 0: - system_text += f" ๐Ÿ’€ (missions): " + self._build_tw_vessels_text(system_station['massacre'], discord) + f"- {green((sum(x['count'] for x in system_station['massacre'].values())), fp=fp)} missions\n" + system_text += f" ๐Ÿ’€ (missions): " + self._build_tw_vessels_text(system_station['massacre'], discord) + f" - {green((sum(x['count'] for x in system_station['massacre'].values())), fp=fp)} missions\n" if (system_station['reactivate'] > 0): system_text += f" ๐Ÿ› ๏ธ x {green(system_station['reactivate'], fp=fp)} missions\n" @@ -1335,7 +1334,7 @@ def _build_tw_vessels_text(self, tw_data: dict, discord: bool) -> str: if isinstance(v, dict): value = int(v.get('sum', 0)) else: value = int(v) - if v == 0: continue + if value == 0: continue if not first: text += ", " text += f"{red(label, fp=fp)} x {green(value, fp=fp)}" @@ -1363,8 +1362,7 @@ def _as_dict(self): 'tickid': self.tick_id, 'ticktime': self.tick_time.strftime(DATETIME_FORMAT_ACTIVITY), 'tickforced': self.tick_forced, - 'discordmessageid': self.discord_bgs_messageid, - 'discordtwmessageid': self.discord_tw_messageid, + 'discordwebhookdata': self.discord_webhook_data, 'discordnotes': self.discord_notes, 'systems': self.systems} @@ -1376,10 +1374,9 @@ def _from_dict(self, dict: Dict): self.tick_id = dict.get('tickid') self.tick_time = datetime.strptime(dict.get('ticktime'), DATETIME_FORMAT_ACTIVITY) self.tick_forced = dict.get('tickforced', False) - self.discord_bgs_messageid = dict.get('discordmessageid') - self.discord_tw_messageid = dict.get('discordtwmessageid') - self.discord_notes = dict.get('discordnotes') - self.systems = dict.get('systems') + self.discord_webhook_data = dict.get('discordwebhookdata', {}) + self.discord_notes = dict.get('discordnotes', "") + self.systems = dict.get('systems', {}) @@ -1421,12 +1418,11 @@ def __deepcopy__(self, memo): setattr(result, 'tick_id', self.tick_id) setattr(result, 'tick_time', self.tick_time) setattr(result, 'tick_forced', self.tick_forced) - setattr(result, 'discord_bgs_messageid', self.discord_bgs_messageid) - setattr(result, 'discord_tw_messageid', self.discord_tw_messageid) setattr(result, 'discord_notes', self.discord_notes) setattr(result, 'megaship_pat', self.megaship_pat) # Deep copied items setattr(result, 'systems', deepcopy(self.systems, memo)) + setattr(result, 'discord_webhook_data', deepcopy(self.discord_webhook_data, memo)) return result diff --git a/bgstally/activitymanager.py b/bgstally/activitymanager.py index 4581b10..adf2ab3 100644 --- a/bgstally/activitymanager.py +++ b/bgstally/activitymanager.py @@ -76,8 +76,7 @@ def new_tick(self, tick: Tick, forced: bool) -> bool: new_activity.tick_id = tick.tick_id new_activity.tick_time = tick.tick_time new_activity.tick_forced = forced - new_activity.discord_bgs_messageid = None - new_activity.discord_tw_messageid = None + new_activity.discord_webhook_data = {} new_activity.discord_notes = "" new_activity.clear_activity(self.bgstally.mission_log) self.activity_data.append(new_activity) @@ -104,14 +103,14 @@ def _load(self): # Handle legacy data if it exists - parse and migrate to new format filepath = path.join(self.bgstally.plugin_dir, FILE_LEGACY_PREVIOUSDATA) - if path.exists(filepath): self._convert_legacy_data(filepath, Tick(self.bgstally), config.get_str('XDiscordPreviousMessageID')) # Fake a tick for previous legacy - we don't have tick_id or tick_time + if path.exists(filepath): self._convert_legacy_data(filepath, Tick(self.bgstally)) # Fake a tick for previous legacy - we don't have tick_id or tick_time filepath = path.join(self.bgstally.plugin_dir, FILE_LEGACY_CURRENTDATA) - if path.exists(filepath): self._convert_legacy_data(filepath, self.bgstally.tick, config.get_str('XDiscordCurrentMessageID')) + if path.exists(filepath): self._convert_legacy_data(filepath, self.bgstally.tick) self.activity_data.sort(reverse=True) - def _convert_legacy_data(self, filepath: str, tick: Tick, discord_bgs_messageid: str): + def _convert_legacy_data(self, filepath: str, tick: Tick): """ Convert a legacy activity data file to new location and format. """ @@ -122,7 +121,7 @@ def _convert_legacy_data(self, filepath: str, tick: Tick, discord_bgs_messageid: remove(filepath) return - activity = Activity(self.bgstally, tick, discord_bgs_messageid) + activity = Activity(self.bgstally, tick) activity.load_legacy_data(filepath) activity.save(path.join(self.bgstally.plugin_dir, FOLDER_ACTIVITYDATA, activity.get_filename())) self.activity_data.append(activity) diff --git a/bgstally/bgstally.py b/bgstally/bgstally.py index eab823b..4443c2a 100644 --- a/bgstally/bgstally.py +++ b/bgstally/bgstally.py @@ -25,6 +25,7 @@ from bgstally.ui import UI from bgstally.updatemanager import UpdateManager from bgstally.utils import get_by_path +from bgstally.webhookmanager import WebhookManager from config import appversion, config TIME_WORKER_PERIOD_S = 60 @@ -79,6 +80,7 @@ def plugin_start(self, plugin_dir: str): self.market:Market = Market(self) self.request_manager:RequestManager = RequestManager(self) self.api_manager:APIManager = APIManager(self) + self.webhook_manager:WebhookManager = WebhookManager(self) self.update_manager:UpdateManager = UpdateManager(self) self.ui:UI = UI(self) @@ -302,6 +304,7 @@ def save_data(self): self.state.save() self.fleet_carrier.save() self.api_manager.save() + self.webhook_manager.save() def new_tick(self, force: bool, uipolicy: UpdateUIPolicy): diff --git a/bgstally/constants.py b/bgstally/constants.py index e298aa9..ad09188 100644 --- a/bgstally/constants.py +++ b/bgstally/constants.py @@ -30,13 +30,14 @@ class UpdateUIPolicy(Enum): IMMEDIATE = 1 LATER = 2 - -class DiscordChannel(Enum): - BGS = 0 - CMDR_INFORMATION = 1 - FLEETCARRIER_MATERIALS = 2 - FLEETCARRIER_OPERATIONS = 3 - THARGOIDWAR = 4 +# Discord channels +# Subclassing from str as well as Enum means json.load and json.dump work seamlessly +class DiscordChannel(str, Enum): + BGS = 'BGS' + CMDR_INFORMATION = 'CMDR-info' + FLEETCARRIER_MATERIALS = 'FC-mats' + FLEETCARRIER_OPERATIONS = 'FC-ops' + THARGOIDWAR = 'TW' class MaterialsCategory(Enum): @@ -65,14 +66,26 @@ class RequestMethod(Enum): OPTIONS = 'options' +class CmdrInteractionReason(int, Enum): + SCANNED = 0 + FRIEND_REQUEST_RECEIVED = 1 + INTERDICTED_BY = 2 + KILLED_BY = 3 + MESSAGE_RECEIVED = 4 + TEAM_INVITE_RECEIVED = 5 + + DATETIME_FORMAT_JOURNAL = "%Y-%m-%dT%H:%M:%SZ" FILE_SUFFIX = ".json" FOLDER_ASSETS = "assets" FOLDER_DATA = "otherdata" FOLDER_BACKUPS = "backups" FOLDER_UPDATES = "updates" -FONT_HEADING:tuple = ("Helvetica", 11, "bold") -FONT_TEXT:tuple = ("Helvetica", 11) +FONT_HEADING_1:tuple = ("Helvetica", 13, "bold") +FONT_HEADING_2:tuple = ("Helvetica", 11, "bold") +FONT_TEXT:tuple = ("Helvetica", 11, "normal") FONT_TEXT_BOLD:tuple = ("Helvetica", 11, "bold") FONT_TEXT_UNDERLINE:tuple = ("Helvetica", 11, "underline") FONT_TEXT_BOLD_UNDERLINE:tuple = ("Helvetica", 11, "bold underline") +FONT_SMALL:tuple = ("Helvetica", 9, "normal") +COLOUR_HEADING_1 = "#A300A3" diff --git a/bgstally/discord.py b/bgstally/discord.py index 2b3b441..93398d4 100644 --- a/bgstally/discord.py +++ b/bgstally/discord.py @@ -1,3 +1,4 @@ +from copy import deepcopy from datetime import datetime from requests import Response @@ -19,69 +20,89 @@ def __init__(self, bgstally): self.bgstally = bgstally - def post_plaintext(self, discord_text:str, previous_messageid:str, channel:DiscordChannel, callback:callable): + def post_plaintext(self, discord_text:str, webhooks_data:dict|None, channel:DiscordChannel, callback:callable): """ Post plain text to Discord """ - webhook_url = self._get_webhook(channel) - if not self._is_webhook_valid(webhook_url): return - - utc_time_now = datetime.utcnow().strftime(DATETIME_FORMAT) - data:dict = {'channel': channel, 'callback': callback, 'webhook_url': webhook_url} # Data that's carried through the request queue and back to the callback - - if previous_messageid == "" or previous_messageid == None: - # No previous post - if discord_text == "": return - - discord_text += f"```ansi\n{blue(f'Posted at: {utc_time_now} | {self.bgstally.plugin_name} v{str(self.bgstally.version)}')}```" - url = webhook_url - payload:dict = {'content': discord_text, 'username': self.bgstally.state.DiscordUsername.get(), 'embeds': []} - - self.bgstally.request_manager.queue_request(url, RequestMethod.POST, payload=payload, callback=self._request_complete, data=data) - else: - # Previous post - if discord_text != "": - discord_text += f"```ansi\n{green(f'Updated at: {utc_time_now} | {self.bgstally.plugin_name} v{str(self.bgstally.version)}')}```" - url = f"{webhook_url}/messages/{previous_messageid}" + # Start with latest webhooks from manager. Will contain True / False for each channel. Copy dict so we don't affect the webhook manager data. + webhooks:dict = deepcopy(self.bgstally.webhook_manager.get_webhooks_as_dict(channel)) + + for webhook in webhooks.values(): + webhook_url:str = webhook.get('url') + if not self._is_webhook_valid(webhook_url): return + + # Get the previous state for this webhook's uuid from the passed in data, if it exists. Default to the state from the webhook manager + specific_webhook_data:dict = {} if webhooks_data is None else webhooks_data.get(webhook.get('uuid', ""), webhook) + + utc_time_now:str = datetime.utcnow().strftime(DATETIME_FORMAT) + data:dict = {'channel': channel, 'callback': callback, 'webhookdata': specific_webhook_data} # Data that's carried through the request queue and back to the callback + + # Fetch the previous post ID, if present, from the webhook data for the channel we're posting in. May be the default True / False value + previous_messageid:str = specific_webhook_data.get(channel, None) + + if previous_messageid == "" or previous_messageid == None or previous_messageid == True or previous_messageid == False: + # No previous post + if discord_text == "": return + + discord_text += f"```ansi\n{blue(f'Posted at: {utc_time_now} | {self.bgstally.plugin_name} v{str(self.bgstally.version)}')}```" + url:str = webhook_url payload:dict = {'content': discord_text, 'username': self.bgstally.state.DiscordUsername.get(), 'embeds': []} - self.bgstally.request_manager.queue_request(url, RequestMethod.PATCH, payload=payload, callback=self._request_complete, data=data) + self.bgstally.request_manager.queue_request(url, RequestMethod.POST, payload=payload, callback=self._request_complete, data=data) else: - url = f"{webhook_url}/messages/{previous_messageid}" + # Previous post + if discord_text != "": + discord_text += f"```ansi\n{green(f'Updated at: {utc_time_now} | {self.bgstally.plugin_name} v{str(self.bgstally.version)}')}```" + url:str = f"{webhook_url}/messages/{previous_messageid}" + payload:dict = {'content': discord_text, 'username': self.bgstally.state.DiscordUsername.get(), 'embeds': []} + + self.bgstally.request_manager.queue_request(url, RequestMethod.PATCH, payload=payload, callback=self._request_complete, data=data) + else: + url:str = f"{webhook_url}/messages/{previous_messageid}" - self.bgstally.request_manager.queue_request(url, RequestMethod.DELETE, callback=self._request_complete, data=data) + self.bgstally.request_manager.queue_request(url, RequestMethod.DELETE, callback=self._request_complete, data=data) - def post_embed(self, title:str, description:str, fields:list, previous_messageid:str, channel:DiscordChannel, callback:callable): + def post_embed(self, title:str, description:str, fields:list, webhooks_data:dict|None, channel:DiscordChannel, callback:callable): """ Post an embed to Discord """ - webhook_url = self._get_webhook(channel) - if not self._is_webhook_valid(webhook_url): return + # Start with latest webhooks from manager. Will contain True / False for each channel. Copy dict so we don't affect the webhook manager data. + webhooks:dict = deepcopy(self.bgstally.webhook_manager.get_webhooks_as_dict(channel)) + + for webhook in webhooks.values(): + webhook_url:str = webhook.get('url') + if not self._is_webhook_valid(webhook_url): return - data:dict = {'channel': channel, 'callback': callback, 'webhook_url': webhook_url} # Data that's carried through the request queue and back to the callback + # Get the previous state for this webhook's uuid from the passed in data, if it exists. Default to the state from the webhook manager + specific_webhook_data:dict = {} if webhooks_data is None else webhooks_data.get(webhook.get('uuid', ""), webhook) - if previous_messageid == "" or previous_messageid == None: - # No previous post - if fields is None or fields == []: return + data:dict = {'channel': channel, 'callback': callback, 'webhookdata': specific_webhook_data} # Data that's carried through the request queue and back to the callback - embed:dict = self._get_embed(title, description, fields, False) - url:str = webhook_url - payload:dict = {'content': "", 'username': self.bgstally.state.DiscordUsername.get(), 'embeds': [embed]} + # Fetch the previous post ID, if present, from the webhook data for the channel we're posting in. May be the default True / False value + previous_messageid:str = specific_webhook_data.get(channel, None) - self.bgstally.request_manager.queue_request(url, RequestMethod.POST, payload=payload, params={'wait': 'true'}, callback=self._request_complete, data=data) - else: - # Previous post - if fields is not None and fields != []: - embed:dict = self._get_embed(title, description, fields, True) - url:str = f"{webhook_url}/messages/{previous_messageid}" + if previous_messageid == "" or previous_messageid == None or previous_messageid == True or previous_messageid == False: + # No previous post + if fields is None or fields == []: return + + embed:dict = self._get_embed(title, description, fields, False) + url:str = webhook_url payload:dict = {'content': "", 'username': self.bgstally.state.DiscordUsername.get(), 'embeds': [embed]} - self.bgstally.request_manager.queue_request(url, RequestMethod.PATCH, payload=payload, callback=self._request_complete, data=data) + self.bgstally.request_manager.queue_request(url, RequestMethod.POST, payload=payload, params={'wait': 'true'}, callback=self._request_complete, data=data) else: - url = f"{webhook_url}/messages/{previous_messageid}" + # Previous post + if fields is not None and fields != []: + embed:dict = self._get_embed(title, description, fields, True) + url:str = f"{webhook_url}/messages/{previous_messageid}" + payload:dict = {'content': "", 'username': self.bgstally.state.DiscordUsername.get(), 'embeds': [embed]} + + self.bgstally.request_manager.queue_request(url, RequestMethod.PATCH, payload=payload, callback=self._request_complete, data=data) + else: + url = f"{webhook_url}/messages/{previous_messageid}" - self.bgstally.request_manager.queue_request(url, RequestMethod.DELETE, callback=self._request_complete, data=data) + self.bgstally.request_manager.queue_request(url, RequestMethod.DELETE, callback=self._request_complete, data=data) def _request_complete(self, success:bool, response:Response, request:BGSTallyRequest): @@ -102,10 +123,10 @@ def _request_complete(self, success:bool, response:Response, request:BGSTallyReq callback:callable = request.data.get('callback') if callback: if request.method == RequestMethod.DELETE: - callback(request.data.get('channel'), "") + callback(request.data.get('channel'), request.data.get('webhookdata'), "") else: response_json:dict = response.json() - callback(request.data.get('channel'), response_json.get('id', "")) + callback(request.data.get('channel'), request.data.get('webhookdata'), response_json.get('id', "")) def _get_embed(self, title:str, description:str, fields:list, update:bool) -> dict: diff --git a/bgstally/fleetcarrier.py b/bgstally/fleetcarrier.py index e9d368d..f369508 100644 --- a/bgstally/fleetcarrier.py +++ b/bgstally/fleetcarrier.py @@ -65,9 +65,9 @@ def update(self, data: dict): self.name = bytes.fromhex(self.data.get('name', {}).get('vanityName', "----")).decode('utf-8') self.callsign = self.data.get('name', {}).get('callsign', "----") - # Sort sell orders - a Dict of Dicts - materials: dict = self.data.get('orders', {}).get('onfootmicroresources', {}).get('sales') - if materials is not None and materials != {}: + # Sort sell orders - a Dict of Dicts, or an empty list + materials:dict|list = self.data.get('orders', {}).get('onfootmicroresources', {}).get('sales') + if materials is not None and type(materials) is dict and materials != {}: self.onfoot_mats_selling = sorted(materials.values(), key=lambda x: x['locName']) else: self.onfoot_mats_selling = [] diff --git a/bgstally/state.py b/bgstally/state.py index 1a0b38b..e95b02a 100644 --- a/bgstally/state.py +++ b/bgstally/state.py @@ -23,11 +23,6 @@ def load(self): self.ShowZeroActivitySystems:tk.StringVar = tk.StringVar(value=config.get_str('XShowZeroActivity', default=CheckStates.STATE_ON)) self.AbbreviateFactionNames:tk.StringVar = tk.StringVar(value=config.get_str('XAbbreviate', default=CheckStates.STATE_OFF)) self.IncludeSecondaryInf:tk.StringVar = tk.StringVar(value=config.get_str('XSecondaryInf', default=CheckStates.STATE_ON)) - self.DiscordBGSWebhook:tk.StringVar = tk.StringVar(value=config.get_str('XDiscordWebhook', default="")) - self.DiscordCMDRInformationWebhook:tk.StringVar = tk.StringVar(value=config.get_str("BGST_DiscordCMDRInformationWebhook", default="")) - self.DiscordFCMaterialsWebhook:tk.StringVar = tk.StringVar(value=config.get_str("BGST_DiscordFCMaterialsWebhook", default="")) - self.DiscordFCOperationsWebhook:tk.StringVar = tk.StringVar(value=config.get_str("BGST_DiscordFCOperationsWebhook", default="")) - self.DiscordTWWebhook:tk.StringVar = tk.StringVar(value=config.get_str("XDiscordTWWebhook", default="")) self.DiscordUsername:tk.StringVar = tk.StringVar(value=config.get_str('XDiscordUsername', default="")) self.DiscordPostStyle:tk.StringVar = tk.StringVar(value=config.get_str('XDiscordPostStyle', default=DiscordPostStyle.EMBED)) self.DiscordActivity:tk.StringVar = tk.StringVar(value=config.get_str('XDiscordActivity', default=DiscordActivity.BOTH)) @@ -38,6 +33,13 @@ def load(self): self.EnableOverlaySystem:tk.StringVar = tk.StringVar(value=config.get_str('BGST_EnableOverlaySystem', default=CheckStates.STATE_ON)) self.EnableSystemActivityByDefault:tk.StringVar = tk.StringVar(value=config.get_str('BGST_EnableSystemActivityByDefault', default=CheckStates.STATE_ON)) + # TODO: Legacy values, remove in future version + self.DiscordBGSWebhook:tk.StringVar = tk.StringVar(value=config.get_str('XDiscordWebhook', default="")) + self.DiscordCMDRInformationWebhook:tk.StringVar = tk.StringVar(value=config.get_str("BGST_DiscordCMDRInformationWebhook", default="")) + self.DiscordFCMaterialsWebhook:tk.StringVar = tk.StringVar(value=config.get_str("BGST_DiscordFCMaterialsWebhook", default="")) + self.DiscordFCOperationsWebhook:tk.StringVar = tk.StringVar(value=config.get_str("BGST_DiscordFCOperationsWebhook", default="")) + self.DiscordTWWebhook:tk.StringVar = tk.StringVar(value=config.get_str("XDiscordTWWebhook", default="")) + # Persistent values self.current_system_id:str = config.get_str('XCurrentSystemID', default="") self.station_faction:str = config.get_str('XStationFaction', default = "") @@ -74,11 +76,6 @@ def save(self): config.set('XShowZeroActivity', self.ShowZeroActivitySystems.get()) config.set('XAbbreviate', self.AbbreviateFactionNames.get()) config.set('XSecondaryInf', self.IncludeSecondaryInf.get()) - config.set('XDiscordWebhook', self.DiscordBGSWebhook.get()) - config.set('BGST_DiscordCMDRInformationWebhook', self.DiscordCMDRInformationWebhook.get()) - config.set('BGST_DiscordFCMaterialsWebhook', self.DiscordFCMaterialsWebhook.get()) - config.set('BGST_DiscordFCOperationsWebhook', self.DiscordFCOperationsWebhook.get()) - config.set('XDiscordTWWebhook', self.DiscordTWWebhook.get()) config.set('XDiscordUsername', self.DiscordUsername.get()) config.set('XDiscordPostStyle', self.DiscordPostStyle.get()) config.set('XDiscordActivity', self.DiscordActivity.get()) diff --git a/bgstally/targetlog.py b/bgstally/targetlog.py index dec8157..99871c9 100644 --- a/bgstally/targetlog.py +++ b/bgstally/targetlog.py @@ -7,7 +7,7 @@ from requests import Response -from bgstally.constants import DATETIME_FORMAT_JOURNAL, FOLDER_DATA, RequestMethod +from bgstally.constants import CmdrInteractionReason, DATETIME_FORMAT_JOURNAL, FOLDER_DATA, RequestMethod from bgstally.debug import Debug from bgstally.requestmanager import BGSTallyRequest @@ -57,6 +57,11 @@ def get_targetlog(self): """ Get the current target log """ + index:int = 0 + for target in self.targetlog: + target['index'] = index + index += 1 + return self.targetlog @@ -97,6 +102,7 @@ def ship_targeted(self, journal_entry: dict, system: str): 'SquadronID': journal_entry.get('SquadronID', "----"), 'Ship': ship_type, 'LegalStatus': journal_entry.get('LegalStatus', '----'), + 'Reason': CmdrInteractionReason.SCANNED, 'Notes': "Scanned", 'Timestamp': journal_entry['timestamp']} @@ -116,7 +122,8 @@ def friend_request(self, journal_entry: dict, system: str): 'SquadronID': "----", 'Ship': "----", 'LegalStatus': "----", - 'Notes': "Received friend request", + 'Reason': CmdrInteractionReason.FRIEND_REQUEST_RECEIVED, + 'Notes': "Received friend request from", 'Timestamp': journal_entry['timestamp']} cmdr_data, different, pending = self._fetch_cmdr_info(cmdr_name, cmdr_data) @@ -137,6 +144,7 @@ def interdicted(self, journal_entry: dict, system: str): 'SquadronID': "----", 'Ship': "----", 'LegalStatus': "----", + 'Reason': CmdrInteractionReason.INTERDICTED_BY, 'Notes': "Interdicted by", 'Timestamp': journal_entry['timestamp']} @@ -164,6 +172,7 @@ def died(self, journal_entry: dict, system: str): 'SquadronID': "----", 'Ship': killer.get('Ship', "----"), 'LegalStatus': "----", + 'Reason': CmdrInteractionReason.KILLED_BY, 'Notes': "Killed by", 'Timestamp': journal_entry['timestamp']} @@ -186,6 +195,7 @@ def received_text(self, journal_entry: dict, system: str): 'SquadronID': "----", 'Ship': "----", 'LegalStatus': "----", + 'Reason': CmdrInteractionReason.MESSAGE_RECEIVED, 'Notes': "Received message from", 'Timestamp': journal_entry['timestamp']} @@ -205,6 +215,7 @@ def team_invite(self, journal_entry: dict, system: str): 'SquadronID': "----", 'Ship': "----", 'LegalStatus': "----", + 'Reason': CmdrInteractionReason.TEAM_INVITE_RECEIVED, 'Notes': "Received team invite from", 'Timestamp': journal_entry['timestamp']} @@ -220,21 +231,23 @@ def _fetch_cmdr_info(self, cmdr_name:str, cmdr_data:dict): # We have cached data. Check whether it's different enough to make a new log entry for this CMDR. # Different enough: Any of System, SquadronID, Ship and LegalStatus don't match (if blank in the new data, ignore). cmdr_cache_data:dict = self.cmdr_cache[cmdr_name] - if cmdr_data['System'] == cmdr_cache_data['System'] \ - and (cmdr_data['SquadronID'] == cmdr_cache_data['SquadronID'] or cmdr_data['SquadronID'] == "----") \ - and (cmdr_data['Ship'] == cmdr_cache_data['Ship'] or cmdr_data['Ship'] == "----") \ - and (cmdr_data['LegalStatus'] == cmdr_cache_data['LegalStatus'] or cmdr_data['LegalStatus'] == "----"): + if cmdr_data.get('System') == cmdr_cache_data.get('System') \ + and (cmdr_data.get('SquadronID') == cmdr_cache_data.get('SquadronID') or cmdr_cache_data.get('SquadronID') == "----") \ + and (cmdr_data.get('Ship') == cmdr_cache_data.get('Ship') or cmdr_data.get('Ship') == "----") \ + and (cmdr_data.get('LegalStatus') == cmdr_cache_data.get('LegalStatus') or cmdr_cache_data.get('LegalStatus') == "----") \ + and (cmdr_data.get('Reason') == cmdr_cache_data.get('Reason')): return cmdr_cache_data, False, False # It's different, make a copy and update the fields that may have changed in the latest data. This ensures we avoid # expensive multiple calls to the Inara API, but keep a record of every sighting of the same CMDR. We assume Inara info # (squadron name, ranks, URLs) stay the same during a play session. cmdr_data_copy:dict = copy(self.cmdr_cache[cmdr_name]) - cmdr_data_copy['System'] = cmdr_data['System'] - if cmdr_data['Ship'] != "----": cmdr_data_copy['Ship'] = cmdr_data['Ship'] - if cmdr_data['LegalStatus'] != "----": cmdr_data_copy['LegalStatus'] = cmdr_data['LegalStatus'] - cmdr_data_copy['Notes'] = cmdr_data['Notes'] - cmdr_data_copy['Timestamp'] = cmdr_data['Timestamp'] + cmdr_data_copy['System'] = cmdr_data.get('System') + if cmdr_data.get('Ship') != "----": cmdr_data_copy['Ship'] = cmdr_data.get('Ship') + if cmdr_data.get('LegalStatus') != "----": cmdr_data_copy['LegalStatus'] = cmdr_data.get('LegalStatus') + cmdr_data_copy['Reason'] = cmdr_data.get('Reason', CmdrInteractionReason.SCANNED) + cmdr_data_copy['Notes'] = cmdr_data.get('Notes') + cmdr_data_copy['Timestamp'] = cmdr_data.get('Timestamp') # Re-cache the data with the latest updates self.cmdr_cache[cmdr_name] = cmdr_data_copy return cmdr_data_copy, True, False diff --git a/bgstally/ui.py b/bgstally/ui.py index 4ced1ac..4840ecb 100644 --- a/bgstally/ui.py +++ b/bgstally/ui.py @@ -12,7 +12,7 @@ from ttkHyperlinkLabel import HyperlinkLabel from bgstally.activity import Activity -from bgstally.constants import FOLDER_ASSETS, FONT_HEADING, CheckStates, DiscordActivity, DiscordPostStyle, UpdateUIPolicy +from bgstally.constants import FOLDER_ASSETS, FONT_HEADING_2, FONT_SMALL, CheckStates, DiscordActivity, DiscordPostStyle, UpdateUIPolicy from bgstally.debug import Debug from bgstally.widgets import EntryPlus from bgstally.windows.activity import WindowActivity @@ -21,6 +21,7 @@ from bgstally.windows.fleetcarrier import WindowFleetCarrier from bgstally.windows.legend import WindowLegend from config import config +from thirdparty.tksheet import Sheet DATETIME_FORMAT_OVERLAY = "%Y-%m-%d %H:%M" SIZE_BUTTON_PIXELS = 30 @@ -125,16 +126,16 @@ def get_prefs_frame(self, parent_frame: tk.Frame): frame.columnconfigure(1, weight=1) current_row = 1 - nb.Label(frame, text=f"BGS Tally (modified by Aussi) v{str(self.bgstally.version)}", font=FONT_HEADING).grid(row=current_row, column=0, padx=10, sticky=tk.W) + nb.Label(frame, text=f"BGS Tally (Aussi) v{str(self.bgstally.version)}", font=FONT_HEADING_2).grid(row=current_row, column=0, padx=10, sticky=tk.W) HyperlinkLabel(frame, text="Instructions for Use", background=nb.Label().cget('background'), url=URL_WIKI, underline=True).grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 ttk.Separator(frame, orient=tk.HORIZONTAL).grid(row=current_row, columnspan=2, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="General", font=FONT_HEADING).grid(row=current_row, column=0, padx=10, sticky=tk.NW) + nb.Label(frame, text="General", font=FONT_HEADING_2).grid(row=current_row, column=0, padx=10, sticky=tk.NW) nb.Checkbutton(frame, text="BGS Tally Active", variable=self.bgstally.state.Status, onvalue="Active", offvalue="Paused").grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 nb.Checkbutton(frame, text="Show Systems with Zero Activity", variable=self.bgstally.state.ShowZeroActivitySystems, onvalue=CheckStates.STATE_ON, offvalue=CheckStates.STATE_OFF).grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 ttk.Separator(frame, orient=tk.HORIZONTAL).grid(row=current_row, columnspan=2, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="Discord", font=FONT_HEADING).grid(row=current_row, column=0, padx=10, sticky=tk.NW) # Don't increment row because we want the 1st radio option to be opposite title + nb.Label(frame, text="Discord", font=FONT_HEADING_2).grid(row=current_row, column=0, padx=10, sticky=tk.NW) # Don't increment row because we want the 1st radio option to be opposite title nb.Label(frame, text="Activity to Include").grid(row=current_row + 1, column=0, padx=10, sticky=tk.W) nb.Radiobutton(frame, text="BGS", variable=self.bgstally.state.DiscordActivity, value=DiscordActivity.BGS).grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 nb.Radiobutton(frame, text="Thargoid War", variable=self.bgstally.state.DiscordActivity, value=DiscordActivity.THARGOIDWAR).grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 @@ -146,21 +147,29 @@ def get_prefs_frame(self, parent_frame: tk.Frame): nb.Checkbutton(frame, text="Abbreviate Faction Names", variable=self.bgstally.state.AbbreviateFactionNames, onvalue=CheckStates.STATE_ON, offvalue=CheckStates.STATE_OFF).grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 nb.Checkbutton(frame, text="Include Secondary INF", variable=self.bgstally.state.IncludeSecondaryInf, onvalue=CheckStates.STATE_ON, offvalue=CheckStates.STATE_OFF).grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 nb.Checkbutton(frame, text="Report Newly Visited System Activity By Default", variable=self.bgstally.state.EnableSystemActivityByDefault, onvalue=CheckStates.STATE_ON, offvalue=CheckStates.STATE_OFF).grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 - nb.Label(frame, text="BGS Webhook URL").grid(row=current_row, column=0, padx=10, sticky=tk.W) - EntryPlus(frame, textvariable=self.bgstally.state.DiscordBGSWebhook).grid(row=current_row, column=1, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="Thargoid War Webhook URL").grid(row=current_row, column=0, padx=10, sticky=tk.W) - EntryPlus(frame, textvariable=self.bgstally.state.DiscordTWWebhook).grid(row=current_row, column=1, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="Fleet Carrier Materials Webhook URL").grid(row=current_row, column=0, padx=10, sticky=tk.W) - EntryPlus(frame, textvariable=self.bgstally.state.DiscordFCMaterialsWebhook).grid(row=current_row, column=1, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="Fleet Carrier Operations Webhook URL").grid(row=current_row, column=0, padx=10, sticky=tk.W) - EntryPlus(frame, textvariable=self.bgstally.state.DiscordFCOperationsWebhook).grid(row=current_row, column=1, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="CMDR Information Webhook URL").grid(row=current_row, column=0, padx=10, sticky=tk.W) - EntryPlus(frame, textvariable=self.bgstally.state.DiscordCMDRInformationWebhook).grid(row=current_row, column=1, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="Post as User").grid(row=current_row, column=0, padx=10, sticky=tk.W) + + ttk.Separator(frame, orient=tk.HORIZONTAL).grid(row=current_row, columnspan=2, padx=10, pady=1, sticky=tk.EW); current_row += 1 + nb.Label(frame, text="Discord Webhooks", font=FONT_HEADING_2).grid(row=current_row, column=0, padx=10, sticky=tk.NW); current_row += 1 + nb.Label(frame, text="Post to Discord as").grid(row=current_row, column=0, padx=10, sticky=tk.W) EntryPlus(frame, textvariable=self.bgstally.state.DiscordUsername).grid(row=current_row, column=1, padx=10, pady=1, sticky=tk.W); current_row += 1 + self.sheet_webhooks:Sheet = Sheet(frame, show_row_index=True, row_index_width=10, enable_edit_cell_auto_resize=False, height=140, width=880, + column_width=55, header_align="left", empty_vertical=15, empty_horizontal=0, font=FONT_SMALL, + show_horizontal_grid=False, show_vertical_grid=False, show_top_left=False, edit_cell_validation=False, + headers=["UUID", "Nickname", "Webhook URL", "BGS", "TW", "FC Mats", "FC Ops", "CMDR"]) + self.sheet_webhooks.grid(row=current_row, columnspan=2, padx=5, pady=5, sticky=tk.NSEW); current_row += 1 + self.sheet_webhooks.hide_columns(columns=[0]) # Visible column indexes + self.sheet_webhooks.checkbox_column(c=[3, 4, 5, 6, 7]) # Data column indexes + self.sheet_webhooks.set_sheet_data(data=self.bgstally.webhook_manager.get_webhooks_as_list()) + self.sheet_webhooks.column_width(column=0, width=150, redraw=False) # Visible column indexes + self.sheet_webhooks.column_width(column=1, width=400, redraw=True) # Visible column indexes + self.sheet_webhooks.enable_bindings(('single_select', 'row_select', 'arrowkeys', 'right_click_popup_menu', 'rc_select', 'rc_insert_row', + 'rc_delete_row', 'copy', 'cut', 'paste', 'delete', 'undo', 'edit_cell', 'modified')) + self.sheet_webhooks.extra_bindings('all_modified_events', func=self._webhooks_table_modified) + nb.Label(frame, text="To add a webhook: Right-click on a row number and select 'Insert rows above / below'.", font=FONT_SMALL).grid(row=current_row, columnspan=2, padx=10, sticky=tk.NW); current_row += 1 + nb.Label(frame, text="To delete a webhook: Right-click on a row number and select 'Delete rows'.", font=FONT_SMALL).grid(row=current_row, columnspan=2, padx=10, sticky=tk.NW); current_row += 1 ttk.Separator(frame, orient=tk.HORIZONTAL).grid(row=current_row, columnspan=2, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="In-game Overlay", font=FONT_HEADING).grid(row=current_row, column=0, padx=10, sticky=tk.NW) + nb.Label(frame, text="In-game Overlay", font=FONT_HEADING_2).grid(row=current_row, column=0, padx=10, sticky=tk.NW) nb.Checkbutton(frame, text="Show In-game Overlay", variable=self.bgstally.state.EnableOverlay, state=self._overlay_options_state(), @@ -204,11 +213,11 @@ def get_prefs_frame(self, parent_frame: tk.Frame): nb.Label(frame, text="In-game overlay support requires the separate EDMCOverlay plugin to be installed - see the instructions for more information.").grid(columnspan=2, padx=10, sticky=tk.W); current_row += 1 ttk.Separator(frame, orient=tk.HORIZONTAL).grid(row=current_row, columnspan=2, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="Integrations", font=FONT_HEADING).grid(row=current_row, column=0, padx=10, sticky=tk.NW) + nb.Label(frame, text="Integrations", font=FONT_HEADING_2).grid(row=current_row, column=0, padx=10, sticky=tk.NW) tk.Button(frame, text="Configure Remote Server", command=partial(self._show_api_window, parent_frame)).grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 ttk.Separator(frame, orient=tk.HORIZONTAL).grid(row=current_row, columnspan=2, padx=10, pady=1, sticky=tk.EW); current_row += 1 - nb.Label(frame, text="Advanced", font=FONT_HEADING).grid(row=current_row, column=0, padx=10, sticky=tk.NW) + nb.Label(frame, text="Advanced", font=FONT_HEADING_2).grid(row=current_row, column=0, padx=10, sticky=tk.NW) tk.Button(frame, text="FORCE Tick", command=self._confirm_force_tick, bg="red", fg="white").grid(row=current_row, column=1, padx=10, sticky=tk.W); current_row += 1 return frame @@ -222,6 +231,16 @@ def show_system_report(self, system_address:int): self.report_system_address = str(system_address) + def _webhooks_table_modified(self, event=None): + """ + Callback for all modifications to the webhooks table + + Args: + event (namedtuple, optional): Variables related to the callback. Defaults to None. + """ + self.bgstally.webhook_manager.set_webhooks_from_list(self.sheet_webhooks.get_sheet_data()) + + def _worker(self) -> None: """ Handle thread work for overlay diff --git a/bgstally/webhookmanager.py b/bgstally/webhookmanager.py new file mode 100644 index 0000000..e39b17e --- /dev/null +++ b/bgstally/webhookmanager.py @@ -0,0 +1,156 @@ +import json +from os import path, remove +from secrets import token_hex + +from bgstally.constants import DiscordChannel, FOLDER_DATA +from bgstally.debug import Debug +from thirdparty.colors import * + +FILENAME = "webhooks.json" + + +class WebhookManager: + """ + Handle the user's Discord webhooks + """ + def __init__(self, bgstally): + self.bgstally = bgstally + self.data:dict = {} + + self.load() + + + def load(self): + """ + Load state from file + """ + file = path.join(self.bgstally.plugin_dir, FOLDER_DATA, FILENAME) + if path.exists(file): + try: + with open(file) as json_file: + self._from_dict(json.load(json_file)) + except Exception as e: + Debug.logger.info(f"Unable to load {file}") + + if self.data == {}: + # We are in default state, initialise from legacy data + self.data = { + 'webhooks': + [ + {'uuid': token_hex(9), 'name': "BGS", 'url': self.bgstally.state.DiscordBGSWebhook.get(), + DiscordChannel.BGS: True, DiscordChannel.THARGOIDWAR: False, DiscordChannel.FLEETCARRIER_MATERIALS: False, + DiscordChannel.FLEETCARRIER_OPERATIONS: False, DiscordChannel.CMDR_INFORMATION: False}, + {'uuid': token_hex(9), 'name': "TW", 'url': self.bgstally.state.DiscordTWWebhook.get(), + DiscordChannel.BGS: False, DiscordChannel.THARGOIDWAR: True, DiscordChannel.FLEETCARRIER_MATERIALS: False, + DiscordChannel.FLEETCARRIER_OPERATIONS: False, DiscordChannel.CMDR_INFORMATION: False}, + {'uuid': token_hex(9), 'name': "FC Materials", 'url': self.bgstally.state.DiscordFCMaterialsWebhook.get(), + DiscordChannel.BGS: False, DiscordChannel.THARGOIDWAR: False, DiscordChannel.FLEETCARRIER_MATERIALS: True, + DiscordChannel.FLEETCARRIER_OPERATIONS: False, DiscordChannel.CMDR_INFORMATION: False}, + {'uuid': token_hex(9), 'name': "FC Ops", 'url': self.bgstally.state.DiscordFCOperationsWebhook.get(), + DiscordChannel.BGS: False, DiscordChannel.THARGOIDWAR: False, DiscordChannel.FLEETCARRIER_MATERIALS: False, + DiscordChannel.FLEETCARRIER_OPERATIONS: True, DiscordChannel.CMDR_INFORMATION: False}, + {'uuid': token_hex(9), 'name': "CMDR Info", 'url': self.bgstally.state.DiscordCMDRInformationWebhook.get(), + DiscordChannel.BGS: False, DiscordChannel.THARGOIDWAR: False, DiscordChannel.FLEETCARRIER_MATERIALS: False, + DiscordChannel.FLEETCARRIER_OPERATIONS: False, DiscordChannel.CMDR_INFORMATION: True} + ] + } + + + def save(self): + """ + Save state to file + """ + file = path.join(self.bgstally.plugin_dir, FOLDER_DATA, FILENAME) + with open(file, 'w') as outfile: + json.dump(self._as_dict(), outfile) + + + def set_webhooks_from_list(self, data: list): + """ + Store webhooks data from a list of lists + + Args: + data (list): A list containing the webhooks, each webhook being a list + """ + self.data['webhooks'] = [] + + if data is None or data == []: + self.save() + return + + for webhook in data: + if len(webhook) == 8: + self.data['webhooks'].append({ + 'uuid': webhook[0] if webhook[0] is not None and webhook[0] != "" else token_hex(9), + 'name': webhook[1], + 'url': webhook[2], + DiscordChannel.BGS: webhook[3], + DiscordChannel.THARGOIDWAR: webhook[4], + DiscordChannel.FLEETCARRIER_MATERIALS: webhook[5], + DiscordChannel.FLEETCARRIER_OPERATIONS: webhook[6], + DiscordChannel.CMDR_INFORMATION: webhook[7] + }) + + self.save() + + + def get_webhooks_as_dict(self, channel:DiscordChannel|None = None) -> list: + """ + Get the webhooks as a dict + + Args: + channel (DiscordChannel | None, optional): If None or omitted, return all webhooks. If specified, only return webhooks for the given channel. Defaults to None. + + Returns: + dict: A dict containing the relevant webhooks, with the key being the uuid and the value being the webhook as a dict + """ + result:dict = {} + + for webhook in self.data.get('webhooks', []): + if channel is None or webhook.get(channel) == True: + uuid:str = webhook.get('uuid', token_hex(9)) + result[uuid] = webhook + + return result + + + def get_webhooks_as_list(self, channel:DiscordChannel|None = None) -> list: + """ + Get the webhooks as a list of lists + + Args: + channel (DiscordChannel | None, optional): If None or omitted, return all webhooks. If specified, only return webhooks for the given channel. Defaults to None. + + Returns: + list: A list containing the relevant webhooks, each webhook being a list + """ + result:list = [] + + for webhook in self.data.get('webhooks', []): + if channel is None or webhook.get(channel) == True: + result.append([ + webhook.get('uuid', token_hex(9)), + webhook.get('name', ""), + webhook.get('url', ""), + webhook.get(DiscordChannel.BGS, False), + webhook.get(DiscordChannel.THARGOIDWAR, False), + webhook.get(DiscordChannel.FLEETCARRIER_MATERIALS, False), + webhook.get(DiscordChannel.FLEETCARRIER_OPERATIONS, False), + webhook.get(DiscordChannel.CMDR_INFORMATION, False) + ]) + + return result + + + def _as_dict(self) -> dict: + """ + Return a Dictionary representation of our data, suitable for serializing + """ + return self.data + + + def _from_dict(self, dict: dict): + """ + Populate our data from a Dictionary that has been deserialized + """ + self.data = dict diff --git a/bgstally/widgets.py b/bgstally/widgets.py index d4a9686..f08d9e1 100644 --- a/bgstally/widgets.py +++ b/bgstally/widgets.py @@ -334,7 +334,9 @@ def _select_item(self, event): clicked_column = int(clicked_column_ref[1:]) - 1 if clicked_column < 0: return - self.callback(clicked_item['values'], clicked_column, self) + iid:str = self.identify('item', event.x, event.y) + + self.callback(clicked_item['values'], clicked_column, self, iid) def _sort(self, column, reverse, data_type, callback): l = [(self.set(k, column), k) for k in self.get_children('')] diff --git a/bgstally/windows/activity.py b/bgstally/windows/activity.py index b36e987..d2dee7e 100644 --- a/bgstally/windows/activity.py +++ b/bgstally/windows/activity.py @@ -4,8 +4,10 @@ from tkinter import PhotoImage, ttk from typing import Dict +from ttkHyperlinkLabel import HyperlinkLabel + from bgstally.activity import STATES_WAR, Activity -from bgstally.constants import FOLDER_ASSETS, FONT_HEADING, FONT_TEXT, CheckStates, CZs, DiscordActivity, DiscordChannel, DiscordPostStyle +from bgstally.constants import COLOUR_HEADING_1, FOLDER_ASSETS, FONT_HEADING_1, FONT_HEADING_2, FONT_TEXT, CheckStates, CZs, DiscordActivity, DiscordChannel, DiscordPostStyle from bgstally.debug import Debug from bgstally.utils import human_format from bgstally.widgets import DiscordAnsiColorText, TextPlus @@ -58,11 +60,11 @@ def _show(self, activity: Activity): DiscordFrame.pack(fill=tk.X, side=tk.BOTTOM, padx=5, pady=5) DiscordFrame.columnconfigure(0, weight=2) DiscordFrame.columnconfigure(1, weight=1) - label_discord_report:ttk.Label = ttk.Label(DiscordFrame, text="โ“ Discord Report Preview", font=FONT_HEADING, cursor="hand2") + label_discord_report:ttk.Label = ttk.Label(DiscordFrame, text="โ“ Discord Report Preview", font=FONT_HEADING_2, cursor="hand2") label_discord_report.grid(row=0, column=0, sticky=tk.W) label_discord_report.bind("", self._show_legend_window) - ttk.Label(DiscordFrame, text="Discord Additional Notes", font=FONT_HEADING).grid(row=0, column=1, sticky=tk.W) - ttk.Label(DiscordFrame, text="Discord Options", font=FONT_HEADING).grid(row=0, column=2, sticky=tk.W) + ttk.Label(DiscordFrame, text="Discord Additional Notes", font=FONT_HEADING_2).grid(row=0, column=1, sticky=tk.W) + ttk.Label(DiscordFrame, text="Discord Options", font=FONT_HEADING_2).grid(row=0, column=2, sticky=tk.W) ttk.Label(DiscordFrame, text="Double-check on-ground CZ tallies, sizes are not always correct", foreground='#f00').grid(row=1, column=0, columnspan=3, sticky=tk.W) DiscordTextFrame = ttk.Frame(DiscordFrame) @@ -113,58 +115,67 @@ def _show(self, activity: Activity): and system['zero_system_activity'] \ and str(system_id) != self.bgstally.state.current_system_id: continue - tab = ttk.Frame(TabParent) - tab.columnconfigure(1, weight=1) # Make the second column (faction name) fill available space + tab:ttk.Frame = ttk.Frame(TabParent) TabParent.add(tab, text=system['System'], compound='right', image=self.image_tab_active_enabled) + frame_header:ttk.Frame = ttk.Frame(tab) + frame_header.pack(fill=tk.X, side=tk.TOP, padx=5, pady=5) + ttk.Label(frame_header, text=system['System'], font=FONT_HEADING_1, foreground=COLOUR_HEADING_1).grid(row=0, column=0, padx=2, pady=2) + + HyperlinkLabel(frame_header, text="Inara โคด", url=f"https://inara.cz/elite/starsystem/?search={system['System']}", underline=True).grid(row=0, column=1, padx=2, pady=2) + + frame_table:ttk.Frame = ttk.Frame(tab) + frame_table.pack(fill=tk.BOTH, side=tk.TOP, padx=5, pady=5, expand=tk.YES) + frame_table.columnconfigure(1, weight=1) # Make the second column (faction name) fill available space + FactionEnableCheckbuttons = [] - ttk.Label(tab, text="Include", font=FONT_HEADING).grid(row=0, column=0, padx=2, pady=2) - EnableAllCheckbutton = ttk.Checkbutton(tab) + ttk.Label(frame_table, text="Include", font=FONT_HEADING_2).grid(row=0, column=0, padx=2, pady=2) + EnableAllCheckbutton = ttk.Checkbutton(frame_table) EnableAllCheckbutton.grid(row=1, column=0, padx=2, pady=2) EnableAllCheckbutton.configure(command=partial(self._enable_all_factions_change, TabParent, tab_index, EnableAllCheckbutton, FactionEnableCheckbuttons, DiscordText, activity, system)) EnableAllCheckbutton.state(['!alternate']) col: int = 1 - ttk.Label(tab, text="Faction", font=FONT_HEADING).grid(row=0, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="State", font=FONT_HEADING).grid(row=0, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="INF", font=FONT_HEADING, anchor=tk.CENTER).grid(row=0, column=col, columnspan=2, padx=2) - ttk.Label(tab, text="Pri", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Sec", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Trade", font=FONT_HEADING, anchor=tk.CENTER).grid(row=0, column=col, columnspan=3, padx=2) - ttk.Label(tab, text="Purch", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Prof", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="BM Prof", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="BVs", font=FONT_HEADING).grid(row=0, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Expl", font=FONT_HEADING).grid(row=0, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Exo", font=FONT_HEADING).grid(row=0, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="CBs", font=FONT_HEADING).grid(row=0, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Fails", font=FONT_HEADING).grid(row=0, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Murders", font=FONT_HEADING, anchor=tk.CENTER).grid(row=0, column=col, columnspan=2, padx=2, pady=2) - ttk.Label(tab, text="Foot", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Ship", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Scens", font=FONT_HEADING).grid(row=0, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Space CZs", font=FONT_HEADING, anchor=tk.CENTER).grid(row=0, column=col, columnspan=3, padx=2) - ttk.Label(tab, text="L", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="M", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="H", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="Foot CZs", font=FONT_HEADING, anchor=tk.CENTER).grid(row=0, column=col, columnspan=3, padx=2) - ttk.Label(tab, text="L", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="M", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Label(tab, text="H", font=FONT_HEADING).grid(row=1, column=col, padx=2, pady=2); col += 1 - ttk.Separator(tab, orient=tk.HORIZONTAL).grid(columnspan=col, padx=2, pady=5, sticky=tk.EW) + ttk.Label(frame_table, text="Faction", font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="State", font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="INF", font=FONT_HEADING_2, anchor=tk.CENTER).grid(row=0, column=col, columnspan=2, padx=2) + ttk.Label(frame_table, text="Pri", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Sec", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Trade", font=FONT_HEADING_2, anchor=tk.CENTER).grid(row=0, column=col, columnspan=3, padx=2) + ttk.Label(frame_table, text="Purch", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Prof", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="BM Prof", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="BVs", font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Expl", font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Exo", font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="CBs", font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Fails", font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Murders", font=FONT_HEADING_2, anchor=tk.CENTER).grid(row=0, column=col, columnspan=2, padx=2, pady=2) + ttk.Label(frame_table, text="Foot", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Ship", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Scens", font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Space CZs", font=FONT_HEADING_2, anchor=tk.CENTER).grid(row=0, column=col, columnspan=3, padx=2) + ttk.Label(frame_table, text="L", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="M", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="H", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="Foot CZs", font=FONT_HEADING_2, anchor=tk.CENTER).grid(row=0, column=col, columnspan=3, padx=2) + ttk.Label(frame_table, text="L", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="M", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Label(frame_table, text="H", font=FONT_HEADING_2).grid(row=1, column=col, padx=2, pady=2); col += 1 + ttk.Separator(frame_table, orient=tk.HORIZONTAL).grid(columnspan=col, padx=2, pady=5, sticky=tk.EW) header_rows = 3 x = 0 for faction in system['Factions'].values(): - EnableCheckbutton = ttk.Checkbutton(tab) + EnableCheckbutton = ttk.Checkbutton(frame_table) EnableCheckbutton.grid(row=x + header_rows, column=0, sticky=tk.N, padx=2, pady=2) EnableCheckbutton.configure(command=partial(self._enable_faction_change, TabParent, tab_index, EnableAllCheckbutton, FactionEnableCheckbuttons, DiscordText, activity, system, faction, x)) EnableCheckbutton.state(['selected', '!alternate'] if faction['Enabled'] == CheckStates.STATE_ON else ['!selected', '!alternate']) FactionEnableCheckbuttons.append(EnableCheckbutton) - FactionNameFrame = ttk.Frame(tab) + FactionNameFrame = ttk.Frame(frame_table) FactionNameFrame.grid(row=x + header_rows, column=1, sticky=tk.NW) FactionName = ttk.Label(FactionNameFrame, text=faction['Faction']) FactionName.grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=2, pady=2) @@ -181,44 +192,44 @@ def _show(self, activity: Activity): settlement_row_index += 1 col = 2 - ttk.Label(tab, text=faction['FactionState']).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=faction['FactionState']).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 MissionPointsVar = tk.IntVar(value=faction['MissionPoints']) - ttk.Spinbox(tab, from_=-999, to=999, width=3, textvariable=MissionPointsVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 + ttk.Spinbox(frame_table, from_=-999, to=999, width=3, textvariable=MissionPointsVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 MissionPointsVar.trace('w', partial(self._mission_points_change, TabParent, tab_index, MissionPointsVar, True, EnableAllCheckbutton, DiscordText, activity, system, faction, x)) MissionPointsSecVar = tk.IntVar(value=faction['MissionPointsSecondary']) - ttk.Spinbox(tab, from_=-999, to=999, width=3, textvariable=MissionPointsSecVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 + ttk.Spinbox(frame_table, from_=-999, to=999, width=3, textvariable=MissionPointsSecVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 MissionPointsSecVar.trace('w', partial(self._mission_points_change, TabParent, tab_index, MissionPointsSecVar, False, EnableAllCheckbutton, DiscordText, activity, system, faction, x)) if faction['TradePurchase'] > 0: - ttk.Label(tab, text=human_format(faction['TradePurchase'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=human_format(faction['TradeProfit'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=human_format(faction['TradePurchase'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=human_format(faction['TradeProfit'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 else: - ttk.Label(tab, text=f"{human_format(faction['TradeBuy'][2]['value'])} | {human_format(faction['TradeBuy'][3]['value'])}").grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=f"{human_format(faction['TradeSell'][0]['profit'])} | {human_format(faction['TradeSell'][2]['profit'])} | {human_format(faction['TradeSell'][3]['profit'])}").grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=human_format(faction['BlackMarketProfit'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=human_format(faction['Bounties'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=human_format(faction['CartData'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=human_format(faction['ExoData'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=human_format(faction['CombatBonds'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=faction['MissionFailed']).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=faction['GroundMurdered']).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 - ttk.Label(tab, text=faction['Murdered']).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=f"{human_format(faction['TradeBuy'][2]['value'])} | {human_format(faction['TradeBuy'][3]['value'])}").grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=f"{human_format(faction['TradeSell'][0]['profit'])} | {human_format(faction['TradeSell'][2]['profit'])} | {human_format(faction['TradeSell'][3]['profit'])}").grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=human_format(faction['BlackMarketProfit'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=human_format(faction['Bounties'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=human_format(faction['CartData'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=human_format(faction['ExoData'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=human_format(faction['CombatBonds'])).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=faction['MissionFailed']).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=faction['GroundMurdered']).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 + ttk.Label(frame_table, text=faction['Murdered']).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 ScenariosVar = tk.IntVar(value=faction['Scenarios']) - ttk.Spinbox(tab, from_=0, to=999, width=3, textvariable=ScenariosVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 + ttk.Spinbox(frame_table, from_=0, to=999, width=3, textvariable=ScenariosVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 ScenariosVar.trace('w', partial(self._scenarios_change, TabParent, tab_index, ScenariosVar, EnableAllCheckbutton, DiscordText, activity, system, faction, x)) if (faction['FactionState'] in STATES_WAR): CZSpaceLVar = tk.StringVar(value=faction['SpaceCZ'].get('l', '0')) - ttk.Spinbox(tab, from_=0, to=999, width=3, textvariable=CZSpaceLVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 + ttk.Spinbox(frame_table, from_=0, to=999, width=3, textvariable=CZSpaceLVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 CZSpaceMVar = tk.StringVar(value=faction['SpaceCZ'].get('m', '0')) - ttk.Spinbox(tab, from_=0, to=999, width=3, textvariable=CZSpaceMVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 + ttk.Spinbox(frame_table, from_=0, to=999, width=3, textvariable=CZSpaceMVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 CZSpaceHVar = tk.StringVar(value=faction['SpaceCZ'].get('h', '0')) - ttk.Spinbox(tab, from_=0, to=999, width=3, textvariable=CZSpaceHVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 + ttk.Spinbox(frame_table, from_=0, to=999, width=3, textvariable=CZSpaceHVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 CZGroundLVar = tk.StringVar(value=faction['GroundCZ'].get('l', '0')) - ttk.Spinbox(tab, from_=0, to=999, width=3, textvariable=CZGroundLVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 + ttk.Spinbox(frame_table, from_=0, to=999, width=3, textvariable=CZGroundLVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 CZGroundMVar = tk.StringVar(value=faction['GroundCZ'].get('m', '0')) - ttk.Spinbox(tab, from_=0, to=999, width=3, textvariable=CZGroundMVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 + ttk.Spinbox(frame_table, from_=0, to=999, width=3, textvariable=CZGroundMVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 CZGroundHVar = tk.StringVar(value=faction['GroundCZ'].get('h', '0')) - ttk.Spinbox(tab, from_=0, to=999, width=3, textvariable=CZGroundHVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 + ttk.Spinbox(frame_table, from_=0, to=999, width=3, textvariable=CZGroundHVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 # Watch for changes on all SpinBox Variables. This approach catches any change, including manual editing, while using 'command' callbacks only catches clicks CZSpaceLVar.trace('w', partial(self._cz_change, TabParent, tab_index, CZSpaceLVar, EnableAllCheckbutton, DiscordText, CZs.SPACE_LOW, activity, system, faction, x)) CZSpaceMVar.trace('w', partial(self._cz_change, TabParent, tab_index, CZSpaceMVar, EnableAllCheckbutton, DiscordText, CZs.SPACE_MED, activity, system, faction, x)) @@ -231,7 +242,6 @@ def _show(self, activity: Activity): self._update_enable_all_factions_checkbutton(TabParent, tab_index, EnableAllCheckbutton, FactionEnableCheckbuttons, system) - tab.pack_forget() tab_index += 1 self._update_discord_field(DiscordText, activity) @@ -279,57 +289,45 @@ def _post_to_discord(self, activity: Activity): """ if self.bgstally.state.DiscordPostStyle.get() == DiscordPostStyle.TEXT: if self.bgstally.state.DiscordActivity.get() == DiscordActivity.BGS: - # BGS Only - one post to BGS channel + # BGS Only - post to BGS channels discord_text:str = activity.generate_text(DiscordActivity.BGS, True) - self.bgstally.discord.post_plaintext(discord_text, activity.discord_bgs_messageid, DiscordChannel.BGS, self.discord_post_complete) + self.bgstally.discord.post_plaintext(discord_text, activity.discord_webhook_data, DiscordChannel.BGS, self.discord_post_complete) elif self.bgstally.state.DiscordActivity.get() == DiscordActivity.THARGOIDWAR: - # TW Only - one post to TW channel + # TW Only - post to TW channels discord_text:str = activity.generate_text(DiscordActivity.THARGOIDWAR, True) - self.bgstally.discord.post_plaintext(discord_text, activity.discord_tw_messageid, DiscordChannel.THARGOIDWAR, self.discord_post_complete) - elif self.bgstally.discord.is_webhook_valid(DiscordChannel.THARGOIDWAR): - # Both, TW channel is available - two posts, one to each channel - discord_text:str = activity.generate_text(DiscordActivity.BGS, True) - self.bgstally.discord.post_plaintext(discord_text, activity.discord_bgs_messageid, DiscordChannel.BGS, self.discord_post_complete) - discord_text:str = activity.generate_text(DiscordActivity.THARGOIDWAR, True) - self.bgstally.discord.post_plaintext(discord_text, activity.discord_tw_messageid, DiscordChannel.THARGOIDWAR, self.discord_post_complete) + self.bgstally.discord.post_plaintext(discord_text, activity.discord_webhook_data, DiscordChannel.THARGOIDWAR, self.discord_post_complete) else: - # Both, TW channel is not available - one combined post to BGS channel + # Both, post to both channels discord_text:str = activity.generate_text(DiscordActivity.BOTH, True) - self.bgstally.discord.post_plaintext(discord_text, activity.discord_bgs_messageid, DiscordChannel.BGS, self.discord_post_complete) + self.bgstally.discord.post_plaintext(discord_text, activity.discord_webhook_data, DiscordChannel.BGS, self.discord_post_complete) else: description = "" if activity.discord_notes is None else activity.discord_notes if self.bgstally.state.DiscordActivity.get() == DiscordActivity.BGS: - # BGS Only - one post to BGS channel + # BGS Only - post to BGS channels discord_fields:Dict = activity.generate_discord_embed_fields(DiscordActivity.BGS) - self.bgstally.discord.post_embed(f"BGS Activity after tick: {activity.get_title()}", description, discord_fields, activity.discord_bgs_messageid, DiscordChannel.BGS, self.discord_post_complete) + self.bgstally.discord.post_embed(f"BGS Activity after tick: {activity.get_title()}", description, discord_fields, activity.discord_webhook_data, DiscordChannel.BGS, self.discord_post_complete) elif self.bgstally.state.DiscordActivity.get() == DiscordActivity.THARGOIDWAR: - # TW Only - one post to TW channel - discord_fields:Dict = activity.generate_discord_embed_fields(DiscordActivity.THARGOIDWAR) - self.bgstally.discord.post_embed(f"TW Activity after tick: {activity.get_title()}", description, discord_fields, activity.discord_tw_messageid, DiscordChannel.THARGOIDWAR, self.discord_post_complete) - elif self.bgstally.discord.is_webhook_valid(DiscordChannel.THARGOIDWAR): - # Both, TW channel is available - two posts, one to each channel - discord_fields:Dict = activity.generate_discord_embed_fields(DiscordActivity.BGS) - self.bgstally.discord.post_embed(f"BGS Activity after tick: {activity.get_title()}", description, discord_fields, activity.discord_bgs_messageid, DiscordChannel.BGS, self.discord_post_complete) + # TW Only - post to TW channels discord_fields:Dict = activity.generate_discord_embed_fields(DiscordActivity.THARGOIDWAR) - self.bgstally.discord.post_embed(f"TW Activity after tick: {activity.get_title()}", description, discord_fields, activity.discord_tw_messageid, DiscordChannel.THARGOIDWAR, self.discord_post_complete) + self.bgstally.discord.post_embed(f"TW Activity after tick: {activity.get_title()}", description, discord_fields, activity.discord_webhook_data, DiscordChannel.THARGOIDWAR, self.discord_post_complete) else: - # Both, TW channel is not available - one combined post to BGS channel + # Both, post to both channels discord_fields:Dict = activity.generate_discord_embed_fields(DiscordActivity.BOTH) - self.bgstally.discord.post_embed(f"Activity after tick: {activity.get_title()}", description, discord_fields, activity.discord_bgs_messageid, DiscordChannel.BGS, self.discord_post_complete) + self.bgstally.discord.post_embed(f"Activity after tick: {activity.get_title()}", description, discord_fields, activity.discord_webhook_data, DiscordChannel.BGS, self.discord_post_complete) activity.dirty = True # Because discord post ID has been changed - def discord_post_complete(self, channel:DiscordChannel, messageid:str): + def discord_post_complete(self, channel:DiscordChannel, webhook_data:dict, messageid:str): """ A discord post request has completed """ - # Store the Message ID - match channel: - case DiscordChannel.BGS: - self.activity.discord_bgs_messageid = messageid - case DiscordChannel.THARGOIDWAR: - self.activity.discord_tw_messageid = messageid + uuid:str = webhook_data.get('uuid') + if uuid is None: return + + activity_webhook_data:dict = self.activity.discord_webhook_data.get(uuid, webhook_data) # Fetch current activity webhook data, default to data from callback. + activity_webhook_data[channel] = messageid # Store the returned messageid against the channel + self.activity.discord_webhook_data[uuid] = activity_webhook_data # Store the webhook dict back to the activity def _discord_notes_change(self, DiscordNotesText, DiscordText, activity: Activity, *args): diff --git a/bgstally/windows/api.py b/bgstally/windows/api.py index e8c4001..d843aa9 100644 --- a/bgstally/windows/api.py +++ b/bgstally/windows/api.py @@ -6,7 +6,7 @@ from os import path from bgstally.api import API -from bgstally.constants import FOLDER_ASSETS, FONT_HEADING +from bgstally.constants import FOLDER_ASSETS, FONT_HEADING_2 from bgstally.debug import Debug from bgstally.widgets import CollapsibleFrame, EntryPlus, HyperlinkManager from requests import Response @@ -67,7 +67,7 @@ def show(self, parent_frame:tk.Frame = None): current_row:int = 0 text_width:int = 400 - tk.Label(frame_main, text="About This", font=FONT_HEADING).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 + tk.Label(frame_main, text="About This", font=FONT_HEADING_2).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 self.txt_intro:tk.Text = tk.Text(frame_main, font=default_font, wrap=tk.WORD, bd=0, highlightthickness=0, borderwidth=0, bg=default_bg, cursor="") self.txt_intro.insert(tk.END, "This screen is used to set up a connection to a server.\n\nTake care when agreeing to this - if " \ "you approve this server, BGS-Tally will send your information to it, which will include CMDR details such as your location, " \ @@ -76,7 +76,7 @@ def show(self, parent_frame:tk.Frame = None): self.txt_intro.tag_config("sel", background=default_bg, foreground=default_fg) # Make the selected text colour the same as the widget background self.txt_intro.grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 - tk.Label(frame_main, text="API Settings", font=FONT_HEADING).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 + tk.Label(frame_main, text="API Settings", font=FONT_HEADING_2).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 self.txt_settings:tk.Text = tk.Text(frame_main, font=default_font, wrap=tk.WORD, bd=0, highlightthickness=0, borderwidth=0, bg=default_bg, cursor="") self.txt_settings.insert(tk.END, "Ask the server administrator for the information below, then click 'Establish Connection' to continue. " \ "Buttons to pre-fill some information for popular servers are provided, but you will need to enter your API key which is unique to you.") @@ -117,7 +117,7 @@ def show(self, parent_frame:tk.Frame = None): self.frame_information.frame.columnconfigure(0, minsize=100) current_row = 0 - tk.Label(self.frame_information.frame, text="API Information", font=FONT_HEADING).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 + tk.Label(self.frame_information.frame, text="API Information", font=FONT_HEADING_2).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 self.txt_information:tk.Text = tk.Text(self.frame_information.frame, font=default_font, wrap=tk.WORD, bd=0, highlightthickness=0, borderwidth=0, bg=default_bg, cursor="") hyperlink = HyperlinkManager(self.txt_information) self.txt_information.insert(tk.END, "The exact set of Events that will be sent is listed in the 'Events Requested' section below. " \ diff --git a/bgstally/windows/cmdrs.py b/bgstally/windows/cmdrs.py index 2a8f53a..9c33680 100644 --- a/bgstally/windows/cmdrs.py +++ b/bgstally/windows/cmdrs.py @@ -3,10 +3,11 @@ from functools import partial from tkinter import ttk -from bgstally.constants import DATETIME_FORMAT_JOURNAL, DiscordChannel, FONT_HEADING +from bgstally.constants import CmdrInteractionReason, DATETIME_FORMAT_JOURNAL, DiscordChannel, COLOUR_HEADING_1, FONT_HEADING_1, FONT_HEADING_2 from bgstally.debug import Debug from bgstally.widgets import TreeviewPlus from ttkHyperlinkLabel import HyperlinkLabel +from thirdparty.colors import * DATETIME_FORMAT_CMDRLIST = "%Y-%m-%d %H:%M:%S" @@ -64,19 +65,22 @@ def show(self): treeview.pack(fill=tk.BOTH, expand=1) current_row = 0 - ttk.Label(details_frame, text="CMDR Details", font=FONT_HEADING).grid(row=current_row, column=0, sticky=tk.W); current_row += 1 - ttk.Label(details_frame, text="Name: ", font=FONT_HEADING).grid(row=current_row, column=0, sticky=tk.W) + ttk.Label(details_frame, text="CMDR Details", font=FONT_HEADING_1, foreground=COLOUR_HEADING_1).grid(row=current_row, column=0, sticky=tk.W); current_row += 1 + ttk.Label(details_frame, text="Name: ", font=FONT_HEADING_2).grid(row=current_row, column=0, sticky=tk.W) self.cmdr_details_name = ttk.Label(details_frame, text="") self.cmdr_details_name.grid(row=current_row, column=1, sticky=tk.W) - ttk.Label(details_frame, text="Inara: ", font=FONT_HEADING).grid(row=current_row, column=2, sticky=tk.W) + ttk.Label(details_frame, text="Inara: ", font=FONT_HEADING_2).grid(row=current_row, column=2, sticky=tk.W) self.cmdr_details_name_inara = HyperlinkLabel(details_frame, text="", url="https://inara.cz/elite/cmdrs/?search=aussi", underline=True) self.cmdr_details_name_inara.grid(row=current_row, column=3, sticky=tk.W); current_row += 1 - ttk.Label(details_frame, text="Squadron: ", font=FONT_HEADING).grid(row=current_row, column=0, sticky=tk.W) + ttk.Label(details_frame, text="Squadron: ", font=FONT_HEADING_2).grid(row=current_row, column=0, sticky=tk.W) self.cmdr_details_squadron = ttk.Label(details_frame, text="") self.cmdr_details_squadron.grid(row=current_row, column=1, sticky=tk.W) - ttk.Label(details_frame, text="Inara: ", font=FONT_HEADING).grid(row=current_row, column=2, sticky=tk.W) + ttk.Label(details_frame, text="Inara: ", font=FONT_HEADING_2).grid(row=current_row, column=2, sticky=tk.W) self.cmdr_details_squadron_inara = HyperlinkLabel(details_frame, text="", url="https://inara.cz/elite/squadrons-search/?search=ghst", underline=True) self.cmdr_details_squadron_inara.grid(row=current_row, column=3, sticky=tk.W); current_row += 1 + ttk.Label(details_frame, text="Interaction: ", font=FONT_HEADING_2).grid(row=current_row, column=0, sticky=tk.W) + self.cmdr_details_interaction = ttk.Label(details_frame, text="") + self.cmdr_details_interaction.grid(row=current_row, column=1, sticky=tk.W); current_row += 1 for column in column_info: treeview.heading(column['title'], text=column['title'].title(), sort_by=column['type']) @@ -90,8 +94,7 @@ def show(self): target.get('LegalStatus', "----"), \ datetime.strptime(target['Timestamp'], DATETIME_FORMAT_JOURNAL).strftime(DATETIME_FORMAT_CMDRLIST), \ target.get('Notes', "Scanned")] - iid:str = treeview.insert("", 'end', values=target_values) - target['iid'] = iid + treeview.insert("", 'end', values=target_values, iid=target.get('index')) if self.bgstally.discord.is_webhook_valid(DiscordChannel.CMDR_INFORMATION) or self.bgstally.discord.is_webhook_valid(DiscordChannel.BGS): self.post_button = tk.Button(buttons_frame, text="Post to Discord", command=partial(self._post_to_discord)) @@ -103,7 +106,7 @@ def show(self): self.delete_button['state'] = tk.DISABLED - def _cmdr_selected(self, values, column, treeview:TreeviewPlus): + def _cmdr_selected(self, values, column, treeview:TreeviewPlus, iid:str): """ A CMDR row has been clicked in the list, show details """ @@ -111,9 +114,11 @@ def _cmdr_selected(self, values, column, treeview:TreeviewPlus): self.cmdr_details_name_inara.configure(text = "", url = "") self.cmdr_details_squadron.config(text = "") self.cmdr_details_squadron_inara.configure(text = "", url = "") + self.cmdr_details_interaction.configure(text = "") + + # Fetch the info for this CMDR. iid is the index into the original (unsorted) CMDR list. + self.selected_cmdr = self.target_data[int(iid)] - # Fetch the latest info for this CMDR - self.selected_cmdr = self.bgstally.target_log.get_target_info(values[0]) if not self.selected_cmdr: self.post_button['state'] = tk.DISABLED self.delete_button['state'] = tk.DISABLED @@ -122,14 +127,15 @@ def _cmdr_selected(self, values, column, treeview:TreeviewPlus): self.post_button['state'] = tk.NORMAL self.delete_button['state'] = tk.NORMAL - if 'TargetName' in self.selected_cmdr: self.cmdr_details_name.config(text = self.selected_cmdr['TargetName']) - if 'inaraURL' in self.selected_cmdr: self.cmdr_details_name_inara.configure(text = "Inara Info Available", url = self.selected_cmdr['inaraURL']) + if 'TargetName' in self.selected_cmdr: self.cmdr_details_name.config(text = self.selected_cmdr.get('TargetName')) + if 'inaraURL' in self.selected_cmdr: self.cmdr_details_name_inara.configure(text = "Inara Info Available โคด", url = self.selected_cmdr.get('inaraURL')) if 'squadron' in self.selected_cmdr: - squadron_info = self.selected_cmdr['squadron'] - if 'squadronName' in squadron_info: self.cmdr_details_squadron.config(text = f"{squadron_info['squadronName']} ({squadron_info['squadronMemberRank']})") - if 'inaraURL' in squadron_info: self.cmdr_details_squadron_inara.configure(text = "Inara Info Available", url = squadron_info['inaraURL']) + squadron_info = self.selected_cmdr.get('squadron') + if 'squadronName' in squadron_info: self.cmdr_details_squadron.config(text = f"{squadron_info.get('squadronName')} ({squadron_info.get('squadronMemberRank')})") + if 'inaraURL' in squadron_info: self.cmdr_details_squadron_inara.configure(text = "Inara Info Available โคด", url = squadron_info.get('inaraURL')) elif 'SquadronID' in self.selected_cmdr: - self.cmdr_details_squadron.config(text = f"{self.selected_cmdr['SquadronID']}") + self.cmdr_details_squadron.config(text = f"{self.selected_cmdr.get('SquadronID')}") + if 'Notes' in self.selected_cmdr: self.cmdr_details_interaction.config(text = self.selected_cmdr.get('Notes')) def _delete_selected(self, treeview:TreeviewPlus): @@ -154,32 +160,32 @@ def _post_to_discord(self): embed_fields = [ { "name": "Name", - "value": self.selected_cmdr['TargetName'], + "value": self.selected_cmdr.get('TargetName'), "inline": True }, { - "name": "Spotted in System", - "value": self.selected_cmdr['System'], + "name": "In System", + "value": self.selected_cmdr.get('System'), "inline": True }, { "name": "In Ship", - "value": self.selected_cmdr['Ship'], + "value": self.selected_cmdr.get('Ship'), "inline": True }, { "name": "In Squadron", - "value": self.selected_cmdr['SquadronID'], + "value": self.selected_cmdr.get('SquadronID'), "inline": True }, { "name": "Legal Status", - "value": self.selected_cmdr['LegalStatus'], + "value": self.selected_cmdr.get('LegalStatus'), "inline": True }, { "name": "Date and Time", - "value": datetime.strptime(self.selected_cmdr['Timestamp'], DATETIME_FORMAT_JOURNAL).strftime(DATETIME_FORMAT_CMDRLIST), + "value": datetime.strptime(self.selected_cmdr.get('Timestamp'), DATETIME_FORMAT_JOURNAL).strftime(DATETIME_FORMAT_CMDRLIST), "inline": True } ] @@ -187,19 +193,38 @@ def _post_to_discord(self): if 'inaraURL' in self.selected_cmdr: embed_fields.append({ "name": "CMDR Inara Link", - "value": f"[{self.selected_cmdr['TargetName']}]({self.selected_cmdr['inaraURL']})", + "value": f"[{self.selected_cmdr.get('TargetName')}]({self.selected_cmdr.get('inaraURL')})", "inline": True }) if 'squadron' in self.selected_cmdr: - squadron_info = self.selected_cmdr['squadron'] + squadron_info = self.selected_cmdr.get('squadron') if 'squadronName' in squadron_info and 'inaraURL' in squadron_info: embed_fields.append({ "name": "Squadron Inara Link", - "value": f"[{squadron_info['squadronName']} ({squadron_info['squadronMemberRank']})]({squadron_info['inaraURL']})", + "value": f"[{squadron_info.get('squadronName')} ({squadron_info.get('squadronMemberRank')})]({squadron_info.get('inaraURL')})", "inline": True }) discord_channel:DiscordChannel = DiscordChannel.BGS + + description:str = "" + + match self.selected_cmdr.get('Reason'): + case CmdrInteractionReason.FRIEND_REQUEST_RECEIVED: + description = f"{cyan('Friend request received from this CMDR')}" + case CmdrInteractionReason.INTERDICTED_BY: + description = f"{red('INTERDICTED BY this CMDR')}" + case CmdrInteractionReason.KILLED_BY: + description = f"{red('KILLED BY this CMDR')}" + case CmdrInteractionReason.MESSAGE_RECEIVED: + description = f"{blue('Message received from this CMDR in local chat')}" + case CmdrInteractionReason.TEAM_INVITE_RECEIVED: + description = f"{green('Team invite received from this CMDR')}" + case _: + description = f"{yellow('I scanned this CMDR')}" + + description = f"```ansi\n{description}\n```" + if self.bgstally.discord.is_webhook_valid(DiscordChannel.CMDR_INFORMATION): discord_channel = DiscordChannel.CMDR_INFORMATION - self.bgstally.discord.post_embed(f"CMDR {self.selected_cmdr['TargetName']} Spotted", None, embed_fields, None, discord_channel, None) + self.bgstally.discord.post_embed(f"CMDR {self.selected_cmdr.get('TargetName')}", description, embed_fields, None, discord_channel, None) diff --git a/bgstally/windows/fleetcarrier.py b/bgstally/windows/fleetcarrier.py index 140772c..c2e79c1 100644 --- a/bgstally/windows/fleetcarrier.py +++ b/bgstally/windows/fleetcarrier.py @@ -2,7 +2,7 @@ from functools import partial from tkinter import ttk -from bgstally.constants import FONT_HEADING, DiscordChannel, MaterialsCategory +from bgstally.constants import COLOUR_HEADING_1, FONT_HEADING_1, FONT_HEADING_2, DiscordChannel, MaterialsCategory from bgstally.debug import Debug from bgstally.fleetcarrier import FleetCarrier from bgstally.widgets import TextPlus @@ -43,8 +43,8 @@ def show(self): buttons_frame = ttk.Frame(container_frame) buttons_frame.pack(fill=tk.X, padx=5, pady=5, side=tk.BOTTOM) - ttk.Label(info_frame, text=f"System: {fc.data['currentStarSystem']} - Docking: {fc.human_format_dockingaccess()} - Notorious Allowed: {fc.human_format_notorious()}", font=FONT_HEADING, foreground='#A300A3').pack(anchor=tk.NW) - ttk.Label(info_frame, text="Selling", font=FONT_HEADING).pack(anchor=tk.NW) + ttk.Label(info_frame, text=f"System: {fc.data['currentStarSystem']} - Docking: {fc.human_format_dockingaccess()} - Notorious Allowed: {fc.human_format_notorious()}", font=FONT_HEADING_1, foreground=COLOUR_HEADING_1).pack(anchor=tk.NW) + ttk.Label(info_frame, text="Selling", font=FONT_HEADING_2).pack(anchor=tk.NW) selling_frame = ttk.Frame(info_frame) selling_frame.pack(fill=tk.BOTH, padx=5, pady=5, anchor=tk.NW, expand=True) selling_text = TextPlus(selling_frame, wrap=tk.WORD, height=1, font=("Helvetica", 9)) @@ -57,7 +57,7 @@ def show(self): selling_text.configure(state='disabled') - ttk.Label(info_frame, text="Buying", font=FONT_HEADING).pack(anchor=tk.NW) + ttk.Label(info_frame, text="Buying", font=FONT_HEADING_2).pack(anchor=tk.NW) buying_frame = ttk.Frame(info_frame) buying_frame.pack(fill=tk.BOTH, padx=5, pady=5, anchor=tk.NW, expand=True) buying_text = TextPlus(buying_frame, wrap=tk.WORD, height=1, font=("Helvetica", 9)) diff --git a/bgstally/windows/legend.py b/bgstally/windows/legend.py index ae49235..a9db7e5 100644 --- a/bgstally/windows/legend.py +++ b/bgstally/windows/legend.py @@ -2,7 +2,7 @@ from os import path from tkinter import PhotoImage, ttk -from bgstally.constants import FOLDER_ASSETS, FONT_HEADING +from bgstally.constants import FOLDER_ASSETS, FONT_HEADING_2 class WindowLegend: @@ -45,13 +45,13 @@ def show(self): frame_container.pack(fill=tk.BOTH, padx=5, pady=5, expand=1) current_row:int = 0 - ttk.Label(frame_container, text="Icons in BGS Reports", font=FONT_HEADING).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 + ttk.Label(frame_container, text="Icons in BGS Reports", font=FONT_HEADING_2).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 ttk.Label(frame_container, image=self.image_icon_bgs_cz).grid(row=current_row, column=0) ttk.Label(frame_container, text=" On-ground Conflict Zone").grid(row=current_row, column=1, sticky=tk.W); current_row += 1 ttk.Label(frame_container, text="๐Ÿ†‰ ๐Ÿ…ป ๐Ÿ…ผ ๐Ÿ…ท", font=("Helvetica", 24)).grid(row=current_row, column=0) ttk.Label(frame_container, text=" Zero / Low / Med / High demand level for trade buy / sell").grid(row=current_row, column=1, sticky=tk.W); current_row += 1 - ttk.Label(frame_container, text="Icons in Thargoid War Reports", font=FONT_HEADING).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 + ttk.Label(frame_container, text="Icons in Thargoid War Reports", font=FONT_HEADING_2).grid(row=current_row, column=0, columnspan=2, sticky=tk.W, pady=4); current_row += 1 ttk.Label(frame_container, image=self.image_icon_tw_passengers).grid(row=current_row, column=0) ttk.Label(frame_container, text=" Passenger missions").grid(row=current_row, column=1, sticky=tk.W); current_row += 1 ttk.Label(frame_container, image=self.image_icon_tw_cargo).grid(row=current_row, column=0) diff --git a/load.py b/load.py index b70db72..e490276 100644 --- a/load.py +++ b/load.py @@ -8,7 +8,7 @@ from bgstally.debug import Debug PLUGIN_NAME = "BGS-Tally" -PLUGIN_VERSION = semantic_version.Version.coerce("3.3.0") +PLUGIN_VERSION = semantic_version.Version.coerce("3.4.0-a1") # Initialise the main plugin class this:BGSTally = BGSTally(PLUGIN_NAME, PLUGIN_VERSION) diff --git a/thirdparty/tksheet/__init__.py b/thirdparty/tksheet/__init__.py new file mode 100644 index 0000000..a9d0ea4 --- /dev/null +++ b/thirdparty/tksheet/__init__.py @@ -0,0 +1,85 @@ +# ruff: noqa: F401 +# ruff: noqa: F403 +from ._tksheet import Sheet, Sheet_Dropdown +from ._tksheet_column_headers import ColumnHeaders +from ._tksheet_formatters import ( + Formatter, + bool_formatter, + bool_to_str, + data_to_str, + float_formatter, + float_to_str, + format_data, + formatter, + get_clipboard_data, + get_data_with_valid_check, + int_formatter, + is_bool_like, + is_none_like, + percentage_formatter, + percentage_to_str, + to_bool, + to_float, + to_int, + to_str, + try_to_bool, +) +from ._tksheet_main_table import MainTable +from ._tksheet_other_classes import ( + BeginDragDropEvent, + CtrlKeyEvent, + CurrentlySelectedClass, + DeleteRowColumnEvent, + DeselectionEvent, + DraggedRowColumn, + DropDownModifiedEvent, + EditCellEvent, + EditHeaderEvent, + EditIndexEvent, + EndDragDropEvent, + GeneratedMouseEvent, + InsertEvent, + PasteEvent, + ResizeEvent, + SelectCellEvent, + SelectColumnEvent, + SelectionBoxEvent, + SelectRowEvent, + TextEditor, + TextEditor_, + UndoEvent, + dropdown_search_function, + get_checkbox_dict, + get_checkbox_kwargs, + get_dropdown_dict, + get_dropdown_kwargs, + get_index_of_gap_in_sorted_integer_seq_forward, + get_index_of_gap_in_sorted_integer_seq_reverse, + get_n2a, + get_seq_without_gaps_at_index, + is_iterable, + num2alpha, +) +from ._tksheet_row_index import RowIndex +from ._tksheet_top_left_rectangle import TopLeftRectangle +from ._tksheet_vars import ( + USER_OS, + Color_Map_, + arrowkey_bindings_helper, + ctrl_key, + emitted_events, + falsy, + get_font, + get_heading_font, + get_index_font, + nonelike, + rc_binding, + symbols_set, + theme_black, + theme_dark, + theme_dark_blue, + theme_dark_green, + theme_light_blue, + theme_light_green, + truthy, +) diff --git a/thirdparty/tksheet/_tksheet.py b/thirdparty/tksheet/_tksheet.py new file mode 100644 index 0000000..389cef3 --- /dev/null +++ b/thirdparty/tksheet/_tksheet.py @@ -0,0 +1,3902 @@ +from __future__ import annotations + +import tkinter as tk +from collections import deque +from itertools import accumulate, chain, islice +from tkinter import ttk +import bisect +from typing import Union, Callable, List, Set, Tuple + +from ._tksheet_column_headers import ColumnHeaders +from ._tksheet_main_table import MainTable +from ._tksheet_other_classes import ( + GeneratedMouseEvent, + SelectCellEvent, + dropdown_search_function, + get_checkbox_kwargs, + get_dropdown_kwargs, + is_iterable, + show_kwargs_warning, +) +from ._tksheet_row_index import RowIndex +from ._tksheet_top_left_rectangle import TopLeftRectangle +from ._tksheet_vars import ( + emitted_events, + get_font, + get_heading_font, + get_index_font, + rc_binding, + theme_black, + theme_dark, + theme_dark_blue, + theme_dark_green, + theme_light_blue, + theme_light_green, +) + + +class Sheet(tk.Frame): + def __init__( + self, + parent, + show_table: bool = True, + show_top_left: bool = True, + show_row_index: bool = True, + show_header: bool = True, + show_x_scrollbar: bool = True, + show_y_scrollbar: bool = True, + width: int = None, + height: int = None, + headers: List = None, + header: List = None, + default_header: str = "letters", # letters, numbers or both + default_row_index: str = "numbers", # letters, numbers or both + to_clipboard_delimiter="\t", + to_clipboard_quotechar='"', + to_clipboard_lineterminator="\n", + from_clipboard_delimiters=["\t"], + show_default_header_for_empty: bool = True, + show_default_index_for_empty: bool = True, + page_up_down_select_row: bool = True, + expand_sheet_if_paste_too_big: bool = False, + paste_insert_column_limit: int = None, + paste_insert_row_limit: int = None, + show_dropdown_borders: bool = False, + arrow_key_down_right_scroll_page: bool = False, + enable_edit_cell_auto_resize: bool = True, + edit_cell_validation: bool = True, + data_reference: List = None, + data: List = None, + # either (start row, end row, "rows"), + # or (start column, end column, "columns") + # or (cells start row, + # cells start column, + # cells end row, + # cells end column, + # "cells") + startup_select: Tuple = None, + startup_focus: bool = True, + total_columns: int = None, + total_rows: int = None, + column_width: int = 120, + header_height: str = "1", # str or int + max_column_width: str = "inf", # str or int + max_row_height: str = "inf", # str or int + max_header_height: str = "inf", # str or int + max_index_width: str = "inf", # str or int + row_index: List = None, + index: List = None, + after_redraw_time_ms: int = 20, + row_index_width: int = None, + auto_resize_default_row_index: bool = True, + auto_resize_columns: Union[int, None] = None, + auto_resize_rows: Union[int, None] = None, + set_all_heights_and_widths: bool = False, + set_cell_sizes_on_zoom: bool = False, + row_height: str = "1", # str or int + zoom: int = 100, + font: Tuple = get_font(), + header_font: Tuple = get_heading_font(), + index_font: Tuple = get_index_font(), # currently has no effect + popup_menu_font: Tuple = get_font(), + align: str = "w", + header_align: str = "center", + row_index_align: str = "center", + displayed_columns: List = [], + all_columns_displayed: bool = True, + displayed_rows: List = [], + all_rows_displayed: bool = True, + max_undos: int = 30, + outline_thickness: int = 0, + outline_color: str = theme_light_blue["outline_color"], + column_drag_and_drop_perform: bool = True, + row_drag_and_drop_perform: bool = True, + empty_horizontal: int = 150, + empty_vertical: int = 100, + selected_rows_to_end_of_window: bool = False, + horizontal_grid_to_end_of_window: bool = False, + vertical_grid_to_end_of_window: bool = False, + show_vertical_grid: bool = True, + show_horizontal_grid: bool = True, + display_selected_fg_over_highlights: bool = False, + show_selected_cells_border: bool = True, + theme="light blue", + popup_menu_fg=theme_light_blue["popup_menu_fg"], + popup_menu_bg=theme_light_blue["popup_menu_bg"], + popup_menu_highlight_bg=theme_light_blue["popup_menu_highlight_bg"], + popup_menu_highlight_fg=theme_light_blue["popup_menu_highlight_fg"], + frame_bg=theme_light_blue["table_bg"], + table_grid_fg=theme_light_blue["table_grid_fg"], + table_bg=theme_light_blue["table_bg"], + table_fg=theme_light_blue["table_fg"], + table_selected_cells_border_fg=theme_light_blue["table_selected_cells_border_fg"], + table_selected_cells_bg=theme_light_blue["table_selected_cells_bg"], + table_selected_cells_fg=theme_light_blue["table_selected_cells_fg"], + table_selected_rows_border_fg=theme_light_blue["table_selected_rows_border_fg"], + table_selected_rows_bg=theme_light_blue["table_selected_rows_bg"], + table_selected_rows_fg=theme_light_blue["table_selected_rows_fg"], + table_selected_columns_border_fg=theme_light_blue["table_selected_columns_border_fg"], + table_selected_columns_bg=theme_light_blue["table_selected_columns_bg"], + table_selected_columns_fg=theme_light_blue["table_selected_columns_fg"], + resizing_line_fg=theme_light_blue["resizing_line_fg"], + drag_and_drop_bg=theme_light_blue["drag_and_drop_bg"], + index_bg=theme_light_blue["index_bg"], + index_border_fg=theme_light_blue["index_border_fg"], + index_grid_fg=theme_light_blue["index_grid_fg"], + index_fg=theme_light_blue["index_fg"], + index_selected_cells_bg=theme_light_blue["index_selected_cells_bg"], + index_selected_cells_fg=theme_light_blue["index_selected_cells_fg"], + index_selected_rows_bg=theme_light_blue["index_selected_rows_bg"], + index_selected_rows_fg=theme_light_blue["index_selected_rows_fg"], + index_hidden_rows_expander_bg=theme_light_blue["index_hidden_rows_expander_bg"], + header_bg=theme_light_blue["header_bg"], + header_border_fg=theme_light_blue["header_border_fg"], + header_grid_fg=theme_light_blue["header_grid_fg"], + header_fg=theme_light_blue["header_fg"], + header_selected_cells_bg=theme_light_blue["header_selected_cells_bg"], + header_selected_cells_fg=theme_light_blue["header_selected_cells_fg"], + header_selected_columns_bg=theme_light_blue["header_selected_columns_bg"], + header_selected_columns_fg=theme_light_blue["header_selected_columns_fg"], + header_hidden_columns_expander_bg=theme_light_blue["header_hidden_columns_expander_bg"], + top_left_bg=theme_light_blue["top_left_bg"], + top_left_fg=theme_light_blue["top_left_fg"], + top_left_fg_highlight=theme_light_blue["top_left_fg_highlight"], + ): + tk.Frame.__init__( + self, + parent, + background=frame_bg, + highlightthickness=outline_thickness, + highlightbackground=outline_color, + highlightcolor=outline_color, + ) + self.C = parent + self.dropdown_class = Sheet_Dropdown + self.after_redraw_id = None + self.after_redraw_time_ms = after_redraw_time_ms + if width is not None or height is not None: + self.grid_propagate(0) + if width is not None: + self.config(width=width) + if height is not None: + self.config(height=height) + if width is not None and height is None: + self.config(height=300) + if height is not None and width is None: + self.config(width=350) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(1, weight=1) + self.RI = RowIndex( + parentframe=self, + row_index_align=self.convert_align(row_index_align), + index_bg=index_bg, + index_border_fg=index_border_fg, + index_grid_fg=index_grid_fg, + index_fg=index_fg, + index_selected_cells_bg=index_selected_cells_bg, + index_selected_cells_fg=index_selected_cells_fg, + index_selected_rows_bg=index_selected_rows_bg, + index_selected_rows_fg=index_selected_rows_fg, + index_hidden_rows_expander_bg=index_hidden_rows_expander_bg, + drag_and_drop_bg=drag_and_drop_bg, + resizing_line_fg=resizing_line_fg, + row_drag_and_drop_perform=row_drag_and_drop_perform, + default_row_index=default_row_index, + auto_resize_width=auto_resize_default_row_index, + show_default_index_for_empty=show_default_index_for_empty, + ) + self.CH = ColumnHeaders( + parentframe=self, + default_header=default_header, + header_align=self.convert_align(header_align), + header_bg=header_bg, + header_border_fg=header_border_fg, + header_grid_fg=header_grid_fg, + header_fg=header_fg, + header_selected_cells_bg=header_selected_cells_bg, + header_selected_cells_fg=header_selected_cells_fg, + header_selected_columns_bg=header_selected_columns_bg, + header_selected_columns_fg=header_selected_columns_fg, + header_hidden_columns_expander_bg=header_hidden_columns_expander_bg, + drag_and_drop_bg=drag_and_drop_bg, + column_drag_and_drop_perform=column_drag_and_drop_perform, + resizing_line_fg=resizing_line_fg, + show_default_header_for_empty=show_default_header_for_empty, + ) + self.MT = MainTable( + parentframe=self, + max_column_width=max_column_width, + max_header_height=max_header_height, + max_row_height=max_row_height, + max_index_width=max_index_width, + row_index_width=row_index_width, + header_height=header_height, + column_width=column_width, + row_height=row_height, + show_index=show_row_index, + show_header=show_header, + enable_edit_cell_auto_resize=enable_edit_cell_auto_resize, + edit_cell_validation=edit_cell_validation, + page_up_down_select_row=page_up_down_select_row, + expand_sheet_if_paste_too_big=expand_sheet_if_paste_too_big, + paste_insert_column_limit=paste_insert_column_limit, + paste_insert_row_limit=paste_insert_row_limit, + show_dropdown_borders=show_dropdown_borders, + arrow_key_down_right_scroll_page=arrow_key_down_right_scroll_page, + display_selected_fg_over_highlights=display_selected_fg_over_highlights, + show_vertical_grid=show_vertical_grid, + show_horizontal_grid=show_horizontal_grid, + to_clipboard_delimiter=to_clipboard_delimiter, + to_clipboard_quotechar=to_clipboard_quotechar, + to_clipboard_lineterminator=to_clipboard_lineterminator, + from_clipboard_delimiters=from_clipboard_delimiters, + column_headers_canvas=self.CH, + row_index_canvas=self.RI, + headers=headers, + header=header, + data_reference=data if data_reference is None else data_reference, + auto_resize_columns=auto_resize_columns, + auto_resize_rows=auto_resize_rows, + set_cell_sizes_on_zoom=set_cell_sizes_on_zoom, + total_cols=total_columns, + total_rows=total_rows, + row_index=row_index, + index=index, + zoom=zoom, + font=font, + header_font=header_font, + index_font=index_font, + popup_menu_font=popup_menu_font, + popup_menu_fg=popup_menu_fg, + popup_menu_bg=popup_menu_bg, + popup_menu_highlight_bg=popup_menu_highlight_bg, + popup_menu_highlight_fg=popup_menu_highlight_fg, + align=self.convert_align(align), + table_bg=table_bg, + table_grid_fg=table_grid_fg, + table_fg=table_fg, + show_selected_cells_border=show_selected_cells_border, + table_selected_cells_border_fg=table_selected_cells_border_fg, + table_selected_cells_bg=table_selected_cells_bg, + table_selected_cells_fg=table_selected_cells_fg, + table_selected_rows_border_fg=table_selected_rows_border_fg, + table_selected_rows_bg=table_selected_rows_bg, + table_selected_rows_fg=table_selected_rows_fg, + table_selected_columns_border_fg=table_selected_columns_border_fg, + table_selected_columns_bg=table_selected_columns_bg, + table_selected_columns_fg=table_selected_columns_fg, + displayed_columns=displayed_columns, + all_columns_displayed=all_columns_displayed, + displayed_rows=displayed_rows, + all_rows_displayed=all_rows_displayed, + selected_rows_to_end_of_window=selected_rows_to_end_of_window, + horizontal_grid_to_end_of_window=horizontal_grid_to_end_of_window, + vertical_grid_to_end_of_window=vertical_grid_to_end_of_window, + empty_horizontal=empty_horizontal, + empty_vertical=empty_vertical, + max_undos=max_undos, + ) + self.TL = TopLeftRectangle( + parentframe=self, + main_canvas=self.MT, + row_index_canvas=self.RI, + header_canvas=self.CH, + top_left_bg=top_left_bg, + top_left_fg=top_left_fg, + top_left_fg_highlight=top_left_fg_highlight, + ) + self.yscroll = ttk.Scrollbar(self, command=self.MT.set_yviews, orient="vertical") + self.xscroll = ttk.Scrollbar(self, command=self.MT.set_xviews, orient="horizontal") + if show_top_left: + self.TL.grid(row=0, column=0) + if show_table: + self.MT.grid(row=1, column=1, sticky="nswe") + self.MT["xscrollcommand"] = self.xscroll.set + self.MT["yscrollcommand"] = self.yscroll.set + if show_row_index: + self.RI.grid(row=1, column=0, sticky="nswe") + self.RI["yscrollcommand"] = self.yscroll.set + if show_header: + self.CH.grid(row=0, column=1, sticky="nswe") + self.CH["xscrollcommand"] = self.xscroll.set + if show_x_scrollbar: + self.xscroll.grid(row=2, column=0, columnspan=2, sticky="nswe") + self.xscroll_showing = True + self.xscroll_disabled = False + else: + self.xscroll_showing = False + self.xscroll_disabled = True + if show_y_scrollbar: + self.yscroll.grid(row=0, column=2, rowspan=3, sticky="nswe") + self.yscroll_showing = True + self.yscroll_disabled = False + else: + self.yscroll_showing = False + self.yscroll_disabled = True + self.update_idletasks() + self.MT.update_idletasks() + self.RI.update_idletasks() + self.CH.update_idletasks() + if theme != "light blue": + self.change_theme(theme) + for k, v in locals().items(): + if k in theme_light_blue and v != theme_light_blue[k]: + self.set_options(**{k: v}) + if set_all_heights_and_widths: + self.set_all_cell_sizes_to_text() + if startup_select is not None: + try: + if startup_select[-1] == "cells": + self.MT.create_selected(*startup_select) + self.MT.set_currently_selected(startup_select[0], startup_select[1], type_="cell", inside=True) + self.see(startup_select[0], startup_select[1]) + elif startup_select[-1] == "rows": + self.MT.create_selected( + startup_select[0], + 0, + startup_select[1], + len(self.MT.col_positions) - 1, + "rows", + ) + self.MT.set_currently_selected(startup_select[0], 0, type_="row", inside=True) + self.see(startup_select[0], 0) + elif startup_select[-1] in ("cols", "columns"): + self.MT.create_selected( + 0, + startup_select[0], + len(self.MT.row_positions) - 1, + startup_select[1], + "columns", + ) + self.MT.set_currently_selected(0, startup_select[0], type_="column", inside=True) + self.see(0, startup_select[0]) + except Exception: + pass + self.refresh() + if startup_focus: + self.MT.focus_set() + + def set_refresh_timer(self, redraw=True): + if redraw and self.after_redraw_id is None: + self.after_redraw_id = self.after(self.after_redraw_time_ms, self.after_redraw) + + def after_redraw(self, redraw_header=True, redraw_row_index=True): + self.MT.main_table_redraw_grid_and_text(redraw_header=redraw_header, redraw_row_index=redraw_row_index) + self.after_redraw_id = None + + def show(self, canvas="all"): + if canvas == "all": + self.hide() + self.TL.grid(row=0, column=0) + self.RI.grid(row=1, column=0, sticky="nswe") + self.CH.grid(row=0, column=1, sticky="nswe") + self.MT.grid(row=1, column=1, sticky="nswe") + self.yscroll.grid(row=0, column=2, rowspan=3, sticky="nswe") + self.xscroll.grid(row=2, column=0, columnspan=2, sticky="nswe") + self.MT["xscrollcommand"] = self.xscroll.set + self.CH["xscrollcommand"] = self.xscroll.set + self.MT["yscrollcommand"] = self.yscroll.set + self.RI["yscrollcommand"] = self.yscroll.set + self.xscroll_showing = True + self.yscroll_showing = True + self.xscroll_disabled = False + self.yscroll_disabled = False + elif canvas == "row_index": + self.RI.grid(row=1, column=0, sticky="nswe") + self.MT["yscrollcommand"] = self.yscroll.set + self.RI["yscrollcommand"] = self.yscroll.set + self.MT.show_index = True + elif canvas == "header": + self.CH.grid(row=0, column=1, sticky="nswe") + self.MT["xscrollcommand"] = self.xscroll.set + self.CH["xscrollcommand"] = self.xscroll.set + self.MT.show_header = True + elif canvas == "top_left": + self.TL.grid(row=0, column=0) + elif canvas == "x_scrollbar": + self.xscroll.grid(row=2, column=0, columnspan=2, sticky="nswe") + self.xscroll_showing = True + self.xscroll_disabled = False + elif canvas == "y_scrollbar": + self.yscroll.grid(row=0, column=2, rowspan=3, sticky="nswe") + self.yscroll_showing = True + self.yscroll_disabled = False + self.MT.update_idletasks() + + def hide(self, canvas="all"): + if canvas.lower() == "all": + self.TL.grid_forget() + self.RI.grid_forget() + self.RI["yscrollcommand"] = 0 + self.MT.show_index = False + self.CH.grid_forget() + self.CH["xscrollcommand"] = 0 + self.MT.show_header = False + self.MT.grid_forget() + self.yscroll.grid_forget() + self.xscroll.grid_forget() + self.xscroll_showing = False + self.yscroll_showing = False + self.xscroll_disabled = True + self.yscroll_disabled = True + elif canvas.lower() == "row_index": + self.RI.grid_forget() + self.RI["yscrollcommand"] = 0 + self.MT.show_index = False + elif canvas.lower() == "header": + self.CH.grid_forget() + self.CH["xscrollcommand"] = 0 + self.MT.show_header = False + elif canvas.lower() == "top_left": + self.TL.grid_forget() + elif canvas.lower() == "x_scrollbar": + self.xscroll.grid_forget() + self.xscroll_showing = False + self.xscroll_disabled = True + elif canvas.lower() == "y_scrollbar": + self.yscroll.grid_forget() + self.yscroll_showing = False + self.yscroll_disabled = True + + def height_and_width(self, height=None, width=None): + if width is not None or height is not None: + self.grid_propagate(0) + elif width is None and height is None: + self.grid_propagate(1) + if width is not None: + self.config(width=width) + if height is not None: + self.config(height=height) + + def focus_set(self, canvas="table"): + if canvas == "table": + self.MT.focus_set() + elif canvas == "header": + self.CH.focus_set() + elif canvas == "index": + self.RI.focus_set() + elif canvas == "topleft": + self.TL.focus_set() + + def displayed_column_to_data(self, c): + return c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + + def displayed_row_to_data(self, r): + return r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + + def popup_menu_add_command( + self, + label, + func, + table_menu=True, + index_menu=True, + header_menu=True, + empty_space_menu=True, + ): + if label not in self.MT.extra_table_rc_menu_funcs and table_menu: + self.MT.extra_table_rc_menu_funcs[label] = func + if label not in self.MT.extra_index_rc_menu_funcs and index_menu: + self.MT.extra_index_rc_menu_funcs[label] = func + if label not in self.MT.extra_header_rc_menu_funcs and header_menu: + self.MT.extra_header_rc_menu_funcs[label] = func + if label not in self.MT.extra_empty_space_rc_menu_funcs and empty_space_menu: + self.MT.extra_empty_space_rc_menu_funcs[label] = func + self.MT.create_rc_menus() + + def popup_menu_del_command(self, label=None): + if label is None: + self.MT.extra_table_rc_menu_funcs = {} + self.MT.extra_index_rc_menu_funcs = {} + self.MT.extra_header_rc_menu_funcs = {} + self.MT.extra_empty_space_rc_menu_funcs = {} + else: + if label in self.MT.extra_table_rc_menu_funcs: + del self.MT.extra_table_rc_menu_funcs[label] + if label in self.MT.extra_index_rc_menu_funcs: + del self.MT.extra_index_rc_menu_funcs[label] + if label in self.MT.extra_header_rc_menu_funcs: + del self.MT.extra_header_rc_menu_funcs[label] + if label in self.MT.extra_empty_space_rc_menu_funcs: + del self.MT.extra_empty_space_rc_menu_funcs[label] + self.MT.create_rc_menus() + + def extra_bindings(self, bindings, func=None): + # bindings is str, func arg is None or Callable + if isinstance(bindings, str): + iterable = [(bindings, func)] + # bindings is list or tuple of strings, func arg is None or Callable + elif is_iterable(bindings) and isinstance(bindings[0], str): + iterable = [(b, func) for b in bindings] + # bindings is a list or tuple of two tuples or lists + # in this case the func arg is ignored + # e.g. [(binding, function), (binding, function), ...] + elif is_iterable(bindings): + iterable = bindings + + for b, f in iterable: + b = b.lower() + + if func is not None and b in emitted_events: + self.bind_event(b, f) + + if b in ( + "all", + "bind_all", + "unbind_all", + ): + self.MT.extra_begin_ctrl_c_func = f + self.MT.extra_begin_ctrl_x_func = f + self.MT.extra_begin_ctrl_v_func = f + self.MT.extra_begin_ctrl_z_func = f + self.MT.extra_begin_delete_key_func = f + self.RI.ri_extra_begin_drag_drop_func = f + self.CH.ch_extra_begin_drag_drop_func = f + self.MT.extra_begin_del_rows_rc_func = f + self.MT.extra_begin_del_cols_rc_func = f + self.MT.extra_begin_insert_cols_rc_func = f + self.MT.extra_begin_insert_rows_rc_func = f + self.MT.extra_begin_edit_cell_func = f + self.CH.extra_begin_edit_cell_func = f + self.RI.extra_begin_edit_cell_func = f + self.CH.column_width_resize_func = f + self.RI.row_height_resize_func = f + + if b in ( + "all", + "bind_all", + "unbind_all", + "all_select_events", + "select", + "selectevents", + "select_events", + ): + self.MT.selection_binding_func = f + self.MT.select_all_binding_func = f + self.RI.selection_binding_func = f + self.CH.selection_binding_func = f + self.MT.drag_selection_binding_func = f + self.RI.drag_selection_binding_func = f + self.CH.drag_selection_binding_func = f + self.MT.shift_selection_binding_func = f + self.RI.shift_selection_binding_func = f + self.CH.shift_selection_binding_func = f + self.MT.ctrl_selection_binding_func = f + self.RI.ctrl_selection_binding_func = f + self.CH.ctrl_selection_binding_func = f + self.MT.deselection_binding_func = f + + if b in ( + "all", + "bind_all", + "unbind_all", + "all_modified_events", + "sheetmodified", + "sheet_modified" "modified_events", + "modified", + ): + self.MT.extra_end_ctrl_c_func = f + self.MT.extra_end_ctrl_x_func = f + self.MT.extra_end_ctrl_v_func = f + self.MT.extra_end_ctrl_z_func = f + self.MT.extra_end_delete_key_func = f + self.RI.ri_extra_end_drag_drop_func = f + self.CH.ch_extra_end_drag_drop_func = f + self.MT.extra_end_del_rows_rc_func = f + self.MT.extra_end_del_cols_rc_func = f + self.MT.extra_end_insert_cols_rc_func = f + self.MT.extra_end_insert_rows_rc_func = f + self.MT.extra_end_edit_cell_func = f + self.CH.extra_end_edit_cell_func = f + self.RI.extra_end_edit_cell_func = f + + if b in ( + "begin_copy", + "begin_ctrl_c", + ): + self.MT.extra_begin_ctrl_c_func = f + if b in ( + "ctrl_c", + "end_copy", + "end_ctrl_c", + "copy", + ): + self.MT.extra_end_ctrl_c_func = f + + if b in ( + "begin_cut", + "begin_ctrl_x", + ): + self.MT.extra_begin_ctrl_x_func = f + if b in ( + "ctrl_x", + "end_cut", + "end_ctrl_x", + "cut", + ): + self.MT.extra_end_ctrl_x_func = f + + if b in ( + "begin_paste", + "begin_ctrl_v", + ): + self.MT.extra_begin_ctrl_v_func = f + if b in ( + "ctrl_v", + "end_paste", + "end_ctrl_v", + "paste", + ): + self.MT.extra_end_ctrl_v_func = f + + if b in ( + "begin_undo", + "begin_ctrl_z", + ): + self.MT.extra_begin_ctrl_z_func = f + if b in ( + "ctrl_z", + "end_undo", + "end_ctrl_z", + "undo", + ): + self.MT.extra_end_ctrl_z_func = f + + if b in ( + "begin_delete_key", + "begin_delete", + ): + self.MT.extra_begin_delete_key_func = f + if b in ( + "delete_key", + "end_delete", + "end_delete_key", + "delete", + ): + self.MT.extra_end_delete_key_func = f + + if b in ( + "begin_edit_cell", + "begin_edit_table", + ): + self.MT.extra_begin_edit_cell_func = f + if b in ( + "end_edit_cell", + "edit_cell", + "edit_table", + ): + self.MT.extra_end_edit_cell_func = f + + if b == "begin_edit_header": + self.CH.extra_begin_edit_cell_func = f + if b in ( + "end_edit_header", + "edit_header", + ): + self.CH.extra_end_edit_cell_func = f + + if b == "begin_edit_index": + self.RI.extra_begin_edit_cell_func = f + if b in ( + "end_edit_index", + "edit_index", + ): + self.RI.extra_end_edit_cell_func = f + + if b in ( + "begin_row_index_drag_drop", + "begin_move_rows", + ): + self.RI.ri_extra_begin_drag_drop_func = f + if b in ( + "row_index_drag_drop", + "move_rows", + "end_move_rows", + "end_row_index_drag_drop", + ): + self.RI.ri_extra_end_drag_drop_func = f + + if b in ( + "begin_column_header_drag_drop", + "begin_move_columns", + ): + self.CH.ch_extra_begin_drag_drop_func = f + if b in ( + "column_header_drag_drop", + "move_columns", + "end_move_columns", + "end_column_header_drag_drop", + ): + self.CH.ch_extra_end_drag_drop_func = f + + if b in ( + "begin_rc_delete_row", + "begin_delete_rows", + ): + self.MT.extra_begin_del_rows_rc_func = f + if b in ( + "rc_delete_row", + "end_rc_delete_row", + "end_delete_rows", + "delete_rows", + ): + self.MT.extra_end_del_rows_rc_func = f + + if b in ( + "begin_rc_delete_column", + "begin_delete_columns", + ): + self.MT.extra_begin_del_cols_rc_func = f + if b in ( + "rc_delete_column", + "end_rc_delete_column", + "end_delete_columns", + "delete_columns", + ): + self.MT.extra_end_del_cols_rc_func = f + + if b in ( + "begin_rc_insert_column", + "begin_insert_column", + "begin_insert_columns", + "begin_add_column", + "begin_rc_add_column", + "begin_add_columns", + ): + self.MT.extra_begin_insert_cols_rc_func = f + if b in ( + "rc_insert_column", + "end_rc_insert_column", + "end_insert_column", + "end_insert_columns", + "rc_add_column", + "end_rc_add_column", + "end_add_column", + "end_add_columns", + ): + self.MT.extra_end_insert_cols_rc_func = f + + if b in ( + "begin_rc_insert_row", + "begin_insert_row", + "begin_insert_rows", + "begin_rc_add_row", + "begin_add_row", + "begin_add_rows", + ): + self.MT.extra_begin_insert_rows_rc_func = f + if b in ( + "rc_insert_row", + "end_rc_insert_row", + "end_insert_row", + "end_insert_rows", + "rc_add_row", + "end_rc_add_row", + "end_add_row", + "end_add_rows", + ): + self.MT.extra_end_insert_rows_rc_func = f + + if b == "column_width_resize": + self.CH.column_width_resize_func = f + if b == "row_height_resize": + self.RI.row_height_resize_func = f + + if b == "cell_select": + self.MT.selection_binding_func = f + if b in ( + "select_all", + "ctrl_a", + ): + self.MT.select_all_binding_func = f + if b == "row_select": + self.RI.selection_binding_func = f + if b in ( + "col_select", + "column_select", + ): + self.CH.selection_binding_func = f + if b == "drag_select_cells": + self.MT.drag_selection_binding_func = f + if b == "drag_select_rows": + self.RI.drag_selection_binding_func = f + if b == "drag_select_columns": + self.CH.drag_selection_binding_func = f + if b == "shift_cell_select": + self.MT.shift_selection_binding_func = f + if b == "shift_row_select": + self.RI.shift_selection_binding_func = f + if b == "shift_column_select": + self.CH.shift_selection_binding_func = f + if b == "ctrl_cell_select": + self.MT.ctrl_selection_binding_func = f + if b == "ctrl_row_select": + self.RI.ctrl_selection_binding_func = f + if b == "ctrl_column_select": + self.CH.ctrl_selection_binding_func = f + if b == "deselect": + self.MT.deselection_binding_func = f + + def emit_event(self, event, data={}): + self.event_generate(event, data=data) + + def bind_event(self, sequence: str, func: Callable, add: Union[str, None] = None) -> None: + widget = self + + def _substitute(*args) -> Tuple[None]: + def e() -> None: + return None + + e.data = args[0] + e.widget = widget + return (e,) + + funcid = widget._register(func, _substitute, needcleanup=1) + cmd = '{0}if {{"[{1} %d]" == "break"}} break\n'.format("+" if add else "", funcid) + widget.tk.call("bind", widget._w, sequence, cmd) + + def sync_scroll(self, widget: object) -> Sheet: + if widget is self: + return self + self.MT.synced_scrolls.add(widget) + if isinstance(widget, Sheet): + widget.MT.synced_scrolls.add(self) + return self + + def unsync_scroll(self, widget: None | Sheet = None) -> Sheet: + if widget is None: + for widget in self.MT.synced_scrolls: + if isinstance(widget, Sheet): + widget.MT.synced_scrolls.discard(self) + self.MT.synced_scrolls = set() + else: + if isinstance(widget, Sheet) and self in widget.MT.synced_scrolls: + widget.MT.synced_scrolls.discard(self) + self.MT.synced_scrolls.discard(widget) + return self + + def bind(self, binding, func, add=None): + if binding == "": + self.MT.extra_b1_press_func = func + self.CH.extra_b1_press_func = func + self.RI.extra_b1_press_func = func + self.TL.extra_b1_press_func = func + elif binding == "": + self.MT.extra_b1_motion_func = func + self.CH.extra_b1_motion_func = func + self.RI.extra_b1_motion_func = func + self.TL.extra_b1_motion_func = func + elif binding == "": + self.MT.extra_b1_release_func = func + self.CH.extra_b1_release_func = func + self.RI.extra_b1_release_func = func + self.TL.extra_b1_release_func = func + elif binding == "": + self.MT.extra_double_b1_func = func + self.CH.extra_double_b1_func = func + self.RI.extra_double_b1_func = func + self.TL.extra_double_b1_func = func + elif binding == "": + self.MT.extra_motion_func = func + self.CH.extra_motion_func = func + self.RI.extra_motion_func = func + self.TL.extra_motion_func = func + elif binding == rc_binding: + self.MT.extra_rc_func = func + self.CH.extra_rc_func = func + self.RI.extra_rc_func = func + self.TL.extra_rc_func = func + else: + self.MT.bind(binding, func, add=add) + self.CH.bind(binding, func, add=add) + self.RI.bind(binding, func, add=add) + self.TL.bind(binding, func, add=add) + + def unbind(self, binding): + if binding == "": + self.MT.extra_b1_press_func = None + self.CH.extra_b1_press_func = None + self.RI.extra_b1_press_func = None + self.TL.extra_b1_press_func = None + elif binding == "": + self.MT.extra_b1_motion_func = None + self.CH.extra_b1_motion_func = None + self.RI.extra_b1_motion_func = None + self.TL.extra_b1_motion_func = None + elif binding == "": + self.MT.extra_b1_release_func = None + self.CH.extra_b1_release_func = None + self.RI.extra_b1_release_func = None + self.TL.extra_b1_release_func = None + elif binding == "": + self.MT.extra_double_b1_func = None + self.CH.extra_double_b1_func = None + self.RI.extra_double_b1_func = None + self.TL.extra_double_b1_func = None + elif binding == "": + self.MT.extra_motion_func = None + self.CH.extra_motion_func = None + self.RI.extra_motion_func = None + self.TL.extra_motion_func = None + elif binding == rc_binding: + self.MT.extra_rc_func = None + self.CH.extra_rc_func = None + self.RI.extra_rc_func = None + self.TL.extra_rc_func = None + else: + self.MT.unbind(binding) + self.CH.unbind(binding) + self.RI.unbind(binding) + self.TL.unbind(binding) + + def enable_bindings(self, *bindings): + self.MT.enable_bindings(bindings) + + def disable_bindings(self, *bindings): + self.MT.disable_bindings(bindings) + + def basic_bindings(self, enable=False): + for canvas in (self.MT, self.CH, self.RI, self.TL): + canvas.basic_bindings(enable) + + def edit_bindings(self, enable=False): + if enable: + self.MT.edit_bindings(True) + elif not enable: + self.MT.edit_bindings(False) + + def cell_edit_binding(self, enable=False, keys=[]): + self.MT.bind_cell_edit(enable, keys=[]) + + def identify_region(self, event): + if event.widget == self.MT: + return "table" + elif event.widget == self.RI: + return "index" + elif event.widget == self.CH: + return "header" + elif event.widget == self.TL: + return "top left" + + def identify_row(self, event, exclude_index=False, allow_end=True): + ev_w = event.widget + if ev_w == self.MT: + return self.MT.identify_row(y=event.y, allow_end=allow_end) + elif ev_w == self.RI: + if exclude_index: + return None + else: + return self.MT.identify_row(y=event.y, allow_end=allow_end) + elif ev_w == self.CH or ev_w == self.TL: + return None + + def identify_column(self, event, exclude_header=False, allow_end=True): + ev_w = event.widget + if ev_w == self.MT: + return self.MT.identify_col(x=event.x, allow_end=allow_end) + elif ev_w == self.RI or ev_w == self.TL: + return None + elif ev_w == self.CH: + if exclude_header: + return None + else: + return self.MT.identify_col(x=event.x, allow_end=allow_end) + + def get_example_canvas_column_widths(self, total_cols=None): + colpos = int(self.MT.default_column_width) + if total_cols is not None: + return list(accumulate(chain([0], (colpos for c in range(total_cols))))) + return list(accumulate(chain([0], (colpos for c in range(len(self.MT.col_positions) - 1))))) + + def get_example_canvas_row_heights(self, total_rows=None): + rowpos = self.MT.default_row_height[1] + if total_rows is not None: + return list(accumulate(chain([0], (rowpos for c in range(total_rows))))) + return list(accumulate(chain([0], (rowpos for c in range(len(self.MT.row_positions) - 1))))) + + def get_column_widths(self, canvas_positions=False): + if canvas_positions: + return [int(n) for n in self.MT.col_positions] + return [ + int(b - a) + for a, b in zip( + self.MT.col_positions, + islice(self.MT.col_positions, 1, len(self.MT.col_positions)), + ) + ] + + def get_row_heights(self, canvas_positions=False): + if canvas_positions: + return [int(n) for n in self.MT.row_positions] + return [ + int(b - a) + for a, b in zip( + self.MT.row_positions, + islice(self.MT.row_positions, 1, len(self.MT.row_positions)), + ) + ] + + def set_all_cell_sizes_to_text(self, redraw=True): + self.MT.set_all_cell_sizes_to_text() + self.set_refresh_timer(redraw) + return self.MT.row_positions, self.MT.col_positions + + def set_all_column_widths( + self, + width=None, + only_set_if_too_small=False, + redraw=True, + recreate_selection_boxes=True, + ): + self.CH.set_width_of_all_cols( + width=width, + only_set_if_too_small=only_set_if_too_small, + recreate=recreate_selection_boxes, + ) + self.set_refresh_timer(redraw) + + def column_width(self, column=None, width=None, only_set_if_too_small=False, redraw=True): + if column == "all": + if width == "default": + self.MT.reset_col_positions() + elif column == "displayed": + if width == "text": + sc, ec = self.MT.get_visible_columns(self.MT.canvasx(0), self.MT.canvasx(self.winfo_width())) + for c in range(sc, ec - 1): + self.CH.set_col_width(c) + elif width == "text" and column is not None: + self.CH.set_col_width(col=column, width=None, only_set_if_too_small=only_set_if_too_small) + elif width is not None and column is not None: + self.CH.set_col_width(col=column, width=width, only_set_if_too_small=only_set_if_too_small) + elif column is not None: + return int(self.MT.col_positions[column + 1] - self.MT.col_positions[column]) + self.set_refresh_timer(redraw) + + def set_column_widths(self, column_widths=None, canvas_positions=False, reset=False, verify=False): + cwx = None + if reset: + self.MT.reset_col_positions() + return + if verify: + cwx = self.verify_column_widths(column_widths, canvas_positions) + if is_iterable(column_widths): + if canvas_positions and isinstance(column_widths, list): + self.MT.col_positions = column_widths + else: + self.MT.col_positions = list(accumulate(chain([0], (width for width in column_widths)))) + return cwx + + def set_all_row_heights( + self, + height=None, + only_set_if_too_small=False, + redraw=True, + recreate_selection_boxes=True, + ): + self.RI.set_height_of_all_rows( + height=height, + only_set_if_too_small=only_set_if_too_small, + recreate=recreate_selection_boxes, + ) + self.set_refresh_timer(redraw) + + def set_cell_size_to_text(self, row, column, only_set_if_too_small=False, redraw=True): + self.MT.set_cell_size_to_text(r=row, c=column, only_set_if_too_small=only_set_if_too_small) + self.set_refresh_timer(redraw) + # backwards compatibility + + def set_width_of_index_to_text(self, text=None, *args, **kwargs): + self.RI.set_width_of_index_to_text(text=text) + + def set_height_of_header_to_text(self, text=None): + self.CH.set_height_of_header_to_text(text=text) + + def row_height(self, row=None, height=None, only_set_if_too_small=False, redraw=True): + if row == "all": + if height == "default": + self.MT.reset_row_positions() + elif row == "displayed": + if height == "text": + sr, er = self.MT.get_visible_rows(self.MT.canvasy(0), self.MT.canvasy(self.winfo_width())) + for r in range(sr, er - 1): + self.RI.set_row_height(r) + elif height == "text" and row is not None: + self.RI.set_row_height(row=row, height=None, only_set_if_too_small=only_set_if_too_small) + elif height is not None and row is not None: + self.RI.set_row_height(row=row, height=height, only_set_if_too_small=only_set_if_too_small) + elif row is not None: + return int(self.MT.row_positions[row + 1] - self.MT.row_positions[row]) + self.set_refresh_timer(redraw) + + def set_row_heights(self, row_heights=None, canvas_positions=False, reset=False, verify=False): + if reset: + self.MT.reset_row_positions() + return + if is_iterable(row_heights): + qmin = self.MT.min_row_height + if canvas_positions and isinstance(row_heights, list): + if verify: + self.MT.row_positions = list( + accumulate( + chain( + [0], + ( + height if qmin < height else qmin + for height in [ + x - z + for z, x in zip( + islice(row_heights, 0, None), + islice(row_heights, 1, None), + ) + ] + ), + ) + ) + ) + else: + self.MT.row_positions = row_heights + else: + if verify: + self.MT.row_positions = [ + qmin if z < qmin or not isinstance(z, int) or isinstance(z, bool) else z for z in row_heights + ] + else: + self.MT.row_positions = list(accumulate(chain([0], (height for height in row_heights)))) + + def verify_row_heights(self, row_heights: List, canvas_positions=False): + if row_heights[0] != 0 or isinstance(row_heights[0], bool): + return False + if not isinstance(row_heights, list): + return False + if canvas_positions: + if any( + x - z < self.MT.min_row_height or not isinstance(x, int) or isinstance(x, bool) + for z, x in zip(islice(row_heights, 0, None), islice(row_heights, 1, None)) + ): + return False + elif not canvas_positions: + if any(z < self.MT.min_row_height or not isinstance(z, int) or isinstance(z, bool) for z in row_heights): + return False + return True + + def verify_column_widths(self, column_widths: List, canvas_positions=False): + if column_widths[0] != 0 or isinstance(column_widths[0], bool): + return False + if not isinstance(column_widths, list): + return False + if canvas_positions: + if any( + x - z < self.MT.min_column_width or not isinstance(x, int) or isinstance(x, bool) + for z, x in zip(islice(column_widths, 0, None), islice(column_widths, 1, None)) + ): + return False + elif not canvas_positions: + if any( + z < self.MT.min_column_width or not isinstance(z, int) or isinstance(z, bool) for z in column_widths + ): + return False + return True + + def default_row_height(self, height=None): + if height is not None: + self.MT.default_row_height = ( + height if isinstance(height, str) else "pixels", + height if isinstance(height, int) else self.MT.get_lines_cell_height(int(height)), + ) + return self.MT.default_row_height[1] + + def default_header_height(self, height=None): + if height is not None: + self.MT.default_header_height = ( + height if isinstance(height, str) else "pixels", + height + if isinstance(height, int) + else self.MT.get_lines_cell_height(int(height), font=self.MT.header_font), + ) + return self.MT.default_header_height[1] + + def default_column_width(self, width=None): + if width is not None: + if width < self.MT.min_column_width: + self.MT.default_column_width = self.MT.min_column_width + 20 + else: + self.MT.default_column_width = int(width) + return self.MT.default_column_width + + def cut(self, event=None): + self.MT.ctrl_x() + + def copy(self, event=None): + self.MT.ctrl_c() + + def paste(self, event=None): + self.MT.ctrl_v() + + def delete(self, event=None): + self.MT.delete_key() + + def undo(self, event=None): + self.MT.ctrl_z() + + def delete_row_position(self, idx: int, deselect_all=False): + self.MT.del_row_position(idx=idx, deselect_all=deselect_all) + + def delete_row(self, idx=0, deselect_all=False, redraw=True): + self.delete_rows(rows={idx}, deselect_all=deselect_all, redraw=False) + self.set_refresh_timer(redraw) + + def delete_rows(self, rows: Set = set(), deselect_all=False, redraw=True): + if deselect_all: + self.deselect("all", redraw=False) + if isinstance(rows, set): + to_del = rows + else: + to_del = set(rows) + if not to_del: + return + self.MT.data[:] = [row for r, row in enumerate(self.MT.data) if r not in to_del] + to_bis = sorted(to_del) + if self.MT.all_rows_displayed: + self.set_row_heights( + row_heights=( + h + for r, h in enumerate( + int(b - a) + for a, b in zip( + self.MT.row_positions, + islice(self.MT.row_positions, 1, len(self.MT.row_positions)), + ) + ) + if r not in to_del + ) + ) + else: + dispset = set(self.MT.displayed_rows) + heights_to_del = {i for i, r in enumerate(to_bis) if r in dispset} + if heights_to_del: + self.set_row_heights( + row_heights=( + h + for r, h in enumerate( + int(b - a) + for a, b in zip( + self.MT.row_positions, + islice(self.MT.row_positions, 1, len(self.MT.row_positions)), + ) + ) + if r not in heights_to_del + ) + ) + self.MT.displayed_rows = [r for r in self.MT.displayed_rows if r not in to_del] + self.MT.cell_options = { + ( + r if not bisect.bisect_left(to_bis, r) else r - bisect.bisect_left(to_bis, r), + c, + ): v + for (r, c), v in self.MT.cell_options.items() + if r not in to_del + } + self.MT.row_options = { + r if not bisect.bisect_left(to_bis, r) else r - bisect.bisect_left(to_bis, r): v + for r, v in self.MT.row_options.items() + if r not in to_del + } + self.RI.cell_options = { + r if not bisect.bisect_left(to_bis, r) else r - bisect.bisect_left(to_bis, r): v + for r, v in self.RI.cell_options.items() + if r not in to_del + } + self.set_refresh_timer(redraw) + + def insert_row_position(self, idx="end", height=None, deselect_all=False, redraw=False): + self.MT.insert_row_position(idx=idx, height=height, deselect_all=deselect_all) + self.set_refresh_timer(redraw) + + def insert_row_positions(self, idx="end", heights=None, deselect_all=False, redraw=False): + self.MT.insert_row_positions(idx=idx, heights=heights, deselect_all=deselect_all) + self.set_refresh_timer(redraw) + + def total_rows(self, number=None, mod_positions=True, mod_data=True): + if number is None: + return int(self.MT.total_data_rows()) + if not isinstance(number, int) or number < 0: + raise ValueError("number argument must be integer and > 0") + if number > len(self.MT.data): + if mod_positions: + height = self.MT.get_lines_cell_height(int(self.MT.default_row_height[0])) + for r in range(number - len(self.MT.data)): + self.MT.insert_row_position("end", height) + elif number < len(self.MT.data): + if not self.MT.all_rows_displayed: + self.MT.display_rows(enable=False, reset_row_positions=False, deselect_all=True) + self.MT.row_positions[number + 1 :] = [] + if mod_data: + self.MT.data_dimensions(total_rows=number) + + def total_columns(self, number=None, mod_positions=True, mod_data=True): + total_cols = self.MT.total_data_cols() + if number is None: + return int(total_cols) + if not isinstance(number, int) or number < 0: + raise ValueError("number argument must be integer and > 0") + if number > total_cols: + if mod_positions: + width = self.MT.default_column_width + for c in range(number - total_cols): + self.MT.insert_col_position("end", width) + elif number < total_cols: + if not self.MT.all_columns_displayed: + self.MT.display_columns(enable=False, reset_col_positions=False, deselect_all=True) + self.MT.col_positions[number + 1 :] = [] + if mod_data: + self.MT.data_dimensions(total_columns=number) + + def sheet_display_dimensions(self, total_rows=None, total_columns=None): + if total_rows is None and total_columns is None: + return len(self.MT.row_positions) - 1, len(self.MT.col_positions) - 1 + if total_rows is not None: + height = self.MT.get_lines_cell_height(int(self.MT.default_row_height[0])) + self.MT.row_positions = list(accumulate(chain([0], (height for row in range(total_rows))))) + if total_columns is not None: + width = self.MT.default_column_width + self.MT.col_positions = list(accumulate(chain([0], (width for column in range(total_columns))))) + + def set_sheet_data_and_display_dimensions(self, total_rows=None, total_columns=None): + self.sheet_display_dimensions(total_rows=total_rows, total_columns=total_columns) + self.MT.data_dimensions(total_rows=total_rows, total_columns=total_columns) + + def move_row_position(self, row: int, moveto: int): + self.MT.move_row_position(row, moveto) + + def move_row(self, row: int, moveto: int): + self.move_rows(moveto, row, 1) + + def delete_column_position(self, idx: int, deselect_all=False): + self.MT.del_col_position(idx, deselect_all=deselect_all) + + def delete_column(self, idx=0, deselect_all=False, redraw=True): + self.delete_columns(columns={idx}, deselect_all=deselect_all, redraw=False) + self.set_refresh_timer(redraw) + + def delete_columns(self, columns: Set = set(), deselect_all=False, redraw=True): + if deselect_all: + self.deselect("all", redraw=False) + if isinstance(columns, set): + to_del = columns + else: + to_del = set(columns) + if not to_del: + return + self.MT.data[:] = [[e for c, e in enumerate(r) if c not in to_del] for r in self.MT.data] + to_bis = sorted(to_del) + if self.MT.all_columns_displayed: + self.set_column_widths( + column_widths=( + w + for c, w in enumerate( + int(b - a) + for a, b in zip( + self.MT.col_positions, + islice(self.MT.col_positions, 1, len(self.MT.col_positions)), + ) + ) + if c not in to_del + ) + ) + else: + dispset = set(self.MT.displayed_columns) + widths_to_del = {i for i, c in enumerate(to_bis) if c in dispset} + if widths_to_del: + self.set_column_widths( + column_widths=( + w + for c, w in enumerate( + int(b - a) + for a, b in zip( + self.MT.col_positions, + islice(self.MT.col_positions, 1, len(self.MT.col_positions)), + ) + ) + if c not in widths_to_del + ) + ) + self.MT.displayed_columns = [ + c if not bisect.bisect_left(to_bis, c) else c - bisect.bisect_left(to_bis, c) + for c in self.MT.displayed_columns + if c not in to_del + ] + self.MT.cell_options = { + ( + r, + c if not bisect.bisect_left(to_bis, c) else c - bisect.bisect_left(to_bis, c), + ): v + for (r, c), v in self.MT.cell_options.items() + if c not in to_del + } + self.MT.col_options = { + c if not bisect.bisect_left(to_bis, c) else c - bisect.bisect_left(to_bis, c): v + for c, v in self.MT.col_options.items() + if c not in to_del + } + self.CH.cell_options = { + c if not bisect.bisect_left(to_bis, c) else c - bisect.bisect_left(to_bis, c): v + for c, v in self.CH.cell_options.items() + if c not in to_del + } + self.set_refresh_timer(redraw) + + def insert_column_position(self, idx="end", width=None, deselect_all=False, redraw=False): + self.MT.insert_col_position(idx=idx, width=width, deselect_all=deselect_all) + self.set_refresh_timer(redraw) + + def insert_column_positions(self, idx="end", widths=None, deselect_all=False, redraw=False): + self.MT.insert_col_positions(idx=idx, widths=widths, deselect_all=deselect_all) + self.set_refresh_timer(redraw) + + def move_column_position(self, column: int, moveto: int): + self.MT.move_col_position(column, moveto) + + def move_column(self, column: int, moveto: int): + self.move_columns(moveto, column, 1) + + def move_columns( + self, + moveto: int, + to_move_min: int, + number_of_columns: int, + move_data: bool = True, + index_type: str = "displayed", + create_selections: bool = True, + redraw=False, + ): + new_selected, dispset = self.MT.move_columns_adjust_options_dict( + moveto, + to_move_min, + number_of_columns, + move_data, + create_selections, + index_type=index_type.lower(), + ) + self.set_refresh_timer(redraw) + return new_selected, dispset + + def move_rows( + self, + moveto: int, + to_move_min: int, + number_of_rows: int, + move_data: bool = True, + index_type: str = "displayed", + create_selections: bool = True, + redraw=False, + ): + new_selected, dispset = self.MT.move_rows_adjust_options_dict( + moveto, + to_move_min, + number_of_rows, + move_data, + create_selections, + index_type=index_type.lower(), + ) + self.set_refresh_timer(redraw) + return new_selected, dispset + + # works on currently selected box + def open_cell(self, ignore_existing_editor=True): + self.MT.open_cell(event=GeneratedMouseEvent(), ignore_existing_editor=ignore_existing_editor) + + def open_header_cell(self, ignore_existing_editor=True): + self.CH.open_cell(event=GeneratedMouseEvent(), ignore_existing_editor=ignore_existing_editor) + + def open_index_cell(self, ignore_existing_editor=True): + self.RI.open_cell(event=GeneratedMouseEvent(), ignore_existing_editor=ignore_existing_editor) + + def set_text_editor_value(self, text="", r=None, c=None): + if self.MT.text_editor is not None and r is None and c is None: + self.MT.text_editor.set_text(text) + elif self.MT.text_editor is not None and self.MT.text_editor_loc == (r, c): + self.MT.text_editor.set_text(text) + + def bind_text_editor_set(self, func, row, column): + self.MT.bind_text_editor_destroy(func, row, column) + + def destroy_text_editor(self, event=None): + self.MT.destroy_text_editor(event=event) + + def get_text_editor_widget(self, event=None): + try: + return self.MT.text_editor.textedit + except Exception: + return None + + def bind_key_text_editor(self, key: str, function): + self.MT.text_editor_user_bound_keys[key] = function + + def unbind_key_text_editor(self, key: str): + if key == "all": + for key in self.MT.text_editor_user_bound_keys: + try: + self.MT.text_editor.textedit.unbind(key) + except Exception: + pass + self.MT.text_editor_user_bound_keys = {} + else: + if key in self.MT.text_editor_user_bound_keys: + del self.MT.text_editor_user_bound_keys[key] + try: + self.MT.text_editor.textedit.unbind(key) + except Exception: + pass + + def get_xview(self): + return self.MT.xview() + + def get_yview(self): + return self.MT.yview() + + def set_xview(self, position, option="moveto"): + self.MT.set_xviews(option, position) + + def set_yview(self, position, option="moveto"): + self.MT.set_yviews(option, position) + + def set_view(self, x_args, y_args): + self.MT.set_view(x_args, y_args) + + def see( + self, + row=0, + column=0, + keep_yscroll=False, + keep_xscroll=False, + bottom_right_corner=False, + check_cell_visibility=True, + redraw=True, + ): + self.MT.see( + row, + column, + keep_yscroll, + keep_xscroll, + bottom_right_corner, + check_cell_visibility=check_cell_visibility, + redraw=False, + ) + self.set_refresh_timer(redraw) + + def select_row(self, row, redraw=True): + self.RI.select_row(int(row) if not isinstance(row, int) else row, redraw=False) + self.set_refresh_timer(redraw) + + def select_column(self, column, redraw=True): + self.CH.select_col(int(column) if not isinstance(column, int) else column, redraw=False) + self.set_refresh_timer(redraw) + + def select_cell(self, row, column, redraw=True): + self.MT.select_cell( + int(row) if not isinstance(row, int) else row, + int(column) if not isinstance(column, int) else column, + redraw=False, + ) + self.set_refresh_timer(redraw) + + def select_all(self, redraw=True, run_binding_func=True): + self.MT.select_all(redraw=False, run_binding_func=run_binding_func) + self.set_refresh_timer(redraw) + + def move_down(self): + self.MT.move_down() + + def add_cell_selection(self, row, column, redraw=True, run_binding_func=True, set_as_current=True): + self.MT.add_selection( + r=row, + c=column, + redraw=False, + run_binding_func=run_binding_func, + set_as_current=set_as_current, + ) + self.set_refresh_timer(redraw) + + def add_row_selection(self, row, redraw=True, run_binding_func=True, set_as_current=True): + self.RI.add_selection( + r=row, + redraw=False, + run_binding_func=run_binding_func, + set_as_current=set_as_current, + ) + self.set_refresh_timer(redraw) + + def add_column_selection(self, column, redraw=True, run_binding_func=True, set_as_current=True): + self.CH.add_selection( + c=column, + redraw=False, + run_binding_func=run_binding_func, + set_as_current=set_as_current, + ) + self.set_refresh_timer(redraw) + + def toggle_select_cell( + self, + row, + column, + add_selection=True, + redraw=True, + run_binding_func=True, + set_as_current=True, + ): + self.MT.toggle_select_cell( + row=row, + column=column, + add_selection=add_selection, + redraw=False, + run_binding_func=run_binding_func, + set_as_current=set_as_current, + ) + self.set_refresh_timer(redraw) + + def toggle_select_row( + self, + row, + add_selection=True, + redraw=True, + run_binding_func=True, + set_as_current=True, + ): + self.RI.toggle_select_row( + row=row, + add_selection=add_selection, + redraw=False, + run_binding_func=run_binding_func, + set_as_current=set_as_current, + ) + self.set_refresh_timer(redraw) + + def toggle_select_column( + self, + column, + add_selection=True, + redraw=True, + run_binding_func=True, + set_as_current=True, + ): + self.CH.toggle_select_col( + column=column, + add_selection=add_selection, + redraw=False, + run_binding_func=run_binding_func, + set_as_current=set_as_current, + ) + self.set_refresh_timer(redraw) + + def deselect(self, row=None, column=None, cell=None, redraw=True): + self.MT.deselect(r=row, c=column, cell=cell, redraw=False) + + # (row, column, type_) e.g. (0, 0, "column") as a named tuple + def get_currently_selected(self): + return self.MT.currently_selected() + + def set_currently_selected(self, row, column, type_="cell", selection_binding=True): + self.MT.set_currently_selected( + r=row, + c=column, + type_=type_, + inside=True if self.MT.cell_selected(row, column) else False, + ) + if selection_binding and self.MT.selection_binding_func is not None: + self.MT.selection_binding_func(SelectCellEvent("select_cell", row, column)) + + def get_selected_rows(self, get_cells=False, get_cells_as_rows=False, return_tuple=False): + if return_tuple: + return tuple(self.MT.get_selected_rows(get_cells=get_cells, get_cells_as_rows=get_cells_as_rows)) + else: + return self.MT.get_selected_rows(get_cells=get_cells, get_cells_as_rows=get_cells_as_rows) + + def get_selected_columns(self, get_cells=False, get_cells_as_columns=False, return_tuple=False): + if return_tuple: + return tuple(self.MT.get_selected_cols(get_cells=get_cells, get_cells_as_cols=get_cells_as_columns)) + else: + return self.MT.get_selected_cols(get_cells=get_cells, get_cells_as_cols=get_cells_as_columns) + + def get_selected_cells(self, get_rows=False, get_columns=False, sort_by_row=False, sort_by_column=False): + if sort_by_row and sort_by_column: + sels = sorted( + self.MT.get_selected_cells(get_rows=get_rows, get_cols=get_columns), + key=lambda t: t[1], + ) + return sorted(sels, key=lambda t: t[0]) + elif sort_by_row: + return sorted( + self.MT.get_selected_cells(get_rows=get_rows, get_cols=get_columns), + key=lambda t: t[0], + ) + elif sort_by_column: + return sorted( + self.MT.get_selected_cells(get_rows=get_rows, get_cols=get_columns), + key=lambda t: t[1], + ) + else: + return self.MT.get_selected_cells(get_rows=get_rows, get_cols=get_columns) + + def get_all_selection_boxes(self): + return self.MT.get_all_selection_boxes() + + def get_all_selection_boxes_with_types(self): + return self.MT.get_all_selection_boxes_with_types() + + def create_selection_box(self, r1, c1, r2, c2, type_="cells"): + return self.MT.create_selected(r1=r1, c1=c1, r2=r2, c2=c2, type_="columns" if type_ == "cols" else type_) + + def recreate_all_selection_boxes(self): + self.MT.recreate_all_selection_boxes() + + def cell_visible(self, r, c): + return self.MT.cell_visible(r, c) + + def cell_completely_visible(self, r, c, seperate_axes=False): + return self.MT.cell_completely_visible(r, c, seperate_axes) + + def cell_selected(self, r, c): + return self.MT.cell_selected(r, c) + + def row_selected(self, r): + return self.MT.row_selected(r) + + def column_selected(self, c): + return self.MT.col_selected(c) + + def anything_selected(self, exclude_columns=False, exclude_rows=False, exclude_cells=False): + if self.MT.anything_selected( + exclude_columns=exclude_columns, + exclude_rows=exclude_rows, + exclude_cells=exclude_cells, + ): + return True + return False + + def all_selected(self): + return self.MT.all_selected() + + def readonly_rows(self, rows=[], readonly=True, redraw=False): + if isinstance(rows, int): + rows_ = [rows] + else: + rows_ = rows + if not readonly: + for r in rows_: + if r in self.MT.row_options and "readonly" in self.MT.row_options[r]: + del self.MT.row_options[r]["readonly"] + else: + for r in rows_: + if r not in self.MT.row_options: + self.MT.row_options[r] = {} + self.MT.row_options[r]["readonly"] = True + self.set_refresh_timer(redraw) + + def readonly_columns(self, columns=[], readonly=True, redraw=False): + if isinstance(columns, int): + cols_ = [columns] + else: + cols_ = columns + if not readonly: + for c in cols_: + if c in self.MT.col_options and "readonly" in self.MT.col_options[c]: + del self.MT.col_options[c]["readonly"] + else: + for c in cols_: + if c not in self.MT.col_options: + self.MT.col_options[c] = {} + self.MT.col_options[c]["readonly"] = True + self.set_refresh_timer(redraw) + + def readonly_cells(self, row=0, column=0, cells=[], readonly=True, redraw=False): + if not readonly: + if cells: + for r, c in cells: + if (r, c) in self.MT.cell_options and "readonly" in self.MT.cell_options[(r, c)]: + del self.MT.cell_options[(r, c)]["readonly"] + else: + if ( + row, + column, + ) in self.MT.cell_options and "readonly" in self.MT.cell_options[(row, column)]: + del self.MT.cell_options[(row, column)]["readonly"] + else: + if cells: + for r, c in cells: + if (r, c) not in self.MT.cell_options: + self.MT.cell_options[(r, c)] = {} + self.MT.cell_options[(r, c)]["readonly"] = True + else: + if (row, column) not in self.MT.cell_options: + self.MT.cell_options[(row, column)] = {} + self.MT.cell_options[(row, column)]["readonly"] = True + self.set_refresh_timer(redraw) + + def readonly_header(self, columns=[], readonly=True, redraw=False): + self.CH.readonly_header(columns=columns, readonly=readonly) + self.set_refresh_timer(redraw) + + def readonly_index(self, rows=[], readonly=True, redraw=False): + self.RI.readonly_index(rows=rows, readonly=readonly) + self.set_refresh_timer(redraw) + + def dehighlight_all(self, redraw=True): + for k in self.MT.cell_options: + if "highlight" in self.MT.cell_options[k]: + del self.MT.cell_options[k]["highlight"] + for k in self.MT.row_options: + if "highlight" in self.MT.row_options[k]: + del self.MT.row_options[k]["highlight"] + for k in self.MT.col_options: + if "highlight" in self.MT.col_options[k]: + del self.MT.col_options[k]["highlight"] + for k in self.RI.cell_options: + if "highlight" in self.RI.cell_options[k]: + del self.RI.cell_options[k]["highlight"] + for k in self.CH.cell_options: + if "highlight" in self.CH.cell_options[k]: + del self.CH.cell_options[k]["highlight"] + self.set_refresh_timer(redraw) + + def dehighlight_rows(self, rows=[], redraw=True): + if isinstance(rows, int): + rows_ = [rows] + else: + rows_ = rows + if not rows_ or rows_ == "all": + for r in self.MT.row_options: + if "highlight" in self.MT.row_options[r]: + del self.MT.row_options[r]["highlight"] + + for r in self.RI.cell_options: + if "highlight" in self.RI.cell_options[r]: + del self.RI.cell_options[r]["highlight"] + else: + for r in rows_: + try: + del self.MT.row_options[r]["highlight"] + except Exception: + pass + try: + del self.RI.cell_options[r]["highlight"] + except Exception: + pass + self.set_refresh_timer(redraw) + + def dehighlight_columns(self, columns=[], redraw=True): + if isinstance(columns, int): + columns_ = [columns] + else: + columns_ = columns + if not columns_ or columns_ == "all": + for c in self.MT.col_options: + if "highlight" in self.MT.col_options[c]: + del self.MT.col_options[c]["highlight"] + + for c in self.CH.cell_options: + if "highlight" in self.CH.cell_options[c]: + del self.CH.cell_options[c]["highlight"] + else: + for c in columns_: + try: + del self.MT.col_options[c]["highlight"] + except Exception: + pass + try: + del self.CH.cell_options[c]["highlight"] + except Exception: + pass + self.set_refresh_timer(redraw) + + def highlight_rows( + self, + rows=[], + bg=None, + fg=None, + highlight_index=True, + redraw=True, + end_of_screen=False, + overwrite=True, + ): + if bg is None and fg is None: + return + for r in (rows,) if isinstance(rows, int) else rows: + if r not in self.MT.row_options: + self.MT.row_options[r] = {} + if "highlight" in self.MT.row_options[r] and not overwrite: + self.MT.row_options[r]["highlight"] = ( + self.MT.row_options[r]["highlight"][0] if bg is None else bg, + self.MT.row_options[r]["highlight"][1] if fg is None else fg, + self.MT.row_options[r]["highlight"][2] + if self.MT.row_options[r]["highlight"][2] != end_of_screen + else end_of_screen, + ) + else: + self.MT.row_options[r]["highlight"] = (bg, fg, end_of_screen) + if highlight_index: + self.highlight_cells(cells=rows, canvas="index", bg=bg, fg=fg, redraw=False) + self.set_refresh_timer(redraw) + + def highlight_columns( + self, + columns=[], + bg=None, + fg=None, + highlight_header=True, + redraw=True, + overwrite=True, + ): + if bg is None and fg is None: + return + for c in (columns,) if isinstance(columns, int) else columns: + if c not in self.MT.col_options: + self.MT.col_options[c] = {} + if "highlight" in self.MT.col_options[c] and not overwrite: + self.MT.col_options[c]["highlight"] = ( + self.MT.col_options[c]["highlight"][0] if bg is None else bg, + self.MT.col_options[c]["highlight"][1] if fg is None else fg, + ) + else: + self.MT.col_options[c]["highlight"] = (bg, fg) + if highlight_header: + self.highlight_cells(cells=columns, canvas="header", bg=bg, fg=fg, redraw=False) + self.set_refresh_timer(redraw) + + def highlight_cells( + self, + row=0, + column=0, + cells=[], + canvas="table", + bg=None, + fg=None, + redraw=True, + overwrite=True, + ): + if bg is None and fg is None: + return + if canvas == "table": + if cells: + for r_, c_ in cells: + if (r_, c_) not in self.MT.cell_options: + self.MT.cell_options[(r_, c_)] = {} + if "highlight" in self.MT.cell_options[(r_, c_)] and not overwrite: + self.MT.cell_options[(r_, c_)]["highlight"] = ( + self.MT.cell_options[(r_, c_)]["highlight"][0] if bg is None else bg, + self.MT.cell_options[(r_, c_)]["highlight"][1] if fg is None else fg, + ) + else: + self.MT.cell_options[(r_, c_)]["highlight"] = (bg, fg) + else: + if isinstance(row, str) and row.lower() == "all" and isinstance(column, int): + riter = range(self.MT.total_data_rows()) + citer = (column,) + elif isinstance(column, str) and column.lower() == "all" and isinstance(row, int): + riter = (row,) + citer = range(self.MT.total_data_cols()) + elif isinstance(row, int) and isinstance(column, int): + riter = (row,) + citer = (column,) + for r_ in riter: + for c_ in citer: + if (r_, c_) not in self.MT.cell_options: + self.MT.cell_options[(r_, c_)] = {} + if "highlight" in self.MT.cell_options[(r_, c_)] and not overwrite: + self.MT.cell_options[(r_, c_)]["highlight"] = ( + self.MT.cell_options[(r_, c_)]["highlight"][0] if bg is None else bg, + self.MT.cell_options[(r_, c_)]["highlight"][1] if fg is None else fg, + ) + else: + self.MT.cell_options[(r_, c_)]["highlight"] = (bg, fg) + elif canvas in ("row_index", "index"): + if bg is None and fg is None: + return + iterable = ( + cells if (cells and not isinstance(cells, int)) else (cells,) if isinstance(cells, int) else (row,) + ) + for r_ in iterable: + if r_ not in self.RI.cell_options: + self.RI.cell_options[r_] = {} + if "highlight" in self.RI.cell_options[r_] and not overwrite: + self.RI.cell_options[r_]["highlight"] = ( + self.RI.cell_options[r_]["highlight"][0] if bg is None else bg, + self.RI.cell_options[r_]["highlight"][1] if fg is None else fg, + ) + else: + self.RI.cell_options[r_]["highlight"] = (bg, fg) + elif canvas == "header": + if bg is None and fg is None: + return + iterable = ( + cells if (cells and not isinstance(cells, int)) else (cells,) if isinstance(cells, int) else (column,) + ) + for c_ in iterable: + if c_ not in self.CH.cell_options: + self.CH.cell_options[c_] = {} + if "highlight" in self.CH.cell_options[c_] and not overwrite: + self.CH.cell_options[c_]["highlight"] = ( + self.CH.cell_options[c_]["highlight"][0] if bg is None else bg, + self.CH.cell_options[c_]["highlight"][1] if fg is None else fg, + ) + else: + self.CH.cell_options[c_]["highlight"] = (bg, fg) + self.set_refresh_timer(redraw) + + def dehighlight_cells(self, row=0, column=0, cells=[], canvas="table", all_=False, redraw=True): + if row == "all" and canvas == "table": + for k, v in self.MT.cell_options.items(): + if "highlight" in v: + del self.MT.cell_options[k]["highlight"] + elif row == "all" and canvas == "row_index": + for k, v in self.RI.cell_options.items(): + if "highlight" in v: + del self.RI.cell_options[k]["highlight"] + elif row == "all" and canvas == "header": + for k, v in self.CH.cell_options.items(): + if "highlight" in v: + del self.CH.cell_options[k]["highlight"] + if canvas == "table": + if cells and not all_: + for t in cells: + try: + del self.MT.cell_options[t]["highlight"] + except Exception: + pass + elif not all_: + if ( + row, + column, + ) in self.MT.cell_options and "highlight" in self.MT.cell_options[(row, column)]: + del self.MT.cell_options[(row, column)]["highlight"] + elif all_: + for k in self.MT.cell_options: + if "highlight" in self.MT.cell_options[k]: + del self.MT.cell_options[k]["highlight"] + elif canvas == "row_index": + if cells and not all_: + for r in cells: + try: + del self.RI.cell_options[r]["highlight"] + except Exception: + pass + elif not all_: + if row in self.RI.cell_options and "highlight" in self.RI.cell_options[row]: + del self.RI.cell_options[row]["highlight"] + elif all_: + for r in self.RI.cell_options: + if "highlight" in self.RI.cell_options[r]: + del self.RI.cell_options[r]["highlight"] + elif canvas == "header": + if cells and not all_: + for c in cells: + try: + del self.CH.cell_options[c]["highlight"] + except Exception: + pass + elif not all_: + if column in self.CH.cell_options and "highlight" in self.CH.cell_options[column]: + del self.CH.cell_options[column]["highlight"] + elif all_: + for c in self.CH.cell_options: + if "highlight" in self.CH.cell_options[c]: + del self.CH.cell_options[c]["highlight"] + self.set_refresh_timer(redraw) + + def delete_out_of_bounds_options(self): + maxc = self.total_columns() + maxr = self.total_rows() + self.MT.cell_options = {k: v for k, v in self.MT.cell_options.items() if k[0] < maxr and k[1] < maxc} + self.RI.cell_options = {k: v for k, v in self.RI.cell_options.items() if k < maxr} + self.CH.cell_options = {k: v for k, v in self.CH.cell_options.items() if k < maxc} + self.MT.col_options = {k: v for k, v in self.MT.col_options.items() if k < maxc} + self.MT.row_options = {k: v for k, v in self.MT.row_options.items() if k < maxr} + + def reset_all_options(self): + self.MT.cell_options = {} + self.RI.cell_options = {} + self.CH.cell_options = {} + self.MT.col_options = {} + self.MT.row_options = {} + + def get_cell_options(self, canvas="table"): + if canvas == "table": + return self.MT.cell_options + elif canvas == "row_index": + return self.RI.cell_options + elif canvas == "header": + return self.CH.cell_options + + def get_highlighted_cells(self, canvas="table"): + if canvas == "table": + return {k: v["highlight"] for k, v in self.MT.cell_options.items() if "highlight" in v} + elif canvas == "row_index": + return {k: v["highlight"] for k, v in self.RI.cell_options.items() if "highlight" in v} + elif canvas == "header": + return {k: v["highlight"] for k, v in self.CH.cell_options.items() if "highlight" in v} + + def get_frame_y(self, y: int): + return y + self.CH.current_height + + def get_frame_x(self, x: int): + return x + self.RI.current_width + + def convert_align(self, align: str): + a = align.lower() + if a in ("c", "center", "centre"): + return "center" + elif a in ("w", "west", "left"): + return "w" + elif a in ("e", "east", "right"): + return "e" + raise ValueError("Align must be one of the following values: c, center, w, west, left, e, east, right") + + def get_cell_alignments(self): + return {(r, c): v["align"] for (r, c), v in self.MT.cell_options.items() if "align" in v} + + def get_column_alignments(self): + return {c: v["align"] for c, v in self.MT.col_options.items() if "align" in v} + + def get_row_alignments(self): + return {r: v["align"] for r, v in self.MT.row_options.items() if "align" in v} + + def align_rows(self, rows=[], align="global", align_index=False, redraw=True): # "center", "w", "e" or "global" + if align == "global" or self.convert_align(align): + if isinstance(rows, dict): + for k, v in rows.items(): + self.MT.align_rows(rows=k, align=v, align_index=align_index) + else: + self.MT.align_rows( + rows=rows, + align=align if align == "global" else self.convert_align(align), + align_index=align_index, + ) + self.set_refresh_timer(redraw) + + def align_columns( + self, columns=[], align="global", align_header=False, redraw=True + ): # "center", "w", "e" or "global" + if align == "global" or self.convert_align(align): + if isinstance(columns, dict): + for k, v in columns.items(): + self.MT.align_columns(columns=k, align=v, align_header=align_header) + else: + self.MT.align_columns( + columns=columns, + align=align if align == "global" else self.convert_align(align), + align_header=align_header, + ) + self.set_refresh_timer(redraw) + + def align_cells(self, row=0, column=0, cells=[], align="global", redraw=True): # "center", "w", "e" or "global" + if align == "global" or self.convert_align(align): + if isinstance(cells, dict): + for (r, c), v in cells.items(): + self.MT.align_cells(row=r, column=c, cells=[], align=v) + else: + self.MT.align_cells( + row=row, + column=column, + cells=cells, + align=align if align == "global" else self.convert_align(align), + ) + self.set_refresh_timer(redraw) + + def align_header(self, columns=[], align="global", redraw=True): + if align == "global" or self.convert_align(align): + if isinstance(columns, dict): + for k, v in columns.items(): + self.CH.align_cells(columns=k, align=v) + else: + self.CH.align_cells( + columns=columns, + align=align if align == "global" else self.convert_align(align), + ) + self.set_refresh_timer(redraw) + + def align_index(self, rows=[], align="global", redraw=True): + if align == "global" or self.convert_align(align): + if isinstance(rows, dict): + for k, v in rows.items(): + self.RI.align_cells(rows=rows, align=v) + else: + self.RI.align_cells( + rows=rows, + align=align if align == "global" else self.convert_align(align), + ) + self.set_refresh_timer(redraw) + + def align(self, align: str = None, redraw=True): + if align is None: + return self.MT.align + elif self.convert_align(align): + self.MT.align = self.convert_align(align) + else: + raise ValueError("Align must be one of the following values: c, center, w, west, e, east") + self.set_refresh_timer(redraw) + + def header_align(self, align: str = None, redraw=True): + if align is None: + return self.CH.align + elif self.convert_align(align): + self.CH.align = self.convert_align(align) + else: + raise ValueError("Align must be one of the following values: c, center, w, west, e, east") + self.set_refresh_timer(redraw) + + def row_index_align(self, align: str = None, redraw=True): + if align is None: + return self.RI.align + elif self.convert_align(align): + self.RI.align = self.convert_align(align) + else: + raise ValueError("Align must be one of the following values: c, center, w, west, e, east") + self.set_refresh_timer(redraw) + + def font(self, newfont=None, reset_row_positions=True): + return self.MT.set_table_font(newfont, reset_row_positions=reset_row_positions) + + def header_font(self, newfont=None): + return self.MT.set_header_font(newfont) + + def set_options(self, redraw=True, **kwargs): + if "set_cell_sizes_on_zoom" in kwargs: + self.MT.set_cell_sizes_on_zoom = kwargs["set_cell_sizes_on_zoom"] + if "auto_resize_columns" in kwargs: + self.MT.auto_resize_columns = kwargs["auto_resize_columns"] + if "auto_resize_rows" in kwargs: + self.MT.auto_resize_rows = kwargs["auto_resize_rows"] + if "to_clipboard_delimiter" in kwargs: + self.MT.to_clipboard_delimiter = kwargs["to_clipboard_delimiter"] + if "to_clipboard_quotechar" in kwargs: + self.MT.to_clipboard_quotechar = kwargs["to_clipboard_quotechar"] + if "to_clipboard_lineterminator" in kwargs: + self.MT.to_clipboard_lineterminator = kwargs["to_clipboard_lineterminator"] + if "from_clipboard_delimiters" in kwargs: + self.MT.from_clipboard_delimiters = kwargs["from_clipboard_delimiters"] + if "show_dropdown_borders" in kwargs: + self.MT.show_dropdown_borders = kwargs["show_dropdown_borders"] + if "edit_cell_validation" in kwargs: + self.MT.edit_cell_validation = kwargs["edit_cell_validation"] + if "show_default_header_for_empty" in kwargs: + self.CH.show_default_header_for_empty = kwargs["show_default_header_for_empty"] + if "show_default_index_for_empty" in kwargs: + self.RI.show_default_index_for_empty = kwargs["show_default_index_for_empty"] + if "selected_rows_to_end_of_window" in kwargs: + self.MT.selected_rows_to_end_of_window = kwargs["selected_rows_to_end_of_window"] + if "horizontal_grid_to_end_of_window" in kwargs: + self.MT.horizontal_grid_to_end_of_window = kwargs["horizontal_grid_to_end_of_window"] + if "vertical_grid_to_end_of_window" in kwargs: + self.MT.vertical_grid_to_end_of_window = kwargs["vertical_grid_to_end_of_window"] + if "paste_insert_column_limit" in kwargs: + self.MT.paste_insert_column_limit = kwargs["paste_insert_column_limit"] + if "paste_insert_row_limit" in kwargs: + self.MT.paste_insert_row_limit = kwargs["paste_insert_row_limit"] + if "expand_sheet_if_paste_too_big" in kwargs: + self.MT.expand_sheet_if_paste_too_big = kwargs["expand_sheet_if_paste_too_big"] + if "arrow_key_down_right_scroll_page" in kwargs: + self.MT.arrow_key_down_right_scroll_page = kwargs["arrow_key_down_right_scroll_page"] + if "enable_edit_cell_auto_resize" in kwargs: + self.MT.cell_auto_resize_enabled = kwargs["enable_edit_cell_auto_resize"] + if "header_hidden_columns_expander_bg" in kwargs: + self.CH.header_hidden_columns_expander_bg = kwargs["header_hidden_columns_expander_bg"] + if "index_hidden_rows_expander_bg" in kwargs: + self.RI.index_hidden_rows_expander_bg = kwargs["index_hidden_rows_expander_bg"] + if "page_up_down_select_row" in kwargs: + self.MT.page_up_down_select_row = kwargs["page_up_down_select_row"] + if "display_selected_fg_over_highlights" in kwargs: + self.MT.display_selected_fg_over_highlights = kwargs["display_selected_fg_over_highlights"] + if "show_horizontal_grid" in kwargs: + self.MT.show_horizontal_grid = kwargs["show_horizontal_grid"] + if "show_vertical_grid" in kwargs: + self.MT.show_vertical_grid = kwargs["show_vertical_grid"] + if "empty_horizontal" in kwargs: + self.MT.empty_horizontal = kwargs["empty_horizontal"] + if "empty_vertical" in kwargs: + self.MT.empty_vertical = kwargs["empty_vertical"] + if "row_height" in kwargs: + self.MT.default_row_height = ( + kwargs["row_height"] if isinstance(kwargs["row_height"], str) else "pixels", + kwargs["row_height"] + if isinstance(kwargs["row_height"], int) + else self.MT.get_lines_cell_height(int(kwargs["row_height"])), + ) + if "column_width" in kwargs: + self.MT.default_column_width = ( + self.MT.min_column_width + 20 + if kwargs["column_width"] < self.MT.min_column_width + else int(kwargs["column_width"]) + ) + if "header_height" in kwargs: + self.MT.default_header_height = ( + kwargs["header_height"] if isinstance(kwargs["header_height"], str) else "pixels", + kwargs["header_height"] + if isinstance(kwargs["header_height"], int) + else self.MT.get_lines_cell_height(int(kwargs["header_height"]), font=self.MT.header_font), + ) + if "row_drag_and_drop_perform" in kwargs: + self.RI.row_drag_and_drop_perform = kwargs["row_drag_and_drop_perform"] + if "column_drag_and_drop_perform" in kwargs: + self.CH.column_drag_and_drop_perform = kwargs["column_drag_and_drop_perform"] + if "popup_menu_font" in kwargs: + self.MT.popup_menu_font = kwargs["popup_menu_font"] + if "popup_menu_fg" in kwargs: + self.MT.popup_menu_fg = kwargs["popup_menu_fg"] + if "popup_menu_bg" in kwargs: + self.MT.popup_menu_bg = kwargs["popup_menu_bg"] + if "popup_menu_highlight_bg" in kwargs: + self.MT.popup_menu_highlight_bg = kwargs["popup_menu_highlight_bg"] + if "popup_menu_highlight_fg" in kwargs: + self.MT.popup_menu_highlight_fg = kwargs["popup_menu_highlight_fg"] + if "top_left_fg_highlight" in kwargs: + self.TL.top_left_fg_highlight = kwargs["top_left_fg_highlight"] + if "auto_resize_default_row_index" in kwargs: + self.RI.auto_resize_width = kwargs["auto_resize_default_row_index"] + if "header_selected_columns_bg" in kwargs: + self.CH.header_selected_columns_bg = kwargs["header_selected_columns_bg"] + if "header_selected_columns_fg" in kwargs: + self.CH.header_selected_columns_fg = kwargs["header_selected_columns_fg"] + if "index_selected_rows_bg" in kwargs: + self.RI.index_selected_rows_bg = kwargs["index_selected_rows_bg"] + if "index_selected_rows_fg" in kwargs: + self.RI.index_selected_rows_fg = kwargs["index_selected_rows_fg"] + if "table_selected_rows_border_fg" in kwargs: + self.MT.table_selected_rows_border_fg = kwargs["table_selected_rows_border_fg"] + if "table_selected_rows_bg" in kwargs: + self.MT.table_selected_rows_bg = kwargs["table_selected_rows_bg"] + if "table_selected_rows_fg" in kwargs: + self.MT.table_selected_rows_fg = kwargs["table_selected_rows_fg"] + if "table_selected_columns_border_fg" in kwargs: + self.MT.table_selected_columns_border_fg = kwargs["table_selected_columns_border_fg"] + if "table_selected_columns_bg" in kwargs: + self.MT.table_selected_columns_bg = kwargs["table_selected_columns_bg"] + if "table_selected_columns_fg" in kwargs: + self.MT.table_selected_columns_fg = kwargs["table_selected_columns_fg"] + if "default_header" in kwargs: + self.CH.default_header = kwargs["default_header"].lower() + if "default_row_index" in kwargs: + self.RI.default_index = kwargs["default_row_index"].lower() + if "max_column_width" in kwargs: + self.MT.max_column_width = float(kwargs["max_column_width"]) + if "max_row_height" in kwargs: + self.MT.max_row_height = float(kwargs["max_row_height"]) + if "max_header_height" in kwargs: + self.MT.max_header_height = float(kwargs["max_header_height"]) + if "max_index_width" in kwargs: + self.MT.max_index_width = float(kwargs["max_index_width"]) + if "font" in kwargs: + self.MT.set_table_font(kwargs["font"]) + if "header_font" in kwargs: + self.MT.set_header_font(kwargs["header_font"]) + if "index_font" in kwargs: + self.MT.set_index_font(kwargs["index_font"]) + if "theme" in kwargs: + self.change_theme(kwargs["theme"]) + if "show_selected_cells_border" in kwargs: + self.MT.show_selected_cells_border = kwargs["show_selected_cells_border"] + if "header_bg" in kwargs: + self.CH.config(background=kwargs["header_bg"]) + self.CH.header_bg = kwargs["header_bg"] + if "header_border_fg" in kwargs: + self.CH.header_border_fg = kwargs["header_border_fg"] + if "header_grid_fg" in kwargs: + self.CH.header_grid_fg = kwargs["header_grid_fg"] + if "header_fg" in kwargs: + self.CH.header_fg = kwargs["header_fg"] + if "header_selected_cells_bg" in kwargs: + self.CH.header_selected_cells_bg = kwargs["header_selected_cells_bg"] + if "header_selected_cells_fg" in kwargs: + self.CH.header_selected_cells_fg = kwargs["header_selected_cells_fg"] + if "index_bg" in kwargs: + self.RI.config(background=kwargs["index_bg"]) + self.RI.index_bg = kwargs["index_bg"] + if "index_border_fg" in kwargs: + self.RI.index_border_fg = kwargs["index_border_fg"] + if "index_grid_fg" in kwargs: + self.RI.index_grid_fg = kwargs["index_grid_fg"] + if "index_fg" in kwargs: + self.RI.index_fg = kwargs["index_fg"] + if "index_selected_cells_bg" in kwargs: + self.RI.index_selected_cells_bg = kwargs["index_selected_cells_bg"] + if "index_selected_cells_fg" in kwargs: + self.RI.index_selected_cells_fg = kwargs["index_selected_cells_fg"] + if "top_left_bg" in kwargs: + self.TL.config(background=kwargs["top_left_bg"]) + if "top_left_fg" in kwargs: + self.TL.top_left_fg = kwargs["top_left_fg"] + self.TL.itemconfig("rw", fill=kwargs["top_left_fg"]) + self.TL.itemconfig("rh", fill=kwargs["top_left_fg"]) + if "frame_bg" in kwargs: + self.config(background=kwargs["frame_bg"]) + if "table_bg" in kwargs: + self.MT.config(background=kwargs["table_bg"]) + self.MT.table_bg = kwargs["table_bg"] + if "table_grid_fg" in kwargs: + self.MT.table_grid_fg = kwargs["table_grid_fg"] + if "table_fg" in kwargs: + self.MT.table_fg = kwargs["table_fg"] + if "table_selected_cells_border_fg" in kwargs: + self.MT.table_selected_cells_border_fg = kwargs["table_selected_cells_border_fg"] + if "table_selected_cells_bg" in kwargs: + self.MT.table_selected_cells_bg = kwargs["table_selected_cells_bg"] + if "table_selected_cells_fg" in kwargs: + self.MT.table_selected_cells_fg = kwargs["table_selected_cells_fg"] + if "resizing_line_fg" in kwargs: + self.CH.resizing_line_fg = kwargs["resizing_line_fg"] + self.RI.resizing_line_fg = kwargs["resizing_line_fg"] + if "drag_and_drop_bg" in kwargs: + self.CH.drag_and_drop_bg = kwargs["drag_and_drop_bg"] + self.RI.drag_and_drop_bg = kwargs["drag_and_drop_bg"] + if "outline_thickness" in kwargs: + self.config(highlightthickness=kwargs["outline_thickness"]) + if "outline_color" in kwargs: + self.config( + highlightbackground=kwargs["outline_color"], + highlightcolor=kwargs["outline_color"], + ) + self.MT.create_rc_menus() + self.set_refresh_timer(redraw) + + def change_theme(self, theme="light blue", redraw=True): + if theme.lower() in ("light blue", "light_blue"): + self.set_options(**theme_light_blue, redraw=False) + self.config(bg=theme_light_blue["table_bg"]) + elif theme.lower() == "dark": + self.set_options(**theme_dark, redraw=False) + self.config(bg=theme_dark["table_bg"]) + elif theme.lower() in ("light green", "light_green"): + self.set_options(**theme_light_green, redraw=False) + self.config(bg=theme_light_green["table_bg"]) + elif theme.lower() in ("dark blue", "dark_blue"): + self.set_options(**theme_dark_blue, redraw=False) + self.config(bg=theme_dark_blue["table_bg"]) + elif theme.lower() in ("dark green", "dark_green"): + self.set_options(**theme_dark_green, redraw=False) + self.config(bg=theme_dark_green["table_bg"]) + elif theme.lower() == "black": + self.set_options(**theme_black, redraw=False) + self.config(bg=theme_black["table_bg"]) + self.MT.recreate_all_selection_boxes() + self.set_refresh_timer(redraw) + + def get_header_data(self, c, get_displayed=False): + return self.CH.get_cell_data(datacn=c, get_displayed=get_displayed) + + def get_index_data(self, r, get_displayed=False): + return self.RI.get_cell_data(datarn=r, get_displayed=get_displayed) + + def get_sheet_data( + self, + get_displayed=False, + get_header=False, + get_index=False, + get_header_displayed=True, + get_index_displayed=True, + only_rows=None, + only_columns=None, + **kwargs, + ): + if kwargs: + show_kwargs_warning(kwargs, "get_sheet_data") + if only_rows is not None: + if isinstance(only_rows, int): + only_rows = (only_rows,) + elif not is_iterable(only_rows): + raise ValueError(f"Argument 'only_rows' must be either int or iterable or None. Not {type(only_rows)}") + if only_columns is not None: + if isinstance(only_columns, int): + only_columns = (only_columns,) + elif not is_iterable(only_columns): + raise ValueError( + f"Argument 'only_columns' must be either int or iterable or None. Not {type(only_columns)}" + ) + if get_header: + maxlen = len(self.MT._headers) if isinstance(self.MT._headers, (list, tuple)) else 0 + data = [] + for rn in only_rows if only_rows is not None else range(len(self.MT.data)): + r = self.get_row_data(rn, get_displayed=get_displayed, only_columns=only_columns) + if len(r) > maxlen: + maxlen = len(r) + if get_index: + data.append([self.get_index_data(rn, get_displayed=get_index_displayed)] + r) + else: + data.append(r) + iterable = only_columns if only_columns is not None else range(maxlen) + if get_index: + return [[""] + [self.get_header_data(cn, get_displayed=get_header_displayed) for cn in iterable]] + data + else: + return [[self.get_header_data(cn, get_displayed=get_header_displayed) for cn in iterable]] + data + elif not get_header: + iterable = only_rows if only_rows is not None else range(len(self.MT.data)) + return [ + self.get_row_data( + rn, + get_displayed=get_displayed, + get_index=get_index, + get_index_displayed=get_index_displayed, + only_columns=only_columns, + ) + for rn in iterable + ] + + def get_cell_data(self, r, c, get_displayed=False, **kwargs): + if kwargs: + show_kwargs_warning(kwargs, "get_cell_data") + return self.MT.get_cell_data(r, c, get_displayed) + + def get_row_data( + self, + r, + get_displayed=False, + get_index=False, + get_index_displayed=True, + only_columns=None, + **kwargs, + ): + if kwargs: + show_kwargs_warning(kwargs, "get_row_data") + if only_columns is not None: + if isinstance(only_columns, int): + only_columns = (only_columns,) + elif not is_iterable(only_columns): + raise ValueError( + f"Argument 'only_columns' must be either int or iterable or None. Not {type(only_columns)}" + ) + if r >= self.MT.total_data_rows(): + raise IndexError(f"Row #{r} is out of range.") + if r >= len(self.MT.data): + total_data_cols = self.MT.total_data_cols() + self.MT.fix_data_len(r, total_data_cols - 1) + iterable = only_columns if only_columns is not None else range(len(self.MT.data[r])) + if get_index: + return [self.get_index_data(r, get_displayed=get_index_displayed)] + [ + self.MT.get_cell_data(r, c, get_displayed=get_displayed) for c in iterable + ] + else: + return [self.MT.get_cell_data(r, c, get_displayed=get_displayed) for c in iterable] + + def get_column_data( + self, + c, + get_displayed=False, + get_header=False, + get_header_displayed=True, + only_rows=None, + **kwargs, + ): + if kwargs: + show_kwargs_warning(kwargs, "get_column_data") + if only_rows is not None: + if isinstance(only_rows, int): + only_rows = (only_rows,) + elif not is_iterable(only_rows): + raise ValueError(f"Argument 'only_rows' must be either int or iterable or None. Not {type(only_rows)}") + iterable = only_rows if only_rows is not None else range(len(self.MT.data)) + return ([self.get_header_data(c, get_displayed=get_header_displayed)] if get_header else []) + [ + self.MT.get_cell_data(r, c, get_displayed=get_displayed) for r in iterable + ] + + def yield_sheet_rows( + self, + get_displayed=False, + get_header=False, + get_index=False, + get_index_displayed=True, + get_header_displayed=True, + only_rows=None, + only_columns=None, + **kwargs, + ): + if kwargs: + show_kwargs_warning(kwargs, "yield_sheet_rows") + if only_rows is not None: + if isinstance(only_rows, int): + only_rows = (only_rows,) + elif not is_iterable(only_rows): + raise ValueError(f"Argument 'only_rows' must be either int or iterable or None. Not {type(only_rows)}") + if only_columns is not None: + if isinstance(only_columns, int): + only_columns = (only_columns,) + elif not is_iterable(only_columns): + raise ValueError( + f"Argument 'only_columns' must be either int or iterable or None. Not {type(only_columns)}" + ) + if get_header: + maxlen = self.MT.total_data_cols() + iterable = only_columns if only_columns is not None else range(maxlen) + yield ([""] if get_index else []) + [ + self.get_header_data(c, get_displayed=get_header_displayed) for c in iterable + ] + iterable = only_rows if only_rows is not None else range(len(self.MT.data)) + yield from ( + self.get_row_data( + r, + get_displayed=get_displayed, + get_index=get_index, + get_index_displayed=get_index_displayed, + only_columns=only_columns, + ) + for r in iterable + ) + + @property + def data(self): + return self.MT.data + + def formatted(self, r, c): + if (r, c) in self.MT.cell_options and "format" in self.MT.cell_options[(r, c)]: + return True + return False + + def data_reference( + self, + newdataref=None, + reset_col_positions=True, + reset_row_positions=True, + redraw=False, + ): + return self.MT.data_reference(newdataref, reset_col_positions, reset_row_positions, redraw) + + def set_sheet_data( + self, + data=[[]], + reset_col_positions=True, + reset_row_positions=True, + redraw=True, + verify=False, + reset_highlights=False, + keep_formatting=True, + ): + if verify and (not isinstance(data, list) or not all(isinstance(row, list) for row in data)): + raise ValueError("Data argument must be a list of lists, sublists being rows") + if reset_highlights: + self.dehighlight_all() + return self.MT.data_reference( + data, + reset_col_positions, + reset_row_positions, + redraw, + return_id=False, + keep_formatting=keep_formatting, + ) + + def set_cell_data(self, r, c, value="", redraw=False, keep_formatting=True): + if not keep_formatting: + self.MT.delete_cell_format(r, c, clear_values=False) + self.MT.set_cell_data(r, c, value) + if redraw: + self.set_refresh_timer() + + def set_row_data(self, r, values=tuple(), add_columns=True, redraw=False, keep_formatting=True): + if r >= len(self.MT.data): + raise Exception("Row number is out of range") + if not keep_formatting: + self.MT.delete_row_format(r, clear_values=False) + maxidx = len(self.MT.data[r]) - 1 + if not values: + self.MT.data[r][:] = self.MT.get_empty_row_seq(r, len(self.MT.data[r])) + if add_columns: + for c, v in enumerate(values): + if c > maxidx: + self.MT.data[r].append(v) + if self.MT.all_columns_displayed: + self.MT.insert_col_position("end") + else: + self.set_cell_data(r=r, c=c, value=v, redraw=False, keep_formatting=keep_formatting) + else: + for c, v in enumerate(values): + if c > maxidx: + self.MT.data[r].append(v) + else: + self.set_cell_data(r=r, c=c, value=v, redraw=False, keep_formatting=keep_formatting) + self.set_refresh_timer(redraw) + + def set_column_data(self, c, values=tuple(), add_rows=True, redraw=False, keep_formatting=True): + if not keep_formatting: + self.MT.delete_column_format(c, clear_values=False) + if add_rows: + maxidx = len(self.MT.data) - 1 + total_cols = None + height = self.MT.default_row_height[1] + for rn, v in enumerate(values): + if rn > maxidx: + if total_cols is None: + total_cols = self.MT.total_data_cols() + self.MT.fix_data_len(rn, total_cols - 1) + if self.MT.all_rows_displayed: + self.MT.insert_row_position("end", height=height) + maxidx += 1 + if c >= len(self.MT.data[rn]): + self.MT.fix_row_len(rn, c) + self.set_cell_data(r=rn, c=c, value=v, redraw=False, keep_formatting=keep_formatting) + else: + for rn, v in enumerate(values): + if c >= len(self.MT.data[rn]): + self.MT.fix_row_len(rn, c) + self.set_cell_data(r=rn, c=c, value=v, redraw=False, keep_formatting=keep_formatting) + self.set_refresh_timer(redraw) + + def insert_column( + self, + values: Union[List, Tuple, int, None] = None, + idx: Union[str, int] = "end", + width=None, + deselect_all=False, + add_rows=True, + equalize_data_row_lengths=True, + mod_column_positions=True, + redraw=True, + ): + self.insert_columns( + (values,) if isinstance(values, (list, tuple)) else 1 if values is None else values, + idx, + (width,) if isinstance(width, int) else width, + deselect_all, + add_rows, + equalize_data_row_lengths, + mod_column_positions, + redraw, + ) + + def insert_columns( + self, + columns: Union[List, Tuple, int, None] = 1, + idx: Union[str, int] = "end", + widths=None, + deselect_all=False, + add_rows=True, + equalize_data_row_lengths=True, + mod_column_positions=True, + redraw=True, + ): + if equalize_data_row_lengths: + old_total = self.MT.equalize_data_row_lengths() + else: + old_total = self.MT.total_data_cols() + if isinstance(columns, int): + if columns < 1: + raise ValueError(f"columns arg must be greater than 0, not {columns}") + total_rows = self.MT.total_data_rows() + start = old_total if idx == "end" else idx + data = [ + [self.MT.get_value_for_empty_cell(datarn, datacn, c_ops=idx == "end") for datarn in range(total_rows)] + for datacn in range(start, start + columns) + ] + numcols = columns + else: + data = columns + numcols = len(columns) + if self.MT.all_columns_displayed: + if mod_column_positions: + self.MT.insert_col_positions( + idx=idx, + widths=columns if isinstance(columns, int) and widths is None else widths, + deselect_all=deselect_all, + ) + elif not self.MT.all_columns_displayed: + if idx != "end": + self.MT.displayed_columns = [c if c < idx else c + numcols for c in self.MT.displayed_columns] + if mod_column_positions: + inspos = bisect.bisect_left(self.MT.displayed_columns, idx) + self.MT.displayed_columns[inspos:inspos] = list(range(idx, idx + numcols)) + self.MT.insert_col_positions( + idx=inspos, + widths=columns if isinstance(columns, int) and widths is None else widths, + deselect_all=deselect_all, + ) + maxidx = len(self.MT.data) - 1 + if add_rows: + height = self.MT.default_row_height[1] + if idx == "end": + for values in reversed(data): + for rn, v in enumerate(values): + if rn > maxidx: + self.MT.data.append(self.MT.get_empty_row_seq(rn, old_total)) + if self.MT.all_rows_displayed: + self.MT.insert_row_position("end", height=height) + maxidx += 1 + self.MT.data[rn].append(v) + else: + for values in reversed(data): + for rn, v in enumerate(values): + if rn > maxidx: + self.MT.data.append(self.MT.get_empty_row_seq(rn, old_total)) + if self.MT.all_rows_displayed: + self.MT.insert_row_position("end", height=height) + maxidx += 1 + self.MT.data[rn].insert(idx, v) + else: + if idx == "end": + for values in reversed(data): + for rn, v in enumerate(values): + if rn > maxidx: + break + self.MT.data[rn].append(v) + else: + for values in reversed(data): + for rn, v in enumerate(values): + if rn > maxidx: + break + self.MT.data[rn].insert(idx, v) + if isinstance(idx, int): + num_add = len(data) + self.MT.cell_options = { + (rn, cn if cn < idx else cn + num_add): t2 for (rn, cn), t2 in self.MT.cell_options.items() + } + self.MT.col_options = {cn if cn < idx else cn + num_add: t for cn, t in self.MT.col_options.items()} + self.CH.cell_options = {cn if cn < idx else cn + num_add: t for cn, t in self.CH.cell_options.items()} + self.set_refresh_timer(redraw) + + def insert_row( + self, + values: Union[List, None] = None, + idx: Union[str, int] = "end", + height=None, + deselect_all=False, + add_columns=False, + mod_row_positions=True, + redraw=True, + ): + self.insert_rows( + rows=1 if values is None else [values], + idx=idx, + heights=height if height is None else [height], + deselect_all=deselect_all, + add_columns=add_columns, + mod_row_positions=mod_row_positions, + redraw=redraw, + ) + + def insert_rows( + self, + rows: Union[List, int] = 1, + idx: Union[str, int] = "end", + heights=None, + deselect_all=False, + add_columns=True, + mod_row_positions=True, + redraw=True, + ): + total_cols = None + datarn = len(self.MT.data) if idx == "end" else idx + if isinstance(rows, int): + if rows < 1: + raise ValueError(f"rows arg must be greater than 0, not {rows}") + total_cols = self.MT.total_data_cols() + data = [self.MT.get_empty_row_seq(datarn + i, total_cols, r_ops=False) for i in range(rows)] + elif not isinstance(rows, list): + data = list(rows) + else: + data = rows + try: + data = [r if isinstance(r, list) else list(r) for r in data] + except Exception as msg: + raise ValueError(f"rows arg must be int or list of lists. {msg}") + if add_columns: + if total_cols is None: + total_cols = self.MT.total_data_cols() + data_max_cols = len(max(data, key=len)) + if data_max_cols > total_cols: + self.MT.equalize_data_row_lengths(total_columns=data_max_cols) + elif total_cols > data_max_cols: + data[:] = [ + data[i] + self.MT.get_empty_row_seq(datarn + i, end=total_cols, start=data_max_cols, r_ops=False) + for i in range(len(data)) + ] + if self.MT.all_columns_displayed: + if not self.MT.col_positions: + self.MT.col_positions = [0] + if data_max_cols > len(self.MT.col_positions) - 1: + self.insert_column_positions("end", data_max_cols - (len(self.MT.col_positions) - 1)) + if self.MT.all_rows_displayed and mod_row_positions: + inspos = idx + if not self.MT.all_rows_displayed: + numrows = len(data) + if idx != "end": + self.MT.displayed_rows = [r if r < idx else r + numrows for r in self.MT.displayed_rows] + if mod_row_positions: + inspos = bisect.bisect_left(self.MT.displayed_rows, idx) + self.MT.displayed_rows[inspos:inspos] = list(range(idx, idx + numrows)) + if mod_row_positions: + self.MT.insert_row_positions( + idx=inspos, + heights=len(data) if heights is None else heights, + deselect_all=deselect_all, + ) + if isinstance(idx, str) and idx.lower() == "end": + self.MT.data.extend(data) + else: + self.MT.data[idx:idx] = data + num_add = len(data) + self.MT.cell_options = { + (rn if rn < idx else rn + num_add, cn): t2 for (rn, cn), t2 in self.MT.cell_options.items() + } + self.MT.row_options = {rn if rn < idx else rn + num_add: t for rn, t in self.MT.row_options.items()} + self.RI.cell_options = {rn if rn < idx else rn + num_add: t for rn, t in self.RI.cell_options.items()} + self.set_refresh_timer(redraw) + + def sheet_data_dimensions(self, total_rows=None, total_columns=None): + self.MT.data_dimensions(total_rows, total_columns) + + def get_total_rows(self, include_index=False): + return self.MT.total_data_rows(include_index=include_index) + + def get_total_columns(self, include_header=False): + return self.MT.total_data_cols(include_header=include_header) + + def equalize_data_row_lengths(self): + return self.MT.equalize_data_row_lengths() + + def display_rows( + self, + rows=None, + all_rows_displayed=None, + reset_row_positions=True, + refresh=False, + redraw=False, + deselect_all=True, + **kwargs, + ): + if "all_displayed" in kwargs: + all_rows_displayed = kwargs["all_displayed"] + res = self.MT.display_rows( + rows=None if isinstance(rows, str) and rows.lower() == "all" else rows, + all_rows_displayed=True if isinstance(rows, str) and rows.lower() == "all" else all_rows_displayed, + reset_row_positions=reset_row_positions, + deselect_all=deselect_all, + ) + if refresh or redraw: + self.set_refresh_timer(redraw if redraw else refresh) + return res + + def display_columns( + self, + columns=None, + all_columns_displayed=None, + reset_col_positions=True, + refresh=False, + redraw=False, + deselect_all=True, + **kwargs, + ): + if "all_displayed" in kwargs: + all_columns_displayed = kwargs["all_displayed"] + res = self.MT.display_columns( + columns=None if isinstance(columns, str) and columns.lower() == "all" else columns, + all_columns_displayed=True + if isinstance(columns, str) and columns.lower() == "all" + else all_columns_displayed, + reset_col_positions=reset_col_positions, + deselect_all=deselect_all, + ) + if refresh or redraw: + self.set_refresh_timer(redraw if redraw else refresh) + return res + + def all_rows_displayed(self, a=None): + v = bool(self.MT.all_rows_displayed) + if isinstance(a, bool): + self.MT.all_rows_displayed = a + return v + + def all_columns_displayed(self, a=None): + v = bool(self.MT.all_columns_displayed) + if isinstance(a, bool): + self.MT.all_columns_displayed = a + return v + + # uses displayed indexes + def hide_rows(self, rows=set(), redraw=True, deselect_all=True): + if isinstance(rows, int): + _rows = {rows} + elif isinstance(rows, set): + _rows = rows + else: + _rows = set(rows) + if not _rows: + return + if self.MT.all_rows_displayed: + _rows = [r for r in range(self.MT.total_data_rows()) if r not in _rows] + else: + _rows = [e for r, e in enumerate(self.MT.displayed_rows) if r not in _rows] + self.display_rows( + rows=_rows, + all_rows_displayed=False, + redraw=redraw, + deselect_all=deselect_all, + ) + + # uses displayed indexes + def hide_columns(self, columns=set(), redraw=True, deselect_all=True): + if isinstance(columns, int): + _columns = {columns} + elif isinstance(columns, set): + _columns = columns + else: + _columns = set(columns) + if not _columns: + return + if self.MT.all_columns_displayed: + _columns = [c for c in range(self.MT.total_data_cols()) if c not in _columns] + else: + _columns = [e for c, e in enumerate(self.MT.displayed_columns) if c not in _columns] + self.display_columns( + columns=_columns, + all_columns_displayed=False, + redraw=redraw, + deselect_all=deselect_all, + ) + + def show_ctrl_outline(self, canvas="table", start_cell=(0, 0), end_cell=(1, 1)): + self.MT.show_ctrl_outline(canvas=canvas, start_cell=start_cell, end_cell=end_cell) + + def get_ctrl_x_c_boxes(self): + return self.MT.get_ctrl_x_c_boxes() + + def get_selected_min_max( + self, + ): # returns (min_y, min_x, max_y, max_x) of any selections including rows/columns + return self.MT.get_selected_min_max() + + def headers( + self, + newheaders=None, + index=None, + reset_col_positions=False, + show_headers_if_not_sheet=True, + redraw=False, + ): + self.set_refresh_timer(redraw) + return self.MT.headers( + newheaders, + index, + reset_col_positions=reset_col_positions, + show_headers_if_not_sheet=show_headers_if_not_sheet, + redraw=False, + ) + + def row_index( + self, + newindex=None, + index=None, + reset_row_positions=False, + show_index_if_not_sheet=True, + redraw=False, + ): + self.set_refresh_timer(redraw) + return self.MT.row_index( + newindex, + index, + reset_row_positions=reset_row_positions, + show_index_if_not_sheet=show_index_if_not_sheet, + redraw=False, + ) + + def reset_undos(self): + self.MT.undo_storage = deque(maxlen=self.MT.max_undos) + + def redraw(self, redraw_header=True, redraw_row_index=True): + self.MT.main_table_redraw_grid_and_text(redraw_header=redraw_header, redraw_row_index=redraw_row_index) + + def refresh(self, redraw_header=True, redraw_row_index=True): + self.MT.main_table_redraw_grid_and_text(redraw_header=redraw_header, redraw_row_index=redraw_row_index) + + def create_checkbox(self, r=0, c=0, *args, **kwargs): + _kwargs = get_checkbox_kwargs(*args, **kwargs) + if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): + for r_ in range(self.MT.total_data_rows()): + self.MT.create_checkbox(datarn=r_, datacn=c, **_kwargs) + elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): + for c_ in range(self.MT.total_data_cols()): + self.MT.create_checkbox(datarn=r, datacn=c_, **_kwargs) + elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": + totalcols = self.MT.total_data_cols() + for r_ in range(self.MT.total_data_rows()): + for c_ in range(totalcols): + self.MT.create_checkbox(datarn=r_, datacn=c_, **_kwargs) + elif isinstance(r, int) and isinstance(c, int): + self.MT.create_checkbox(datarn=r, datacn=c, **_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def checkbox_cell(self, r=0, c=0, *args, **kwargs): + self.create_checkbox(r=r, c=c, **get_checkbox_kwargs(*args, **kwargs)) + + def create_header_checkbox(self, c=0, *args, **kwargs): + _kwargs = get_checkbox_kwargs(*args, **kwargs) + if isinstance(c, str) and c.lower() == "all": + for c_ in range(self.MT.total_data_cols()): + self.CH.create_checkbox(datacn=c_, **_kwargs) + elif isinstance(c, int): + self.CH.create_checkbox(datacn=c, **_kwargs) + elif is_iterable(c): + for c_ in c: + self.CH.create_checkbox(datacn=c_, **_kwargs) + else: + self.CH.checkbox_header(**_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def create_index_checkbox(self, r=0, *args, **kwargs): + _kwargs = get_checkbox_kwargs(*args, **kwargs) + if isinstance(r, str) and r.lower() == "all": + for r_ in range(self.MT.total_data_rows()): + self.RI.create_checkbox(datarn=r_, **_kwargs) + elif isinstance(r, int): + self.RI.create_checkbox(datarn=r, **_kwargs) + elif is_iterable(r): + for r_ in r: + self.RI.create_checkbox(datarn=r_, **_kwargs) + else: + self.RI.checkbox_index(**_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def checkbox_row(self, r=0, *args, **kwargs): + _kwargs = get_checkbox_kwargs(*args, **kwargs) + if isinstance(r, str) and r.lower() == "all": + for r_ in range(self.MT.total_data_rows()): + self.MT.checkbox_row(datarn=r_, **_kwargs) + elif isinstance(r, int): + self.MT.checkbox_row(datarn=r, **_kwargs) + elif is_iterable(r): + for r_ in r: + self.MT.checkbox_row(datarn=r_, **_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def checkbox_column(self, c=0, *args, **kwargs): + _kwargs = get_checkbox_kwargs(*args, **kwargs) + if isinstance(c, str) and c.lower() == "all": + for c in range(self.MT.total_data_cols()): + self.MT.checkbox_column(datacn=c, **_kwargs) + elif isinstance(c, int): + self.MT.checkbox_column(datacn=c, **_kwargs) + elif is_iterable(c): + for c_ in c: + self.MT.checkbox_column(datacn=c_, **_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def checkbox_sheet(self, *args, **kwargs): + self.MT.checkbox_sheet(**get_checkbox_kwargs(*args, **kwargs)) + + def delete_checkbox(self, r=0, c=0): + if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): + for r_, c_ in self.MT.cell_options: + if "checkbox" in self.MT.cell_options[(r_, c)]: + self.MT.delete_cell_options_checkbox(r_, c) + elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): + for r_, c_ in self.MT.cell_options: + if "checkbox" in self.MT.cell_options[(r, c_)]: + self.MT.delete_cell_options_checkbox(r, c_) + elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": + for r_, c_ in self.MT.cell_options: + if "checkbox" in self.MT.cell_options[(r_, c_)]: + self.MT.delete_cell_options_checkbox(r_, c_) + elif isinstance(r, int) and isinstance(c, int): + self.MT.delete_cell_options_checkbox(r, c) + + def delete_cell_checkbox(self, r=0, c=0): + self.delete_checkbox(r, c) + + def delete_row_checkbox(self, r=0): + if isinstance(r, str) and r.lower() == "all": + for r_ in self.MT.row_options: + self.MT.delete_row_options_checkbox(r_) + elif isinstance(r, int): + self.MT.delete_row_options_checkbox(r) + elif is_iterable(r): + for r_ in r: + self.MT.delete_row_options_checkbox(r_) + + def delete_column_checkbox(self, c=0): + if isinstance(c, str) and c.lower() == "all": + for c_ in self.MT.col_options: + self.MT.delete_column_options_checkbox(c_) + elif isinstance(c, int): + self.MT.delete_column_options_checkbox(c) + elif is_iterable(c): + for c_ in c: + self.MT.delete_column_options_checkbox(c_) + + def delete_sheet_checkbox(self): + self.MT.delete_options_checkbox() + + def delete_header_checkbox(self, c=0): + if isinstance(c, str) and c.lower() == "all": + for c_ in self.CH.cell_options: + if "checkbox" in self.CH.cell_options[c_]: + self.CH.delete_cell_options_checkbox(c_) + if isinstance(c, int): + self.CH.delete_cell_options_checkbox(c) + else: + self.CH.delete_options_checkbox() + + def delete_index_checkbox(self, r=0): + if isinstance(r, str) and r.lower() == "all": + for r_ in self.RI.cell_options: + if "checkbox" in self.RI.cell_options[r_]: + self.RI.delete_cell_options_checkbox(r_) + if isinstance(r, int): + self.RI.delete_cell_options_checkbox(r) + else: + self.RI.delete_options_checkbox() + + def click_checkbox(self, r, c, checked=None): + kwargs = self.MT.get_cell_kwargs(r, c, key="checkbox") + if kwargs: + if not isinstance(self.MT.data[r][c], bool): + if checked is None: + self.MT.data[r][c] = False + else: + self.MT.data[r][c] = bool(checked) + else: + self.MT.data[r][c] = not self.MT.data[r][c] + + def click_header_checkbox(self, c, checked=None): + kwargs = self.CH.get_cell_kwargs(c, key="checkbox") + if kwargs: + if not isinstance(self.MT._headers[c], bool): + if checked is None: + self.MT._headers[c] = False + else: + self.MT._headers[c] = bool(checked) + else: + self.MT._headers[c] = not self.MT._headers[c] + + def click_index_checkbox(self, r, checked=None): + kwargs = self.RI.get_cell_kwargs(r, key="checkbox") + if kwargs: + if not isinstance(self.MT._row_index[r], bool): + if checked is None: + self.MT._row_index[r] = False + else: + self.MT._row_index[r] = bool(checked) + else: + self.MT._row_index[r] = not self.MT._row_index[r] + + def get_checkboxes(self): + d = { + **{k: v["checkbox"] for k, v in self.MT.cell_options.items() if "checkbox" in v}, + **{k: v["checkbox"] for k, v in self.MT.row_options.items() if "checkbox" in v}, + **{k: v["checkbox"] for k, v in self.MT.col_options.items() if "checkbox" in v}, + } + if "checkbox" in self.MT.options: + return {**d, "checkbox": self.MT.options["checkbox"]} + return d + + def get_header_checkboxes(self): + d = {k: v["checkbox"] for k, v in self.CH.cell_options.items() if "checkbox" in v} + if "checkbox" in self.CH.options: + return {**d, "checkbox": self.CH.options["checkbox"]} + return d + + def get_index_checkboxes(self): + d = {k: v["checkbox"] for k, v in self.RI.cell_options.items() if "checkbox" in v} + if "checkbox" in self.RI.options: + return {**d, "checkbox": self.RI.options["checkbox"]} + return d + + def checkbox(self, r, c, checked=None, state=None, check_function="", text=None): + if isinstance(checked, bool): + self.set_cell_data(r, c, checked) + kwargs = self.MT.get_cell_kwargs(r, c, key="checkbox") + if check_function != "": + kwargs["check_function"] = check_function + if state and state.lower() in ("normal", "disabled"): + kwargs["state"] = state + if text is not None: + kwargs["text"] = text + return {**kwargs, "checked": self.MT.data[r][c]} + + def header_checkbox(self, c, checked=None, state=None, check_function="", text=None): + if isinstance(checked, bool): + self.headers(newheaders=checked, index=c) + kwargs = self.CH.get_cell_kwargs(c, key="checkbox") + if kwargs: + if check_function != "": + kwargs["check_function"] = check_function + if state and state.lower() in ("normal", "disabled"): + kwargs["state"] = state + if text is not None: + kwargs["text"] = text + return {**kwargs, "checked": self.MT._headers[c]} + + def index_checkbox(self, r, checked=None, state=None, check_function="", text=None): + if isinstance(checked, bool): + self.row_index(newindex=checked, index=r) + kwargs = self.RI.get_cell_kwargs(r, key="checkbox") + if kwargs: + if check_function != "": + kwargs["check_function"] = check_function + if state and state.lower() in ("normal", "disabled"): + kwargs["state"] = state + if text is not None: + kwargs["text"] = text + return {**kwargs, "checked": self.MT._row_index[r]} + + def create_dropdown(self, r=0, c=0, *args, **kwargs): + _kwargs = get_dropdown_kwargs(*args, **kwargs) + if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): + for r_ in range(self.MT.total_data_rows()): + self.MT.create_dropdown(datarn=r_, datacn=c, **_kwargs) + elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): + for c_ in range(self.MT.total_data_cols()): + self.MT.create_dropdown(datarn=r, datacn=c_, **_kwargs) + elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": + totalcols = self.MT.total_data_cols() + for r_ in range(self.MT.total_data_rows()): + for c_ in range(totalcols): + self.MT.create_dropdown(datarn=r_, datacn=c_, **_kwargs) + elif isinstance(r, int) and isinstance(c, int): + self.MT.create_dropdown(datarn=r, datacn=c, **_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def dropdown_cell(self, r=0, c=0, *args, **kwargs): + self.create_dropdown(r=r, c=c, **get_dropdown_kwargs(*args, **kwargs)) + + def dropdown_row(self, r=0, *args, **kwargs): + _kwargs = get_dropdown_kwargs(*args, **kwargs) + if isinstance(r, str) and r.lower() == "all": + for r_ in range(self.MT.total_data_rows()): + self.MT.dropdown_row(datarn=r_, **_kwargs) + elif isinstance(r, int): + self.MT.dropdown_row(datarn=r, **_kwargs) + elif is_iterable(r): + for r_ in r: + self.MT.dropdown_row(datarn=r_, **_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def dropdown_column(self, c=0, *args, **kwargs): + _kwargs = get_dropdown_kwargs(*args, **kwargs) + if isinstance(c, str) and c.lower() == "all": + for c_ in range(self.MT.total_data_cols()): + self.MT.dropdown_column(datacn=c_, **_kwargs) + elif isinstance(c, int): + self.MT.dropdown_column(datacn=c, **_kwargs) + elif is_iterable(c): + for c_ in c: + self.MT.dropdown_column(datacn=c_, **_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def dropdown_sheet(self, *args, **kwargs): + _kwargs = get_dropdown_kwargs(*args, **kwargs) + self.MT.dropdown_sheet(**_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def create_header_dropdown(self, c=0, *args, **kwargs): + _kwargs = get_dropdown_kwargs(*args, **kwargs) + if isinstance(c, str) and c.lower() == "all": + for c_ in range(self.MT.total_data_cols()): + self.CH.create_dropdown(datacn=c_, **_kwargs) + elif isinstance(c, int): + self.CH.create_dropdown(datacn=c, **_kwargs) + elif is_iterable(c): + for c_ in c: + self.CH.create_dropdown(datacn=c_, **_kwargs) + elif c is None: + self.CH.dropdown_header(**_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def create_index_dropdown(self, r=0, *args, **kwargs): + _kwargs = get_dropdown_kwargs(*args, **kwargs) + if isinstance(r, str) and r.lower() == "all": + for r_ in range(self.MT.total_data_rows()): + self.RI.create_dropdown(datarn=r_, **_kwargs) + elif isinstance(r, int): + self.RI.create_dropdown(datarn=r, **_kwargs) + elif is_iterable(r): + for r_ in r: + self.RI.create_dropdown(datarn=r_, **_kwargs) + elif r is None: + self.RI.dropdown_index(**_kwargs) + self.set_refresh_timer(_kwargs["redraw"]) + + def delete_dropdown(self, r=0, c=0): + if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): + for r_, c_ in self.MT.cell_options: + if "dropdown" in self.MT.cell_options[(r_, c)]: + self.MT.delete_cell_options_dropdown(r_, c) + elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): + for r_, c_ in self.MT.cell_options: + if "dropdown" in self.MT.cell_options[(r, c_)]: + self.MT.delete_cell_options_dropdown(r, c_) + elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": + for r_, c_ in self.MT.cell_options: + if "dropdown" in self.MT.cell_options[(r_, c_)]: + self.MT.delete_cell_options_dropdown(r_, c_) + elif isinstance(r, int) and isinstance(c, int): + self.MT.delete_cell_options_dropdown(r, c) + + def delete_cell_dropdown(self, r=0, c=0): + self.delete_dropdown(r=r, c=c) + + def delete_row_dropdown(self, r="all"): + if isinstance(r, str) and r.lower() == "all": + for r_ in self.MT.row_options: + if "dropdown" in self.MT.row_options[r_]: + self.MT.delete_row_options_dropdown(datarn=r_) + elif isinstance(r, int): + self.MT.delete_row_options_dropdown(datarn=r) + elif is_iterable(r): + for r_ in r: + self.MT.delete_row_options_dropdown(datarn=r_) + + def delete_column_dropdown(self, c="all"): + if isinstance(c, str) and c.lower() == "all": + for c_ in self.MT.col_options: + if "dropdown" in self.MT.col_options[c_]: + self.MT.delete_column_options_dropdown(datacn=c_) + elif isinstance(c, int): + self.MT.delete_column_options_dropdown(datacn=c) + elif is_iterable(c): + for c_ in c: + self.MT.delete_column_options_dropdown(datacn=c_) + + def delete_sheet_dropdown(self): + self.MT.delete_options_dropdown() + + def delete_header_dropdown(self, c=None): + if isinstance(c, str) and c.lower() == "all": + for c_ in self.CH.cell_options: + if "dropdown" in self.CH.cell_options[c_]: + self.CH.delete_cell_options_dropdown(c_) + elif isinstance(c, int): + self.CH.delete_cell_options_dropdown(c) + elif is_iterable(c): + for c_ in c: + self.CH.delete_cell_options_dropdown(c_) + elif c is None: + self.CH.delete_options_dropdown(c) + + def delete_index_dropdown(self, r=0): + if isinstance(r, str) and r.lower() == "all": + for r_ in self.RI.cell_options: + if "dropdown" in self.RI.cell_options[r_]: + self.RI.delete_cell_options_dropdown(r_) + elif isinstance(r, int): + self.RI.delete_cell_options_dropdown(r) + elif is_iterable(r): + for r_ in r: + self.RI.delete_cell_options_dropdown(r_) + elif r is None: + self.RI.delete_options_dropdown() + + def get_dropdowns(self): + d = { + **{k: v["dropdown"] for k, v in self.MT.cell_options.items() if "dropdown" in v}, + **{k: v["dropdown"] for k, v in self.MT.row_options.items() if "dropdown" in v}, + **{k: v["dropdown"] for k, v in self.MT.col_options.items() if "dropdown" in v}, + } + if "dropdown" in self.MT.options: + return {**d, "dropdown": self.MT.options["dropdown"]} + return d + + def get_header_dropdowns(self): + d = {k: v["dropdown"] for k, v in self.CH.cell_options.items() if "dropdown" in v} + if "dropdown" in self.CH.options: + return {**d, "dropdown": self.CH.options["dropdown"]} + return d + + def get_index_dropdowns(self): + d = {k: v["dropdown"] for k, v in self.RI.cell_options.items() if "dropdown" in v} + if "dropdown" in self.RI.options: + return {**d, "dropdown": self.RI.options["dropdown"]} + return d + + def set_dropdown_values(self, r=0, c=0, set_existing_dropdown=False, values=[], set_value=None): + if set_existing_dropdown: + if self.MT.existing_dropdown_window is not None: + r_ = self.MT.existing_dropdown_window.r + c_ = self.MT.existing_dropdown_window.c + else: + raise Exception("No dropdown box is currently open") + else: + r_ = r + c_ = c + kwargs = self.MT.get_cell_kwargs(r, c, key="dropdown") + kwargs["values"] = values + if kwargs["window"] != "no dropdown open": + kwargs["window"].values(values) + if set_value is not None: + self.set_cell_data(r_, c_, set_value) + if ( + kwargs["window"] != "no dropdown open" + and self.MT.text_editor_loc is not None + and self.MT.text_editor is not None + ): + self.MT.text_editor.set_text(set_value) + + def set_header_dropdown_values(self, c=0, set_existing_dropdown=False, values=[], set_value=None): + if set_existing_dropdown: + if self.CH.existing_dropdown_window is not None: + c_ = self.CH.existing_dropdown_window.c + else: + raise Exception("No dropdown box is currently open") + else: + c_ = c + kwargs = self.CH.get_cell_kwargs(c_, key="dropdown") + if kwargs: + kwargs["values"] = values + if kwargs["window"] != "no dropdown open": + kwargs["window"].values(values) + if set_value is not None: + self.MT.headers(newheaders=set_value, index=c_) + + def set_index_dropdown_values(self, r, set_existing_dropdown=False, values=[], set_value=None): + if set_existing_dropdown: + if self.RI.existing_dropdown_window is not None: + r_ = self.RI.existing_dropdown_window.r + else: + raise Exception("No dropdown box is currently open") + else: + r_ = r + kwargs = self.RI.get_cell_kwargs(r_, key="dropdown") + if kwargs: + kwargs["values"] = values + if kwargs["window"] != "no dropdown open": + kwargs["window"].values(values) + if set_value is not None: + self.MT.row_index(newindex=set_value, index=r_) + + def get_dropdown_values(self, r=0, c=0): + kwargs = self.MT.get_cell_kwargs(r, c, key="dropdown") + if kwargs: + return kwargs["values"] + + def get_header_dropdown_values(self, c=0): + kwargs = self.CH.get_cell_kwargs(c, key="dropdown") + if kwargs: + return kwargs["values"] + + def get_index_dropdown_values(self, r=0): + kwargs = self.RI.get_cell_kwargs(r, key="dropdown") + if kwargs: + kwargs["values"] + + def dropdown_functions(self, r, c, selection_function="", modified_function=""): + kwargs = self.MT.get_cell_kwargs(r, c, key="dropdown") + if kwargs: + if selection_function != "": + kwargs["select_function"] = selection_function + if modified_function != "": + kwargs["modified_function"] = modified_function + return kwargs + + def header_dropdown_functions(self, c, selection_function="", modified_function=""): + kwargs = self.CH.get_cell_kwargs(c, key="dropdown") + if selection_function != "": + kwargs["selection_function"] = selection_function + if modified_function != "": + kwargs["modified_function"] = modified_function + return kwargs + + def index_dropdown_functions(self, r, selection_function="", modified_function=""): + kwargs = self.RI.get_cell_kwargs(r, key="dropdown") + if selection_function != "": + kwargs["select_function"] = selection_function + if modified_function != "": + kwargs["modified_function"] = modified_function + return kwargs + + def get_dropdown_value(self, r=0, c=0): + if self.MT.get_cell_kwargs(r, c, key="dropdown"): + return self.get_cell_data(r, c) + + def get_header_dropdown_value(self, c=0): + if self.CH.get_cell_kwargs(c, key="dropdown"): + return self.MT._headers[c] + + def get_index_dropdown_value(self, r=0): + if self.RI.get_cell_kwargs(r, key="dropdown"): + return self.MT._row_index[r] + + def open_dropdown(self, r, c): + self.MT.open_dropdown_window(r, c) + + def close_dropdown(self, r, c): + self.MT.close_dropdown_window(r, c) + + def open_header_dropdown(self, c): + self.CH.open_dropdown_window(c) + + def close_header_dropdown(self, c): + self.CH.close_dropdown_window(c) + + def open_index_dropdown(self, r): + self.RI.open_dropdown_window(r) + + def close_index_dropdown(self, r): + self.RI.close_dropdown_window(r) + + def reapply_formatting(self): + self.MT.reapply_formatting() + + def delete_all_formatting(self, clear_values=False): + self.MT.delete_all_formatting(clear_values=clear_values) + + def format_cell( + self, + r, + c, + formatter_options={}, + formatter_class=None, + redraw=True, + **kwargs, + ): + if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): + for r_ in range(self.MT.total_data_rows()): + self.MT.format_cell( + datarn=r_, + datacn=c, + **{"formatter": formatter_class, **formatter_options, **kwargs}, + ) + elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): + for c_ in range(self.MT.total_data_cols()): + self.MT.format_cell( + datarn=r, + datacn=c_, + **{"formatter": formatter_class, **formatter_options, **kwargs}, + ) + elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": + for r_ in range(self.MT.total_data_rows()): + for c_ in range(self.MT.total_data_cols()): + self.MT.format_cell( + datarn=r_, + datacn=c_, + **{"formatter": formatter_class, **formatter_options, **kwargs}, + ) + else: + self.MT.format_cell( + datarn=r, + datacn=c, + **{"formatter": formatter_class, **formatter_options, **kwargs}, + ) + self.set_refresh_timer(redraw) + + def delete_cell_format( + self, + r="all", + c="all", + clear_values=False, + ): + if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): + for r_, c_ in self.MT.cell_options: + if "format" in self.MT.cell_options[(r_, c)]: + self.MT.delete_cell_format(r_, c, clear_values=clear_values) + elif isinstance(c, str) and c.lower() == "all" and isinstance(r, int): + for r_, c_ in self.MT.cell_options: + if "format" in self.MT.cell_options[(r, c_)]: + self.MT.delete_cell_format(r, c_, clear_values=clear_values) + elif isinstance(r, str) and r.lower() == "all" and isinstance(c, str) and c.lower() == "all": + for r_, c_ in self.MT.cell_options: + if "format" in self.MT.cell_options[(r_, c_)]: + self.MT.delete_cell_format(r_, c_, clear_values=clear_values) + else: + self.MT.delete_cell_format(r, c, clear_values=clear_values) + + def format_row( + self, + r, + formatter_options={}, + formatter_class=None, + redraw=True, + **kwargs, + ): + if isinstance(r, str) and r.lower() == "all": + for r_ in range(len(self.MT.data)): + self.MT.format_row(r_, **{"formatter": formatter_class, **formatter_options, **kwargs}) + elif is_iterable(r): + for r_ in r: + self.MT.format_row(r_, **{"formatter": formatter_class, **formatter_options, **kwargs}) + else: + self.MT.format_row(r, **{"formatter": formatter_class, **formatter_options, **kwargs}) + self.set_refresh_timer(redraw) + + def delete_row_format(self, r="all", clear_values=False): + if is_iterable(r): + for r_ in r: + self.MT.delete_row_format(r_, clear_values=clear_values) + else: + self.MT.delete_row_format(r, clear_values=clear_values) + + def format_column( + self, + c, + formatter_options={}, + formatter_class=None, + redraw=True, + **kwargs, + ): + if isinstance(c, str) and c.lower() == "all": + for c_ in range(self.MT.total_data_cols()): + self.MT.format_column(c_, **{"formatter": formatter_class, **formatter_options, **kwargs}) + elif is_iterable(c): + for c_ in c: + self.MT.format_column(c_, **{"formatter": formatter_class, **formatter_options, **kwargs}) + else: + self.MT.format_column(c, **{"formatter": formatter_class, **formatter_options, **kwargs}) + self.set_refresh_timer(redraw) + + def delete_column_format(self, c="all", clear_values=False): + if is_iterable(c): + for c_ in c: + self.MT.delete_column_format(c_, clear_values=clear_values) + else: + self.MT.delete_column_format(c, clear_values=clear_values) + + def format_sheet(self, formatter_options={}, formatter_class=None, redraw=True, **kwargs): + self.MT.format_sheet(**{"formatter": formatter_class, **formatter_options, **kwargs}) + self.set_refresh_timer(redraw) + + def delete_sheet_format(self, clear_values=False): + self.MT.delete_sheet_format(clear_values=clear_values) + + +class Sheet_Dropdown(Sheet): + def __init__( + self, + parent, + r, + c, + width=None, + height=None, + font=None, + colors={ + "bg": theme_light_blue["popup_menu_bg"], + "fg": theme_light_blue["popup_menu_fg"], + "highlight_bg": theme_light_blue["popup_menu_highlight_bg"], + "highlight_fg": theme_light_blue["popup_menu_highlight_fg"], + }, + outline_color=theme_light_blue["table_fg"], + outline_thickness=2, + values=[], + close_dropdown_window=None, + modified_function=None, + search_function=dropdown_search_function, + arrowkey_RIGHT=None, + arrowkey_LEFT=None, + align="w", + # False for using r, c "r" for r "c" for c + single_index=False, + ): + Sheet.__init__( + self, + parent=parent, + outline_thickness=outline_thickness, + outline_color=outline_color, + table_grid_fg=colors["fg"], + show_horizontal_grid=True, + show_vertical_grid=False, + show_header=False, + show_row_index=False, + show_top_left=False, + align="w", # alignments other than w for dropdown boxes are broken at the moment + empty_horizontal=0, + empty_vertical=0, + selected_rows_to_end_of_window=True, + horizontal_grid_to_end_of_window=True, + set_cell_sizes_on_zoom=True, + show_selected_cells_border=False, + table_selected_cells_border_fg=colors["fg"], + table_selected_cells_bg=colors["highlight_bg"], + table_selected_rows_border_fg=colors["fg"], + table_selected_rows_bg=colors["highlight_bg"], + table_selected_rows_fg=colors["highlight_fg"], + width=width, + height=height, + font=font if font else get_font(), + table_fg=colors["fg"], + table_bg=colors["bg"], + ) + self.parent = parent + self.close_dropdown_window = close_dropdown_window + self.modified_function = modified_function + self.search_function = search_function + self.arrowkey_RIGHT = arrowkey_RIGHT + self.arrowkey_LEFT = arrowkey_LEFT + self.h_ = height + self.w_ = width + self.r = r + self.c = c + self.row = -1 + self.single_index = single_index + self.bind("", self.mouse_motion) + self.bind("", self.b1) + self.bind("", self.arrowkey_UP) + self.bind("", self.arrowkey_RIGHT) + self.bind("", self.arrowkey_RIGHT) + self.bind("", self.arrowkey_DOWN) + self.bind("", self.arrowkey_LEFT) + self.bind("", self.arrowkey_UP) + self.bind("", self.arrowkey_DOWN) + self.bind("", self.b1) + if values: + self.values(values, redraw=False) + + def arrowkey_UP(self, event=None): + self.deselect("all") + if self.row > 0: + self.row -= 1 + else: + self.row = 0 + self.see(self.row, 0, redraw=False) + self.select_row(self.row) + + def arrowkey_DOWN(self, event=None): + self.deselect("all") + if len(self.MT.data) - 1 > self.row: + self.row += 1 + self.see(self.row, 0, redraw=False) + self.select_row(self.row) + + def search_and_see(self, event=None): + if self.modified_function is not None: + self.modified_function(event) + if self.search_function is not None: + rn = self.search_function(search_for=rf"{event.value}".lower(), data=self.MT.data) + if rn is not None: + self.row = rn + self.deselect("all") + self.see(self.row, 0, redraw=False) + self.select_row(self.row) + + def mouse_motion(self, event=None): + self.row = self.identify_row(event, exclude_index=True, allow_end=False) + self.deselect("all") + if self.row is not None: + self.select_row(self.row) + + def _reselect(self): + rows = self.get_selected_rows() + if rows: + self.select_row(next(iter(rows))) + + def b1(self, event=None): + if event is None: + row = None + elif event.keycode == 13: + row = self.get_selected_rows() + if not row: + row = None + else: + row = next(iter(row)) + else: + row = self.identify_row(event, exclude_index=True, allow_end=False) + if self.single_index: + if row is None: + self.close_dropdown_window(self.r if self.single_index == "r" else self.c) + else: + self.close_dropdown_window( + self.r if self.single_index == "r" else self.c, + self.get_cell_data(row, 0), + ) + else: + if row is None: + self.close_dropdown_window(self.r, self.c) + else: + self.close_dropdown_window(self.r, self.c, self.get_cell_data(row, 0)) + + def values(self, values=[], redraw=True): + self.set_sheet_data( + [[v] for v in values], + reset_col_positions=False, + reset_row_positions=False, + redraw=False, + verify=False, + ) + self.set_all_cell_sizes_to_text(redraw=True) diff --git a/thirdparty/tksheet/_tksheet_column_headers.py b/thirdparty/tksheet/_tksheet_column_headers.py new file mode 100644 index 0000000..7f71a21 --- /dev/null +++ b/thirdparty/tksheet/_tksheet_column_headers.py @@ -0,0 +1,2421 @@ +from __future__ import annotations + +import pickle +import tkinter as tk +import zlib +from collections import defaultdict +from itertools import accumulate, chain, cycle, islice +from math import ceil, floor + +from ._tksheet_formatters import ( + is_bool_like, + try_to_bool, +) +from ._tksheet_other_classes import ( + BeginDragDropEvent, + DraggedRowColumn, + DropDownModifiedEvent, + EditHeaderEvent, + EndDragDropEvent, + ResizeEvent, + SelectColumnEvent, + SelectionBoxEvent, + TextEditor, + get_checkbox_dict, + get_dropdown_dict, + get_n2a, + get_seq_without_gaps_at_index, +) +from ._tksheet_vars import ( + USER_OS, + Color_Map_, + rc_binding, + symbols_set, +) + + +class ColumnHeaders(tk.Canvas): + def __init__(self, *args, **kwargs): + tk.Canvas.__init__( + self, + kwargs["parentframe"], + background=kwargs["header_bg"], + highlightthickness=0, + ) + self.parentframe = kwargs["parentframe"] + self.current_height = None # is set from within MainTable() __init__ or from Sheet parameters + self.MT = None # is set from within MainTable() __init__ + self.RI = None # is set from within MainTable() __init__ + self.TL = None # is set from within TopLeftRectangle() __init__ + self.popup_menu_loc = None + self.extra_begin_edit_cell_func = None + self.extra_end_edit_cell_func = None + self.text_editor = None + self.text_editor_id = None + self.text_editor_loc = None + self.centre_alignment_text_mod_indexes = (slice(1, None), slice(None, -1)) + self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) + self.b1_pressed_loc = None + self.existing_dropdown_canvas_id = None + self.existing_dropdown_window = None + self.closed_dropdown = None + self.being_drawn_rect = None + self.extra_motion_func = None + self.extra_b1_press_func = None + self.extra_b1_motion_func = None + self.extra_b1_release_func = None + self.extra_double_b1_func = None + self.ch_extra_begin_drag_drop_func = None + self.ch_extra_end_drag_drop_func = None + self.extra_rc_func = None + self.selection_binding_func = None + self.shift_selection_binding_func = None + self.ctrl_selection_binding_func = None + self.drag_selection_binding_func = None + self.column_width_resize_func = None + self.width_resizing_enabled = False + self.height_resizing_enabled = False + self.double_click_resizing_enabled = False + self.col_selection_enabled = False + self.drag_and_drop_enabled = False + self.rc_delete_col_enabled = False + self.rc_insert_col_enabled = False + self.hide_columns_enabled = False + self.edit_cell_enabled = False + self.dragged_col = None + self.visible_col_dividers = {} + self.col_height_resize_bbox = tuple() + self.cell_options = {} + self.options = {} + self.rsz_w = None + self.rsz_h = None + self.new_col_height = 0 + self.lines_start_at = 0 + self.currently_resizing_width = False + self.currently_resizing_height = False + self.ch_rc_popup_menu = None + + self.disp_text = {} + self.disp_high = {} + self.disp_grid = {} + self.disp_fill_sels = {} + self.disp_resize_lines = {} + self.disp_dropdown = {} + self.disp_checkbox = {} + self.hidd_text = {} + self.hidd_high = {} + self.hidd_grid = {} + self.hidd_fill_sels = {} + self.hidd_resize_lines = {} + self.hidd_dropdown = {} + self.hidd_checkbox = {} + + self.column_drag_and_drop_perform = kwargs["column_drag_and_drop_perform"] + self.default_header = kwargs["default_header"].lower() + self.header_bg = kwargs["header_bg"] + self.header_fg = kwargs["header_fg"] + self.header_grid_fg = kwargs["header_grid_fg"] + self.header_border_fg = kwargs["header_border_fg"] + self.header_selected_cells_bg = kwargs["header_selected_cells_bg"] + self.header_selected_cells_fg = kwargs["header_selected_cells_fg"] + self.header_selected_columns_bg = kwargs["header_selected_columns_bg"] + self.header_selected_columns_fg = kwargs["header_selected_columns_fg"] + self.header_hidden_columns_expander_bg = kwargs["header_hidden_columns_expander_bg"] + self.show_default_header_for_empty = kwargs["show_default_header_for_empty"] + self.drag_and_drop_bg = kwargs["drag_and_drop_bg"] + self.resizing_line_fg = kwargs["resizing_line_fg"] + self.align = kwargs["header_align"] + self.basic_bindings() + + def basic_bindings(self, enable=True): + if enable: + self.bind("", self.mouse_motion) + self.bind("", self.b1_press) + self.bind("", self.b1_motion) + self.bind("", self.b1_release) + self.bind("", self.double_b1) + self.bind(rc_binding, self.rc) + self.bind("", self.mousewheel) + else: + self.unbind("") + self.unbind("") + self.unbind("") + self.unbind("") + self.unbind("") + self.unbind(rc_binding) + self.unbind("") + + def mousewheel(self, event=None): + maxlines = 0 + if isinstance(self.MT._headers, int): + if len(self.MT.data) > self.MT._headers: + maxlines = max( + len( + self.MT.get_valid_cell_data_as_str(self.MT._headers, datacn, get_displayed=True) + .rstrip() + .split("\n") + ) + for datacn in range(len(self.MT.data[self.MT._headers])) + ) + elif isinstance(self.MT._headers, (list, tuple)): + maxlines = max( + len(e.rstrip().split("\n")) if isinstance(e, str) else len(f"{e}".rstrip().split("\n")) + for e in self.MT._headers + ) + if maxlines == 1: + maxlines = 0 + if self.lines_start_at > maxlines: + self.lines_start_at = maxlines + if (event.delta < 0 or event.num == 5) and self.lines_start_at < maxlines: + self.lines_start_at += 1 + elif (event.delta >= 0 or event.num == 4) and self.lines_start_at > 0: + self.lines_start_at -= 1 + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False, redraw_table=False) + + def set_height(self, new_height, set_TL=False): + self.current_height = new_height + try: + self.config(height=new_height) + except Exception: + return + if set_TL and self.TL is not None: + self.TL.set_dimensions(new_h=new_height) + + def enable_bindings(self, binding): + if binding == "column_width_resize": + self.width_resizing_enabled = True + if binding == "column_height_resize": + self.height_resizing_enabled = True + if binding == "double_click_column_resize": + self.double_click_resizing_enabled = True + if binding == "column_select": + self.col_selection_enabled = True + if binding == "drag_and_drop": + self.drag_and_drop_enabled = True + if binding == "hide_columns": + self.hide_columns_enabled = True + + def disable_bindings(self, binding): + if binding == "column_width_resize": + self.width_resizing_enabled = False + if binding == "column_height_resize": + self.height_resizing_enabled = False + if binding == "double_click_column_resize": + self.double_click_resizing_enabled = False + if binding == "column_select": + self.col_selection_enabled = False + if binding == "drag_and_drop": + self.drag_and_drop_enabled = False + if binding == "hide_columns": + self.hide_columns_enabled = False + + def check_mouse_position_width_resizers(self, x, y): + for c, (x1, y1, x2, y2) in self.visible_col_dividers.items(): + if x >= x1 and y >= y1 and x <= x2 and y <= y2: + return c + + def rc(self, event): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + popup_menu = None + if self.MT.identify_col(x=event.x, allow_end=False) is None: + self.MT.deselect("all") + if self.MT.rc_popup_menus_enabled: + popup_menu = self.MT.empty_rc_popup_menu + elif self.col_selection_enabled and not self.currently_resizing_width and not self.currently_resizing_height: + c = self.MT.identify_col(x=event.x) + if c < len(self.MT.col_positions) - 1: + if self.MT.col_selected(c): + if self.MT.rc_popup_menus_enabled: + popup_menu = self.ch_rc_popup_menu + else: + if self.MT.single_selection_enabled and self.MT.rc_select_enabled: + self.select_col(c, redraw=True) + elif self.MT.toggle_selection_enabled and self.MT.rc_select_enabled: + self.toggle_select_col(c, redraw=True) + if self.MT.rc_popup_menus_enabled: + popup_menu = self.ch_rc_popup_menu + if self.extra_rc_func is not None: + self.extra_rc_func(event) + if popup_menu is not None: + self.popup_menu_loc = c + popup_menu.tk_popup(event.x_root, event.y_root) + + def ctrl_b1_press(self, event=None): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + if ( + (self.drag_and_drop_enabled or self.col_selection_enabled) + and self.MT.ctrl_select_enabled + and self.rsz_h is None + and self.rsz_w is None + ): + c = self.MT.identify_col(x=event.x) + if c < len(self.MT.col_positions) - 1: + c_selected = self.MT.col_selected(c) + if not c_selected and self.col_selection_enabled: + self.add_selection(c, set_as_current=True) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.ctrl_selection_binding_func is not None: + self.ctrl_selection_binding_func(SelectionBoxEvent("ctrl_select_columns", (c, c + 1))) + elif c_selected: + self.dragged_col = DraggedRowColumn( + dragged=c, + to_move=get_seq_without_gaps_at_index(sorted(self.MT.get_selected_cols()), c), + ) + elif not self.MT.ctrl_select_enabled: + self.b1_press(event) + + def ctrl_shift_b1_press(self, event): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + x = event.x + c = self.MT.identify_col(x=x) + if ( + (self.drag_and_drop_enabled or self.col_selection_enabled) + and self.MT.ctrl_select_enabled + and self.rsz_h is None + and self.rsz_w is None + ): + if c < len(self.MT.col_positions) - 1: + c_selected = self.MT.col_selected(c) + if not c_selected and self.col_selection_enabled: + currently_selected = self.MT.currently_selected() + if currently_selected and currently_selected.type_ == "column": + min_c = int(currently_selected[1]) + if c > min_c: + self.MT.create_selected( + 0, + min_c, + len(self.MT.row_positions) - 1, + c + 1, + "columns", + ) + func_event = tuple(range(min_c, c + 1)) + elif c < min_c: + self.MT.create_selected( + 0, + c, + len(self.MT.row_positions) - 1, + min_c + 1, + "columns", + ) + func_event = tuple(range(c, min_c + 1)) + else: + self.add_selection(c, set_as_current=True) + func_event = (c,) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.ctrl_selection_binding_func is not None: + self.ctrl_selection_binding_func(SelectionBoxEvent("ctrl_select_columns", func_event)) + elif c_selected: + self.dragged_col = DraggedRowColumn( + dragged=c, + to_move=get_seq_without_gaps_at_index(sorted(self.MT.get_selected_cols()), c), + ) + elif not self.MT.ctrl_select_enabled: + self.shift_b1_press(event) + + def shift_b1_press(self, event): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + x = event.x + c = self.MT.identify_col(x=x) + if (self.drag_and_drop_enabled or self.col_selection_enabled) and self.rsz_h is None and self.rsz_w is None: + if c < len(self.MT.col_positions) - 1: + c_selected = self.MT.col_selected(c) + if not c_selected and self.col_selection_enabled: + currently_selected = self.MT.currently_selected() + if currently_selected and currently_selected.type_ == "column": + min_c = int(currently_selected[1]) + self.MT.delete_selection_rects(delete_current=False) + if c > min_c: + self.MT.create_selected( + 0, + min_c, + len(self.MT.row_positions) - 1, + c + 1, + "columns", + ) + func_event = tuple(range(min_c, c + 1)) + elif c < min_c: + self.MT.create_selected( + 0, + c, + len(self.MT.row_positions) - 1, + min_c + 1, + "columns", + ) + func_event = tuple(range(c, min_c + 1)) + else: + self.select_col(c) + func_event = (c,) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.shift_selection_binding_func is not None: + self.shift_selection_binding_func(SelectionBoxEvent("shift_select_columns", func_event)) + elif c_selected: + self.dragged_col = DraggedRowColumn( + dragged=c, + to_move=get_seq_without_gaps_at_index(sorted(self.MT.get_selected_cols()), c), + ) + + def create_resize_line(self, x1, y1, x2, y2, width, fill, tag): + if self.hidd_resize_lines: + t, sh = self.hidd_resize_lines.popitem() + self.coords(t, x1, y1, x2, y2) + if sh: + self.itemconfig(t, width=width, fill=fill, tag=tag) + else: + self.itemconfig(t, width=width, fill=fill, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_line(x1, y1, x2, y2, width=width, fill=fill, tag=tag) + self.disp_resize_lines[t] = True + + def delete_resize_lines(self): + self.hidd_resize_lines.update(self.disp_resize_lines) + self.disp_resize_lines = {} + for t, sh in self.hidd_resize_lines.items(): + if sh: + self.itemconfig(t, state="hidden") + self.hidd_resize_lines[t] = False + + def mouse_motion(self, event): + if not self.currently_resizing_height and not self.currently_resizing_width: + x = self.canvasx(event.x) + y = self.canvasy(event.y) + mouse_over_resize = False + mouse_over_selected = False + if self.width_resizing_enabled: + c = self.check_mouse_position_width_resizers(x, y) + if c is not None: + self.rsz_w, mouse_over_resize = c, True + self.config(cursor="sb_h_double_arrow") + else: + self.rsz_w = None + if self.height_resizing_enabled and not mouse_over_resize: + try: + x1, y1, x2, y2 = ( + self.col_height_resize_bbox[0], + self.col_height_resize_bbox[1], + self.col_height_resize_bbox[2], + self.col_height_resize_bbox[3], + ) + if x >= x1 and y >= y1 and x <= x2 and y <= y2: + self.config(cursor="sb_v_double_arrow") + self.rsz_h = True + mouse_over_resize = True + else: + self.rsz_h = None + except Exception: + self.rsz_h = None + if not mouse_over_resize: + if self.MT.col_selected(self.MT.identify_col(event, allow_end=False)): + self.config(cursor="hand2") + mouse_over_selected = True + if not mouse_over_resize and not mouse_over_selected: + self.MT.reset_mouse_motion_creations() + if self.extra_motion_func is not None: + self.extra_motion_func(event) + + def double_b1(self, event=None): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + if ( + self.double_click_resizing_enabled + and self.width_resizing_enabled + and self.rsz_w is not None + and not self.currently_resizing_width + ): + col = self.rsz_w - 1 + old_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] + new_width = self.set_col_width(col) + self.MT.allow_auto_resize_columns = False + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.column_width_resize_func is not None and old_width != new_width: + self.column_width_resize_func(ResizeEvent("column_width_resize", col, old_width, new_width)) + elif self.col_selection_enabled and self.rsz_h is None and self.rsz_w is None: + c = self.MT.identify_col(x=event.x) + if c < len(self.MT.col_positions) - 1: + if self.MT.single_selection_enabled: + self.select_col(c, redraw=True) + elif self.MT.toggle_selection_enabled: + self.toggle_select_col(c, redraw=True) + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + if ( + self.get_cell_kwargs(datacn, key="dropdown") + or self.get_cell_kwargs(datacn, key="checkbox") + or self.edit_cell_enabled + ): + self.open_cell(event) + self.rsz_w = None + self.mouse_motion(event) + if self.extra_double_b1_func is not None: + self.extra_double_b1_func(event) + + def b1_press(self, event=None): + self.MT.unbind("") + self.focus_set() + self.closed_dropdown = self.mouseclick_outside_editor_or_dropdown_all_canvases() + x = self.canvasx(event.x) + y = self.canvasy(event.y) + c = self.MT.identify_col(x=event.x) + self.b1_pressed_loc = c + if self.check_mouse_position_width_resizers(x, y) is None: + self.rsz_w = None + if self.width_resizing_enabled and self.rsz_w is not None: + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + self.currently_resizing_width = True + x = self.MT.col_positions[self.rsz_w] + line2x = self.MT.col_positions[self.rsz_w - 1] + self.create_resize_line( + x, + 0, + x, + self.current_height, + width=1, + fill=self.resizing_line_fg, + tag="rwl", + ) + self.MT.create_resize_line(x, y1, x, y2, width=1, fill=self.resizing_line_fg, tag="rwl") + self.create_resize_line( + line2x, + 0, + line2x, + self.current_height, + width=1, + fill=self.resizing_line_fg, + tag="rwl2", + ) + self.MT.create_resize_line(line2x, y1, line2x, y2, width=1, fill=self.resizing_line_fg, tag="rwl2") + elif self.height_resizing_enabled and self.rsz_w is None and self.rsz_h is not None: + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + self.currently_resizing_height = True + y = event.y + if y < self.MT.min_header_height: + y = int(self.MT.min_header_height) + self.new_col_height = y + self.create_resize_line(x1, y, x2, y, width=1, fill=self.resizing_line_fg, tag="rhl") + elif self.MT.identify_col(x=event.x, allow_end=False) is None: + self.MT.deselect("all") + elif self.col_selection_enabled and self.rsz_w is None and self.rsz_h is None: + if c < len(self.MT.col_positions) - 1: + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + if ( + self.MT.col_selected(c) + and not self.event_over_dropdown(c, datacn, event, x) + and not self.event_over_checkbox(c, datacn, event, x) + ): + self.dragged_col = DraggedRowColumn( + dragged=c, + to_move=get_seq_without_gaps_at_index(sorted(self.MT.get_selected_cols()), c), + ) + else: + self.being_drawn_rect = ( + 0, + c, + len(self.MT.row_positions) - 1, + c + 1, + "columns", + ) + if self.MT.single_selection_enabled: + self.select_col(c, redraw=True) + elif self.MT.toggle_selection_enabled: + self.toggle_select_col(c, redraw=True) + if self.extra_b1_press_func is not None: + self.extra_b1_press_func(event) + + def b1_motion(self, event): + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + if self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: + x = self.canvasx(event.x) + size = x - self.MT.col_positions[self.rsz_w - 1] + if size >= self.MT.min_column_width and size < self.MT.max_column_width: + self.delete_all_resize_and_ctrl_lines(ctrl_lines=False) + line2x = self.MT.col_positions[self.rsz_w - 1] + self.create_resize_line( + x, + 0, + x, + self.current_height, + width=1, + fill=self.resizing_line_fg, + tag="rwl", + ) + self.MT.create_resize_line(x, y1, x, y2, width=1, fill=self.resizing_line_fg, tag="rwl") + self.create_resize_line( + line2x, + 0, + line2x, + self.current_height, + width=1, + fill=self.resizing_line_fg, + tag="rwl2", + ) + self.MT.create_resize_line( + line2x, + y1, + line2x, + y2, + width=1, + fill=self.resizing_line_fg, + tag="rwl2", + ) + elif self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: + evy = event.y + self.delete_all_resize_and_ctrl_lines(ctrl_lines=False) + if evy > self.current_height: + y = self.MT.canvasy(evy - self.current_height) + if evy > self.MT.max_header_height: + evy = int(self.MT.max_header_height) + y = self.MT.canvasy(evy - self.current_height) + self.new_col_height = evy + self.MT.create_resize_line(x1, y, x2, y, width=1, fill=self.resizing_line_fg, tag="rhl") + else: + y = evy + if y < self.MT.min_header_height: + y = int(self.MT.min_header_height) + self.new_col_height = y + self.create_resize_line(x1, y, x2, y, width=1, fill=self.resizing_line_fg, tag="rhl") + elif ( + self.drag_and_drop_enabled + and self.col_selection_enabled + and self.MT.anything_selected(exclude_cells=True, exclude_rows=True) + and self.rsz_h is None + and self.rsz_w is None + and self.dragged_col is not None + ): + x = self.canvasx(event.x) + if x > 0 and x < self.MT.col_positions[-1]: + self.show_drag_and_drop_indicators( + self.drag_and_drop_motion(event), + y1, + y2, + self.dragged_col.to_move[0], + self.dragged_col.to_move[-1], + ) + elif ( + self.MT.drag_selection_enabled and self.col_selection_enabled and self.rsz_h is None and self.rsz_w is None + ): + need_redraw = False + end_col = self.MT.identify_col(x=event.x) + currently_selected = self.MT.currently_selected() + if end_col < len(self.MT.col_positions) - 1 and currently_selected: + if currently_selected.type_ == "column": + start_col = currently_selected[1] + if end_col >= start_col: + rect = ( + 0, + start_col, + len(self.MT.row_positions) - 1, + end_col + 1, + "columns", + ) + func_event = tuple(range(start_col, end_col + 1)) + elif end_col < start_col: + rect = ( + 0, + end_col, + len(self.MT.row_positions) - 1, + start_col + 1, + "columns", + ) + func_event = tuple(range(end_col, start_col + 1)) + if self.being_drawn_rect != rect: + need_redraw = True + self.MT.delete_selection_rects(delete_current=False) + self.MT.create_selected(*rect) + self.being_drawn_rect = rect + if self.drag_selection_binding_func is not None: + self.drag_selection_binding_func(SelectionBoxEvent("drag_select_columns", func_event)) + if self.scroll_if_event_offscreen(event): + need_redraw = True + if need_redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False) + if self.extra_b1_motion_func is not None: + self.extra_b1_motion_func(event) + + def ctrl_b1_motion(self, event): + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + if ( + self.drag_and_drop_enabled + and self.col_selection_enabled + and self.MT.anything_selected(exclude_cells=True, exclude_rows=True) + and self.rsz_h is None + and self.rsz_w is None + and self.dragged_col is not None + ): + x = self.canvasx(event.x) + if x > 0 and x < self.MT.col_positions[-1]: + self.show_drag_and_drop_indicators( + self.drag_and_drop_motion(event), + y1, + y2, + self.dragged_col.to_move[0], + self.dragged_col.to_move[-1], + ) + elif ( + self.MT.ctrl_select_enabled + and self.MT.drag_selection_enabled + and self.col_selection_enabled + and self.rsz_h is None + and self.rsz_w is None + ): + need_redraw = False + end_col = self.MT.identify_col(x=event.x) + currently_selected = self.MT.currently_selected() + if end_col < len(self.MT.col_positions) - 1 and currently_selected: + if currently_selected.type_ == "column": + start_col = currently_selected[1] + if end_col >= start_col: + rect = ( + 0, + start_col, + len(self.MT.row_positions) - 1, + end_col + 1, + "columns", + ) + func_event = tuple(range(start_col, end_col + 1)) + elif end_col < start_col: + rect = ( + 0, + end_col, + len(self.MT.row_positions) - 1, + start_col + 1, + "columns", + ) + func_event = tuple(range(end_col, start_col + 1)) + if self.being_drawn_rect != rect: + need_redraw = True + if self.being_drawn_rect is not None: + self.MT.delete_selected(*self.being_drawn_rect) + self.MT.create_selected(*rect) + self.being_drawn_rect = rect + if self.drag_selection_binding_func is not None: + self.drag_selection_binding_func(SelectionBoxEvent("drag_select_columns", func_event)) + if self.scroll_if_event_offscreen(event): + need_redraw = True + if need_redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False) + elif not self.MT.ctrl_select_enabled: + self.b1_motion(event) + + def drag_and_drop_motion(self, event): + x = event.x + wend = self.winfo_width() + xcheck = self.xview() + if x >= wend - 0 and len(xcheck) > 1 and xcheck[1] < 1: + if x >= wend + 15: + self.MT.xview_scroll(2, "units") + self.xview_scroll(2, "units") + else: + self.MT.xview_scroll(1, "units") + self.xview_scroll(1, "units") + self.fix_xview() + self.MT.x_move_synced_scrolls("moveto", self.MT.xview()[0]) + self.MT.main_table_redraw_grid_and_text(redraw_header=True) + elif x <= 0 and len(xcheck) > 1 and xcheck[0] > 0: + if x >= -15: + self.MT.xview_scroll(-1, "units") + self.xview_scroll(-1, "units") + else: + self.MT.xview_scroll(-2, "units") + self.xview_scroll(-2, "units") + self.fix_xview() + self.MT.x_move_synced_scrolls("moveto", self.MT.xview()[0]) + self.MT.main_table_redraw_grid_and_text(redraw_header=True) + col = self.MT.identify_col(x=event.x) + if col >= self.dragged_col.to_move[0] and col <= self.dragged_col.to_move[-1]: + xpos = self.MT.col_positions[self.dragged_col.to_move[0]] + else: + if col < self.dragged_col.to_move[0]: + xpos = self.MT.col_positions[col] + else: + xpos = ( + self.MT.col_positions[col + 1] + if len(self.MT.col_positions) - 1 > col + else self.MT.col_positions[col] + ) + return xpos + + def show_drag_and_drop_indicators(self, xpos, y1, y2, start_col, end_col): + self.delete_all_resize_and_ctrl_lines() + self.create_resize_line( + xpos, + 0, + xpos, + self.current_height, + width=3, + fill=self.drag_and_drop_bg, + tag="dd", + ) + self.MT.create_resize_line(xpos, y1, xpos, y2, width=3, fill=self.drag_and_drop_bg, tag="dd") + self.MT.show_ctrl_outline( + start_cell=(start_col, 0), + end_cell=(end_col + 1, len(self.MT.row_positions) - 1), + dash=(), + outline=self.drag_and_drop_bg, + delete_on_timer=False, + ) + + def delete_all_resize_and_ctrl_lines(self, ctrl_lines=True): + self.delete_resize_lines() + self.MT.delete_resize_lines() + if ctrl_lines: + self.MT.delete_ctrl_outlines() + + def scroll_if_event_offscreen(self, event): + xcheck = self.xview() + need_redraw = False + if event.x > self.winfo_width() and len(xcheck) > 1 and xcheck[1] < 1: + try: + self.MT.xview_scroll(1, "units") + self.xview_scroll(1, "units") + except Exception: + pass + self.fix_xview() + self.MT.x_move_synced_scrolls("moveto", self.MT.xview()[0]) + need_redraw = True + elif event.x < 0 and self.canvasx(self.winfo_width()) > 0 and xcheck and xcheck[0] > 0: + try: + self.xview_scroll(-1, "units") + self.MT.xview_scroll(-1, "units") + except Exception: + pass + self.fix_xview() + self.MT.x_move_synced_scrolls("moveto", self.MT.xview()[0]) + need_redraw = True + return need_redraw + + def fix_xview(self): + xcheck = self.xview() + if xcheck and xcheck[0] < 0: + self.MT.set_xviews("moveto", 0) + elif len(xcheck) > 1 and xcheck[1] > 1: + self.MT.set_xviews("moveto", 1) + + def event_over_dropdown(self, c, datacn, event, canvasx): + if ( + event.y < self.MT.header_txt_h + 5 + and self.get_cell_kwargs(datacn, key="dropdown") + and canvasx < self.MT.col_positions[c + 1] + and canvasx > self.MT.col_positions[c + 1] - self.MT.header_txt_h - 4 + ): + return True + return False + + def event_over_checkbox(self, c, datacn, event, canvasx): + if ( + event.y < self.MT.header_txt_h + 5 + and self.get_cell_kwargs(datacn, key="checkbox") + and canvasx < self.MT.col_positions[c] + self.MT.header_txt_h + 4 + ): + return True + return False + + def b1_release(self, event=None): + if self.being_drawn_rect is not None: + self.MT.delete_selected(*self.being_drawn_rect) + to_sel = tuple(self.being_drawn_rect) + self.being_drawn_rect = None + self.MT.create_selected(*to_sel) + self.MT.bind("", self.MT.mousewheel) + if self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: + self.currently_resizing_width = False + new_col_pos = int(self.coords("rwl")[0]) + self.delete_all_resize_and_ctrl_lines(ctrl_lines=False) + old_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] + size = new_col_pos - self.MT.col_positions[self.rsz_w - 1] + if size < self.MT.min_column_width: + new_col_pos = ceil(self.MT.col_positions[self.rsz_w - 1] + self.MT.min_column_width) + elif size > self.MT.max_column_width: + new_col_pos = floor(self.MT.col_positions[self.rsz_w - 1] + self.MT.max_column_width) + increment = new_col_pos - self.MT.col_positions[self.rsz_w] + self.MT.col_positions[self.rsz_w + 1 :] = [ + e + increment for e in islice(self.MT.col_positions, self.rsz_w + 1, len(self.MT.col_positions)) + ] + self.MT.col_positions[self.rsz_w] = new_col_pos + new_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] + self.MT.allow_auto_resize_columns = False + self.MT.recreate_all_selection_boxes() + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.column_width_resize_func is not None and old_width != new_width: + self.column_width_resize_func(ResizeEvent("column_width_resize", self.rsz_w - 1, old_width, new_width)) + elif self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: + self.currently_resizing_height = False + self.delete_all_resize_and_ctrl_lines(ctrl_lines=False) + self.set_height(self.new_col_height, set_TL=True) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + elif ( + self.drag_and_drop_enabled + and self.col_selection_enabled + and self.MT.anything_selected(exclude_cells=True, exclude_rows=True) + and self.rsz_h is None + and self.rsz_w is None + and self.dragged_col is not None + ): + self.delete_all_resize_and_ctrl_lines() + x = event.x + c = self.MT.identify_col(x=x) + orig_selected = self.dragged_col.to_move + if ( + c != self.dragged_col.dragged + and c is not None + and (c < self.dragged_col.to_move[0] or c > self.dragged_col.to_move[-1]) + and len(orig_selected) != len(self.MT.col_positions) - 1 + ): + rm1start = orig_selected[0] + totalcols = len(orig_selected) + extra_func_success = True + if c >= len(self.MT.col_positions) - 1: + c -= 1 + if self.ch_extra_begin_drag_drop_func is not None: + try: + self.ch_extra_begin_drag_drop_func( + BeginDragDropEvent( + "begin_column_header_drag_drop", + tuple(orig_selected), + int(c), + ) + ) + except Exception: + extra_func_success = False + if extra_func_success: + new_selected, dispset = self.MT.move_columns_adjust_options_dict( + c, + rm1start, + totalcols, + move_data=self.column_drag_and_drop_perform, + ) + if self.MT.undo_enabled: + self.MT.undo_storage.append( + zlib.compress(pickle.dumps(("move_cols", orig_selected, new_selected))) + ) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.ch_extra_end_drag_drop_func is not None: + self.ch_extra_end_drag_drop_func( + EndDragDropEvent( + "end_column_header_drag_drop", + orig_selected, + new_selected, + int(c), + ) + ) + self.parentframe.emit_event("<>") + elif self.b1_pressed_loc is not None and self.rsz_w is None and self.rsz_h is None: + c = self.MT.identify_col(x=event.x) + if ( + c is not None + and c < len(self.MT.col_positions) - 1 + and c == self.b1_pressed_loc + and self.b1_pressed_loc != self.closed_dropdown + ): + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + canvasx = self.canvasx(event.x) + if self.event_over_dropdown(c, datacn, event, canvasx) or self.event_over_checkbox( + c, datacn, event, canvasx + ): + self.open_cell(event) + else: + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.b1_pressed_loc = None + self.closed_dropdown = None + self.dragged_col = None + self.currently_resizing_width = False + self.currently_resizing_height = False + self.rsz_w = None + self.rsz_h = None + self.mouse_motion(event) + if self.extra_b1_release_func is not None: + self.extra_b1_release_func(event) + + def readonly_header(self, columns=[], readonly=True): + if isinstance(columns, int): + columns_ = [columns] + else: + columns_ = columns + if not readonly: + for c in columns_: + if c in self.cell_options and "readonly" in self.cell_options[c]: + del self.cell_options[c]["readonly"] + else: + for c in columns_: + if c not in self.cell_options: + self.cell_options[c] = {} + self.cell_options[c]["readonly"] = True + + def toggle_select_col( + self, + column, + add_selection=True, + redraw=True, + run_binding_func=True, + set_as_current=True, + ): + if add_selection: + if self.MT.col_selected(column): + self.MT.deselect(c=column, redraw=redraw) + else: + self.add_selection( + c=column, + redraw=redraw, + run_binding_func=run_binding_func, + set_as_current=set_as_current, + ) + else: + if self.MT.col_selected(column): + self.MT.deselect(c=column, redraw=redraw) + else: + self.select_col(column, redraw=redraw) + + def select_col(self, c, redraw=False): + self.MT.delete_selection_rects() + self.MT.create_selected(0, c, len(self.MT.row_positions) - 1, c + 1, "columns") + self.MT.set_currently_selected(0, c, type_="column") + if redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.selection_binding_func is not None: + self.selection_binding_func(SelectColumnEvent("select_column", int(c))) + + def add_selection(self, c, redraw=False, run_binding_func=True, set_as_current=True): + if set_as_current: + self.MT.set_currently_selected(0, c, type_="column") + self.MT.create_selected(0, c, len(self.MT.row_positions) - 1, c + 1, "columns") + if redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.selection_binding_func is not None and run_binding_func: + self.selection_binding_func(("select_column", c)) + + def get_cell_dimensions(self, datacn): + txt = self.get_valid_cell_data_as_str(datacn, fix=False) + if txt: + self.MT.txt_measure_canvas.itemconfig(self.MT.txt_measure_canvas_text, text=txt, font=self.MT.header_font) + b = self.MT.txt_measure_canvas.bbox(self.MT.txt_measure_canvas_text) + w = b[2] - b[0] + 7 + h = b[3] - b[1] + 5 + else: + w = self.MT.min_column_width + h = self.MT.min_header_height + if datacn in self.cell_options and ( + self.get_cell_kwargs(datacn, key="dropdown") or self.get_cell_kwargs(datacn, key="checkbox") + ): + return w + self.MT.header_txt_h, h + return w, h + + def set_height_of_header_to_text(self, text=None, only_increase=False): + if ( + text is None + and not self.MT._headers + and isinstance(self.MT._headers, list) + or isinstance(self.MT._headers, int) + and self.MT._headers >= len(self.MT.data) + ): + return + qconf = self.MT.txt_measure_canvas.itemconfig + qbbox = self.MT.txt_measure_canvas.bbox + qtxtm = self.MT.txt_measure_canvas_text + new_height = self.MT.min_header_height + self.fix_header() + if text is not None: + if text: + qconf(qtxtm, text=text) + b = qbbox(qtxtm) + h = b[3] - b[1] + 5 + if h > new_height: + new_height = h + else: + if self.MT.all_columns_displayed: + if isinstance(self.MT._headers, list): + iterable = range(len(self.MT._headers)) + else: + iterable = range(len(self.MT.data[self.MT._headers])) + else: + iterable = self.MT.displayed_columns + if isinstance(self.MT._headers, list): + for datacn in iterable: + w_, h = self.get_cell_dimensions(datacn) + if h < self.MT.min_header_height: + h = int(self.MT.min_header_height) + elif h > self.MT.max_header_height: + h = int(self.MT.max_header_height) + if h > new_height: + new_height = h + elif isinstance(self.MT._headers, int): + datarn = self.MT._headers + for datacn in iterable: + txt = self.MT.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True) + if txt: + qconf(qtxtm, text=txt) + b = qbbox(qtxtm) + h = b[3] - b[1] + 5 + else: + h = self.MT.default_header_height + if h < self.MT.min_header_height: + h = int(self.MT.min_header_height) + elif h > self.MT.max_header_height: + h = int(self.MT.max_header_height) + if h > new_height: + new_height = h + space_bot = self.MT.get_space_bot(0) + if new_height > space_bot: + new_height = space_bot + if not only_increase or (only_increase and new_height > self.current_height): + self.set_height(new_height, set_TL=True) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + return new_height + + def set_col_width( + self, + col, + width=None, + only_set_if_too_small=False, + displayed_only=False, + recreate=True, + return_new_width=False, + ): + if col < 0: + return + qconf = self.MT.txt_measure_canvas.itemconfig + qbbox = self.MT.txt_measure_canvas.bbox + qtxtm = self.MT.txt_measure_canvas_text + qtxth = self.MT.txt_h + qfont = self.MT.table_font + self.fix_header() + if width is None: + w = self.MT.min_column_width + hw = self.MT.min_column_width + if self.MT.all_rows_displayed: + if displayed_only: + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + start_row, end_row = self.MT.get_visible_rows(y1, y2) + else: + start_row, end_row = 0, len(self.MT.data) + iterable = range(start_row, end_row) + else: + if displayed_only: + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + start_row, end_row = self.MT.get_visible_rows(y1, y2) + else: + start_row, end_row = 0, len(self.MT.displayed_rows) + iterable = self.MT.displayed_rows[start_row:end_row] + datacn = col if self.MT.all_columns_displayed else self.MT.displayed_columns[col] + # header + hw, hh_ = self.get_cell_dimensions(datacn) + # table + if self.MT.data: + for datarn in iterable: + txt = self.MT.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True) + if txt: + qconf(qtxtm, text=txt, font=qfont) + b = qbbox(qtxtm) + if self.MT.get_cell_kwargs(datarn, datacn, key="dropdown") or self.MT.get_cell_kwargs( + datarn, datacn, key="checkbox" + ): + tw = b[2] - b[0] + qtxth + 7 + else: + tw = b[2] - b[0] + 7 + if tw > w: + w = tw + if w > hw: + new_width = w + else: + new_width = hw + else: + new_width = int(width) + if new_width <= self.MT.min_column_width: + new_width = int(self.MT.min_column_width) + elif new_width > self.MT.max_column_width: + new_width = int(self.MT.max_column_width) + if only_set_if_too_small: + if new_width <= self.MT.col_positions[col + 1] - self.MT.col_positions[col]: + return self.MT.col_positions[col + 1] - self.MT.col_positions[col] + if not return_new_width: + new_col_pos = self.MT.col_positions[col] + new_width + increment = new_col_pos - self.MT.col_positions[col + 1] + self.MT.col_positions[col + 2 :] = [ + e + increment for e in islice(self.MT.col_positions, col + 2, len(self.MT.col_positions)) + ] + self.MT.col_positions[col + 1] = new_col_pos + if recreate: + self.MT.recreate_all_selection_boxes() + return new_width + + def set_width_of_all_cols(self, width=None, only_set_if_too_small=False, recreate=True): + if width is None: + if self.MT.all_columns_displayed: + iterable = range(self.MT.total_data_cols()) + else: + iterable = range(len(self.MT.displayed_columns)) + self.MT.col_positions = list( + accumulate( + chain( + [0], + ( + self.set_col_width( + cn, + only_set_if_too_small=only_set_if_too_small, + recreate=False, + return_new_width=True, + ) + for cn in iterable + ), + ) + ) + ) + elif width is not None: + if self.MT.all_columns_displayed: + self.MT.col_positions = list(accumulate(chain([0], (width for cn in range(self.MT.total_data_cols()))))) + else: + self.MT.col_positions = list( + accumulate(chain([0], (width for cn in range(len(self.MT.displayed_columns))))) + ) + if recreate: + self.MT.recreate_all_selection_boxes() + + def align_cells(self, columns=[], align="global"): + if isinstance(columns, int): + cols = [columns] + else: + cols = columns + if align == "global": + for c in cols: + if c in self.cell_options and "align" in self.cell_options[c]: + del self.cell_options[c]["align"] + else: + for c in cols: + if c not in self.cell_options: + self.cell_options[c] = {} + self.cell_options[c]["align"] = align + + def redraw_highlight_get_text_fg(self, fc, sc, c, c_2, c_3, selections, datacn): + redrawn = False + kwargs = self.get_cell_kwargs(datacn, key="highlight") + if kwargs: + if kwargs[0] is not None: + c_1 = kwargs[0] if kwargs[0].startswith("#") else Color_Map_[kwargs[0]] + if "columns" in selections and c in selections["columns"]: + tf = ( + self.header_selected_columns_fg + if kwargs[1] is None or self.MT.display_selected_fg_over_highlights + else kwargs[1] + ) + if kwargs[0] is not None: + fill = ( + f"#{int((int(c_1[1:3], 16) + int(c_3[1:3], 16)) / 2):02X}" + + f"{int((int(c_1[3:5], 16) + int(c_3[3:5], 16)) / 2):02X}" + + f"{int((int(c_1[5:], 16) + int(c_3[5:], 16)) / 2):02X}" + ) + elif "cells" in selections and c in selections["cells"]: + tf = ( + self.header_selected_cells_fg + if kwargs[1] is None or self.MT.display_selected_fg_over_highlights + else kwargs[1] + ) + if kwargs[0] is not None: + fill = ( + f"#{int((int(c_1[1:3], 16) + int(c_2[1:3], 16)) / 2):02X}" + + f"{int((int(c_1[3:5], 16) + int(c_2[3:5], 16)) / 2):02X}" + + f"{int((int(c_1[5:], 16) + int(c_2[5:], 16)) / 2):02X}" + ) + else: + tf = self.header_fg if kwargs[1] is None else kwargs[1] + if kwargs[0] is not None: + fill = kwargs[0] + if kwargs[0] is not None: + redrawn = self.redraw_highlight( + fc + 1, + 0, + sc, + self.current_height - 1, + fill=fill, + outline=self.header_fg + if self.get_cell_kwargs(datacn, key="dropdown") and self.MT.show_dropdown_borders + else "", + tag="hi", + ) + elif not kwargs: + if "columns" in selections and c in selections["columns"]: + tf = self.header_selected_columns_fg + elif "cells" in selections and c in selections["cells"]: + tf = self.header_selected_cells_fg + else: + tf = self.header_fg + return tf, redrawn + + def redraw_highlight(self, x1, y1, x2, y2, fill, outline, tag): + coords = (x1 - 1 if outline else x1, y1 - 1 if outline else y1, x2, y2) + if self.hidd_high: + iid, showing = self.hidd_high.popitem() + self.coords(iid, coords) + if showing: + self.itemconfig(iid, fill=fill, outline=outline) + else: + self.itemconfig(iid, fill=fill, outline=outline, tag=tag, state="normal") + else: + iid = self.create_rectangle(coords, fill=fill, outline=outline, tag=tag) + self.disp_high[iid] = True + return True + + def redraw_gridline(self, points, fill, width, tag): + if self.hidd_grid: + t, sh = self.hidd_grid.popitem() + self.coords(t, points) + if sh: + self.itemconfig( + t, + fill=fill, + width=width, + tag=tag, + capstyle=tk.BUTT, + joinstyle=tk.ROUND, + ) + else: + self.itemconfig( + t, + fill=fill, + width=width, + tag=tag, + capstyle=tk.BUTT, + joinstyle=tk.ROUND, + state="normal", + ) + self.disp_grid[t] = True + else: + self.disp_grid[self.create_line(points, fill=fill, width=width, tag=tag)] = True + + def redraw_dropdown( + self, + x1, + y1, + x2, + y2, + fill, + outline, + tag, + draw_outline=True, + draw_arrow=True, + dd_is_open=False, + ): + if draw_outline and self.MT.show_dropdown_borders: + self.redraw_highlight(x1 + 1, y1 + 1, x2, y2, fill="", outline=self.header_fg, tag=tag) + if draw_arrow: + topysub = floor(self.MT.header_half_txt_h / 2) + mid_y = y1 + floor(self.MT.min_header_height / 2) + if mid_y + topysub + 1 >= y1 + self.MT.header_txt_h - 1: + mid_y -= 1 + if mid_y - topysub + 2 <= y1 + 4 + topysub: + mid_y -= 1 + ty1 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 3 + ty2 = mid_y - topysub + 3 if dd_is_open else mid_y + topysub + 1 + ty3 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 3 + else: + ty1 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 2 + ty2 = mid_y - topysub + 2 if dd_is_open else mid_y + topysub + 1 + ty3 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 2 + tx1 = x2 - self.MT.header_txt_h + 1 + tx2 = x2 - self.MT.header_half_txt_h - 1 + tx3 = x2 - 3 + if tx2 - tx1 > tx3 - tx2: + tx1 += (tx2 - tx1) - (tx3 - tx2) + elif tx2 - tx1 < tx3 - tx2: + tx1 -= (tx3 - tx2) - (tx2 - tx1) + points = (tx1, ty1, tx2, ty2, tx3, ty3) + if self.hidd_dropdown: + t, sh = self.hidd_dropdown.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=fill) + else: + self.itemconfig(t, fill=fill, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_line( + points, + fill=fill, + width=2, + capstyle=tk.ROUND, + joinstyle=tk.ROUND, + tag=tag, + ) + self.disp_dropdown[t] = True + + def redraw_checkbox(self, x1, y1, x2, y2, fill, outline, tag, draw_check=False): + points = self.MT.get_checkbox_points(x1, y1, x2, y2) + if self.hidd_checkbox: + t, sh = self.hidd_checkbox.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=outline, outline=fill) + else: + self.itemconfig(t, fill=outline, outline=fill, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_polygon(points, fill=outline, outline=fill, tag=tag, smooth=True) + self.disp_checkbox[t] = True + if draw_check: + # draw filled box + x1 = x1 + 4 + y1 = y1 + 4 + x2 = x2 - 3 + y2 = y2 - 3 + points = self.MT.get_checkbox_points(x1, y1, x2, y2, radius=4) + if self.hidd_checkbox: + t, sh = self.hidd_checkbox.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=fill, outline=outline) + else: + self.itemconfig(t, fill=fill, outline=outline, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_polygon(points, fill=fill, outline=outline, tag=tag, smooth=True) + self.disp_checkbox[t] = True + + def redraw_grid_and_text( + self, + last_col_line_pos, + scrollpos_left, + x_stop, + start_col, + end_col, + scrollpos_right, + col_pos_exists, + ): + try: + self.configure( + scrollregion=( + 0, + 0, + last_col_line_pos + self.MT.empty_horizontal + 2, + self.current_height, + ) + ) + except Exception: + return + self.hidd_text.update(self.disp_text) + self.disp_text = {} + self.hidd_high.update(self.disp_high) + self.disp_high = {} + self.hidd_grid.update(self.disp_grid) + self.disp_grid = {} + self.hidd_dropdown.update(self.disp_dropdown) + self.disp_dropdown = {} + self.hidd_checkbox.update(self.disp_checkbox) + self.disp_checkbox = {} + self.visible_col_dividers = {} + self.col_height_resize_bbox = ( + scrollpos_left, + self.current_height - 2, + x_stop, + self.current_height, + ) + draw_x = self.MT.col_positions[start_col] + yend = self.current_height - 5 + if (self.MT.show_vertical_grid or self.width_resizing_enabled) and col_pos_exists: + points = [ + x_stop - 1, + self.current_height - 1, + scrollpos_left - 1, + self.current_height - 1, + scrollpos_left - 1, + -1, + ] + for c in range(start_col + 1, end_col): + draw_x = self.MT.col_positions[c] + if self.width_resizing_enabled: + self.visible_col_dividers[c] = (draw_x - 2, 1, draw_x + 2, yend) + points.extend( + ( + draw_x, + -1, + draw_x, + self.current_height, + draw_x, + -1, + self.MT.col_positions[c + 1] if len(self.MT.col_positions) - 1 > c else draw_x, + -1, + ) + ) + self.redraw_gridline(points=points, fill=self.header_grid_fg, width=1, tag="v") + top = self.canvasy(0) + c_2 = ( + self.header_selected_cells_bg + if self.header_selected_cells_bg.startswith("#") + else Color_Map_[self.header_selected_cells_bg] + ) + c_3 = ( + self.header_selected_columns_bg + if self.header_selected_columns_bg.startswith("#") + else Color_Map_[self.header_selected_columns_bg] + ) + font = self.MT.header_font + selections = self.get_redraw_selections(start_col, end_col) + for c in range(start_col, end_col - 1): + draw_y = self.MT.header_fl_ins + cleftgridln = self.MT.col_positions[c] + crightgridln = self.MT.col_positions[c + 1] + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + fill, dd_drawn = self.redraw_highlight_get_text_fg( + cleftgridln, crightgridln, c, c_2, c_3, selections, datacn + ) + + if datacn in self.cell_options and "align" in self.cell_options[datacn]: + align = self.cell_options[datacn]["align"] + else: + align = self.align + + kwargs = self.get_cell_kwargs(datacn, key="dropdown") + if align == "w": + draw_x = cleftgridln + 3 + if kwargs: + mw = crightgridln - cleftgridln - self.MT.header_txt_h - 2 + self.redraw_dropdown( + cleftgridln, + 0, + crightgridln, + self.current_height - 1, + fill=fill, + outline=fill, + tag="dd", + draw_outline=not dd_drawn, + draw_arrow=mw >= 5, + dd_is_open=kwargs["window"] != "no dropdown open", + ) + else: + mw = crightgridln - cleftgridln - 1 + + elif align == "e": + if kwargs: + mw = crightgridln - cleftgridln - self.MT.header_txt_h - 2 + draw_x = crightgridln - 5 - self.MT.header_txt_h + self.redraw_dropdown( + cleftgridln, + 0, + crightgridln, + self.current_height - 1, + fill=fill, + outline=fill, + tag="dd", + draw_outline=not dd_drawn, + draw_arrow=mw >= 5, + dd_is_open=kwargs["window"] != "no dropdown open", + ) + else: + mw = crightgridln - cleftgridln - 1 + draw_x = crightgridln - 3 + + elif align == "center": + # stop = cleftgridln + 5 + if kwargs: + mw = crightgridln - cleftgridln - self.MT.header_txt_h - 2 + draw_x = cleftgridln + ceil((crightgridln - cleftgridln - self.MT.header_txt_h) / 2) + self.redraw_dropdown( + cleftgridln, + 0, + crightgridln, + self.current_height - 1, + fill=fill, + outline=fill, + tag="dd", + draw_outline=not dd_drawn, + draw_arrow=mw >= 5, + dd_is_open=kwargs["window"] != "no dropdown open", + ) + else: + mw = crightgridln - cleftgridln - 1 + draw_x = cleftgridln + floor((crightgridln - cleftgridln) / 2) + if not kwargs: + kwargs = self.get_cell_kwargs(datacn, key="checkbox") + if kwargs and mw > self.MT.header_txt_h + 2: + box_w = self.MT.header_txt_h + 1 + mw -= box_w + if align == "w": + draw_x += box_w + 1 + elif align == "center": + draw_x += ceil(box_w / 2) + 1 + mw -= 1 + else: + mw -= 3 + try: + draw_check = ( + self.MT._headers[datacn] + if isinstance(self.MT._headers, (list, tuple)) + else self.MT.data[self.MT._headers][datacn] + ) + except Exception: + draw_check = False + self.redraw_checkbox( + cleftgridln + 2, + 2, + cleftgridln + self.MT.header_txt_h + 3, + self.MT.header_txt_h + 3, + fill=fill if kwargs["state"] == "normal" else self.header_grid_fg, + outline="", + tag="cb", + draw_check=draw_check, + ) + lns = self.get_valid_cell_data_as_str(datacn, fix=False).split("\n") + if lns == [""]: + if self.show_default_header_for_empty: + lns = (get_n2a(datacn, self.default_header),) + else: + continue + if mw > self.MT.header_txt_w and not ( + (align == "w" and (draw_x > x_stop)) + or (align == "e" and (draw_x > x_stop)) + or (align == "center" and (cleftgridln + 5 > x_stop)) + ): + for txt in islice( + lns, + self.lines_start_at if self.lines_start_at < len(lns) else len(lns) - 1, + None, + ): + if draw_y > top: + if self.hidd_text: + iid, showing = self.hidd_text.popitem() + self.coords(iid, draw_x, draw_y) + if showing: + self.itemconfig( + iid, + text=txt, + fill=fill, + font=font, + anchor=align, + ) + else: + self.itemconfig( + iid, + text=txt, + fill=fill, + font=font, + anchor=align, + state="normal", + ) + self.tag_raise(iid) + else: + iid = self.create_text( + draw_x, + draw_y, + text=txt, + fill=fill, + font=font, + anchor=align, + tag="t", + ) + self.disp_text[iid] = True + wd = self.bbox(iid) + wd = wd[2] - wd[0] + if wd > mw: + if align == "w": + txt = txt[: int(len(txt) * (mw / wd))] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + while wd[2] - wd[0] > mw: + txt = txt[:-1] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + elif align == "e": + txt = txt[len(txt) - int(len(txt) * (mw / wd)) :] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + while wd[2] - wd[0] > mw: + txt = txt[1:] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + elif align == "center": + self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) + tmod = ceil((len(txt) - int(len(txt) * (mw / wd))) / 2) + txt = txt[tmod - 1 : -tmod] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + while wd[2] - wd[0] > mw: + txt = txt[next(self.c_align_cyc)] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + self.coords(iid, draw_x, draw_y) + draw_y += self.MT.header_xtra_lines_increment + if draw_y - 1 > self.current_height: + break + for dct in (self.hidd_text, self.hidd_high, self.hidd_grid, self.hidd_dropdown, self.hidd_checkbox): + for iid, showing in dct.items(): + if showing: + self.itemconfig(iid, state="hidden") + dct[iid] = False + + def get_redraw_selections(self, startc, endc): + d = defaultdict(list) + for item in chain(self.find_withtag("cells"), self.find_withtag("columns")): + tags = self.gettags(item) + d[tags[0]].append(tuple(int(e) for e in tags[1].split("_") if e)) + d2 = {} + if "cells" in d: + d2["cells"] = {c for c in range(startc, endc) for r1, c1, r2, c2 in d["cells"] if c1 <= c and c2 > c} + if "columns" in d: + d2["columns"] = {c for c in range(startc, endc) for r1, c1, r2, c2 in d["columns"] if c1 <= c and c2 > c} + return d2 + + def open_cell(self, event=None, ignore_existing_editor=False): + if not self.MT.anything_selected() or (not ignore_existing_editor and self.text_editor_id is not None): + return + currently_selected = self.MT.currently_selected() + if not currently_selected: + return + x1 = int(currently_selected[1]) + datacn = x1 if self.MT.all_columns_displayed else self.MT.displayed_columns[x1] + if self.get_cell_kwargs(datacn, key="readonly"): + return + elif self.get_cell_kwargs(datacn, key="dropdown") or self.get_cell_kwargs(datacn, key="checkbox"): + if self.MT.event_opens_dropdown_or_checkbox(event): + if self.get_cell_kwargs(datacn, key="dropdown"): + self.open_dropdown_window(x1, event=event) + elif self.get_cell_kwargs(datacn, key="checkbox"): + self.click_checkbox(x1, datacn) + elif self.edit_cell_enabled: + self.open_text_editor(event=event, c=x1, dropdown=False) + + # displayed indexes + def get_cell_align(self, c): + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + if datacn in self.cell_options and "align" in self.cell_options[datacn]: + align = self.cell_options[datacn]["align"] + else: + align = self.align + return align + + # c is displayed col + def open_text_editor( + self, + event=None, + c=0, + text=None, + state="normal", + see=True, + set_data_on_close=True, + binding=None, + dropdown=False, + ): + text = None + extra_func_key = "??" + if event is None or self.MT.event_opens_dropdown_or_checkbox(event): + if event is not None: + if hasattr(event, "keysym") and event.keysym == "Return": + extra_func_key = "Return" + elif hasattr(event, "keysym") and event.keysym == "F2": + extra_func_key = "F2" + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + text = self.get_cell_data(datacn, none_to_empty_str=True, redirect_int=True) + elif event is not None and ( + (hasattr(event, "keysym") and event.keysym == "BackSpace") or event.keycode in (8, 855638143) + ): + extra_func_key = "BackSpace" + text = "" + elif event is not None and ( + (hasattr(event, "char") and event.char.isalpha()) + or (hasattr(event, "char") and event.char.isdigit()) + or (hasattr(event, "char") and event.char in symbols_set) + ): + extra_func_key = event.char + text = event.char + else: + return False + self.text_editor_loc = c + if self.extra_begin_edit_cell_func is not None: + try: + text = self.extra_begin_edit_cell_func(EditHeaderEvent(c, extra_func_key, text, "begin_edit_header")) + except Exception: + return False + if text is None: + return False + else: + text = text if isinstance(text, str) else f"{text}" + text = "" if text is None else text + if self.MT.cell_auto_resize_enabled: + if self.height_resizing_enabled: + self.set_height_of_header_to_text(text, only_increase=True) + self.set_col_width_run_binding(c) + + if c == self.text_editor_loc and self.text_editor is not None: + self.text_editor.set_text(self.text_editor.get() + "" if not isinstance(text, str) else text) + return + if self.text_editor is not None: + self.destroy_text_editor() + if see: + has_redrawn = self.MT.see(r=0, c=c, keep_yscroll=True, check_cell_visibility=True) + if not has_redrawn: + self.MT.refresh() + self.text_editor_loc = c + x = self.MT.col_positions[c] + 1 + y = 0 + w = self.MT.col_positions[c + 1] - x + h = self.current_height + 1 + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + if text is None: + text = self.get_cell_data(datacn, none_to_empty_str=True, redirect_int=True) + bg, fg = self.header_bg, self.header_fg + self.text_editor = TextEditor( + self, + text=text, + font=self.MT.header_font, + state=state, + width=w, + height=h, + border_color=self.MT.table_selected_cells_border_fg, + show_border=False, + bg=bg, + fg=fg, + popup_menu_font=self.MT.popup_menu_font, + popup_menu_fg=self.MT.popup_menu_fg, + popup_menu_bg=self.MT.popup_menu_bg, + popup_menu_highlight_bg=self.MT.popup_menu_highlight_bg, + popup_menu_highlight_fg=self.MT.popup_menu_highlight_fg, + binding=binding, + align=self.get_cell_align(c), + c=c, + newline_binding=self.text_editor_has_wrapped, + ) + self.text_editor.update_idletasks() + self.text_editor_id = self.create_window((x, y), window=self.text_editor, anchor="nw") + if not dropdown: + self.text_editor.textedit.focus_set() + self.text_editor.scroll_to_bottom() + self.text_editor.textedit.bind("", lambda x: self.text_editor_newline_binding(c=c)) + if USER_OS == "darwin": + self.text_editor.textedit.bind("", lambda x: self.text_editor_newline_binding(c=c)) + for key, func in self.MT.text_editor_user_bound_keys.items(): + self.text_editor.textedit.bind(key, func) + if binding is not None: + self.text_editor.textedit.bind("", lambda x: binding((c, "Tab"))) + self.text_editor.textedit.bind("", lambda x: binding((c, "Return"))) + self.text_editor.textedit.bind("", lambda x: binding((c, "FocusOut"))) + self.text_editor.textedit.bind("", lambda x: binding((c, "Escape"))) + elif binding is None and set_data_on_close: + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((c, "Tab"))) + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((c, "Return"))) + if not dropdown: + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((c, "FocusOut"))) + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((c, "Escape"))) + else: + self.text_editor.textedit.bind("", lambda x: self.destroy_text_editor("Escape")) + return True + + # displayed indexes #just here to receive text editor arg + def text_editor_has_wrapped(self, r=0, c=0, check_lines=None): + if self.width_resizing_enabled: + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + curr_width = self.text_editor.winfo_width() + new_width = curr_width + (self.MT.header_txt_h * 2) + if new_width != curr_width: + self.text_editor.config(width=new_width) + self.set_col_width_run_binding(c, width=new_width, only_set_if_too_small=False) + kwargs = self.get_cell_kwargs(datacn, key="dropdown") + if kwargs: + self.itemconfig(kwargs["canvas_id"], width=new_width) + kwargs["window"].update_idletasks() + kwargs["window"]._reselect() + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False, redraw_table=True) + self.coords(self.text_editor_id, self.MT.col_positions[c] + 1, 0) + + # displayed indexes + def text_editor_newline_binding(self, r=0, c=0, event=None, check_lines=True): + if self.height_resizing_enabled: + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + curr_height = self.text_editor.winfo_height() + if ( + not check_lines + or self.MT.get_lines_cell_height(self.text_editor.get_num_lines() + 1, font=self.MT.header_font) + > curr_height + ): + new_height = curr_height + self.MT.header_xtra_lines_increment + space_bot = self.MT.get_space_bot(0) + if new_height > space_bot: + new_height = space_bot + if new_height != curr_height: + self.text_editor.config(height=new_height) + self.set_height(new_height, set_TL=True) + kwargs = self.get_cell_kwargs(datacn, key="dropdown") + if kwargs: + win_h, anchor = self.get_dropdown_height_anchor(c, new_height) + self.coords( + kwargs["canvas_id"], + self.MT.col_positions[c], + new_height - 1, + ) + self.itemconfig(kwargs["canvas_id"], anchor=anchor, height=win_h) + + def refresh_open_window_positions(self): + if self.text_editor is not None: + c = self.text_editor_loc + self.text_editor.config(height=self.MT.col_positions[c + 1] - self.MT.col_positions[c]) + self.coords( + self.text_editor_id, + 0, + self.MT.col_positions[c], + ) + if self.existing_dropdown_window is not None: + c = self.get_existing_dropdown_coords() + if self.text_editor is None: + text_editor_h = self.MT.col_positions[c + 1] - self.MT.col_positions[c] + anchor = self.itemcget(self.existing_dropdown_canvas_id, "anchor") + win_h = 0 + else: + text_editor_h = self.text_editor.winfo_height() + win_h, anchor = self.get_dropdown_height_anchor(c, text_editor_h) + if anchor == "nw": + self.coords( + self.existing_dropdown_canvas_id, + 0, + self.MT.col_positions[c] + text_editor_h - 1, + ) + # self.itemconfig(self.existing_dropdown_canvas_id, anchor=anchor, height=win_h) + elif anchor == "sw": + self.coords( + self.existing_dropdown_canvas_id, + 0, + self.MT.col_positions[c], + ) + # self.itemconfig(self.existing_dropdown_canvas_id, anchor=anchor, height=win_h) + + def bind_cell_edit(self, enable=True): + if enable: + self.edit_cell_enabled = True + else: + self.edit_cell_enabled = False + + def bind_text_editor_destroy(self, binding, c): + self.text_editor.textedit.bind("", lambda x: binding((c, "Return"))) + self.text_editor.textedit.bind("", lambda x: binding((c, "FocusOut"))) + self.text_editor.textedit.bind("", lambda x: binding((c, "Escape"))) + self.text_editor.textedit.focus_set() + + def destroy_text_editor(self, event=None): + if event is not None and self.extra_end_edit_cell_func is not None and self.text_editor_loc is not None: + self.extra_end_edit_cell_func( + EditHeaderEvent(int(self.text_editor_loc), "Escape", None, "escape_edit_header") + ) + self.text_editor_loc = None + try: + self.delete(self.text_editor_id) + except Exception: + pass + try: + self.text_editor.destroy() + except Exception: + pass + self.text_editor = None + self.text_editor_id = None + if event is not None and len(event) >= 3 and "Escape" in event: + self.focus_set() + + # c is displayed col + def close_text_editor( + self, + editor_info=None, + c=None, + set_data_on_close=True, + event=None, + destroy=True, + move_down=True, + redraw=True, + recreate=True, + ): + if self.focus_get() is None and editor_info: + return + if editor_info is not None and len(editor_info) >= 2 and editor_info[1] == "Escape": + self.destroy_text_editor("Escape") + self.close_dropdown_window(c) + return + if self.text_editor is not None: + self.text_editor_value = self.text_editor.get() + if destroy: + self.destroy_text_editor() + if set_data_on_close: + if c is None and editor_info is not None and len(editor_info) >= 2: + c = editor_info[0] + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + if self.extra_end_edit_cell_func is None and self.input_valid_for_cell(datacn, self.text_editor_value): + self.set_cell_data_undo( + c, + datacn=datacn, + value=self.text_editor_value, + check_input_valid=False, + ) + elif ( + self.extra_end_edit_cell_func is not None + and not self.MT.edit_cell_validation + and self.input_valid_for_cell(datacn, self.text_editor_value) + ): + self.set_cell_data_undo( + c, + datacn=datacn, + value=self.text_editor_value, + check_input_valid=False, + ) + self.extra_end_edit_cell_func( + EditHeaderEvent( + c, + editor_info[1] if len(editor_info) >= 2 else "FocusOut", + f"{self.text_editor_value}", + "end_edit_header", + ) + ) + elif self.extra_end_edit_cell_func is not None and self.MT.edit_cell_validation: + validation = self.extra_end_edit_cell_func( + EditHeaderEvent( + c, + editor_info[1] if len(editor_info) >= 2 else "FocusOut", + f"{self.text_editor_value}", + "end_edit_header", + ) + ) + if validation is not None: + self.text_editor_value = validation + if self.input_valid_for_cell(datacn, self.text_editor_value): + self.set_cell_data_undo( + c, + datacn=datacn, + value=self.text_editor_value, + check_input_valid=False, + ) + if move_down: + pass + self.close_dropdown_window(c) + if recreate: + self.MT.recreate_all_selection_boxes() + if redraw: + self.MT.refresh() + if editor_info is not None and len(editor_info) >= 2 and editor_info[1] != "FocusOut": + self.focus_set() + return "break" + + # internal event use + def set_cell_data_undo( + self, + c=0, + datacn=None, + value="", + cell_resize=True, + undo=True, + redraw=True, + check_input_valid=True, + ): + if datacn is None: + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + if isinstance(self.MT._headers, int): + self.MT.set_cell_data_undo(r=self.MT._headers, c=c, datacn=datacn, value=value, undo=True) + else: + self.fix_header(datacn) + if not check_input_valid or self.input_valid_for_cell(datacn, value): + if self.MT.undo_enabled and undo: + self.MT.undo_storage.append( + zlib.compress( + pickle.dumps( + ( + "edit_header", + {datacn: self.MT._headers[datacn]}, + self.MT.get_boxes(include_current=False), + self.MT.currently_selected(), + ) + ) + ) + ) + self.set_cell_data(datacn=datacn, value=value) + if cell_resize and self.MT.cell_auto_resize_enabled: + if self.height_resizing_enabled: + self.set_height_of_header_to_text(self.get_valid_cell_data_as_str(datacn, fix=False)) + self.set_col_width_run_binding(c) + if redraw: + self.MT.refresh() + self.parentframe.emit_event("<>") + + def set_cell_data(self, datacn=None, value=""): + if isinstance(self.MT._headers, int): + self.MT.set_cell_data(datarn=self.MT._headers, datacn=datacn, value=value) + else: + self.fix_header(datacn) + if self.get_cell_kwargs(datacn, key="checkbox"): + self.MT._headers[datacn] = try_to_bool(value) + else: + self.MT._headers[datacn] = value + + def input_valid_for_cell(self, datacn, value): + if self.get_cell_kwargs(datacn, key="readonly"): + return False + if self.get_cell_kwargs(datacn, key="checkbox"): + return is_bool_like(value) + if self.cell_equal_to(datacn, value): + return False + kwargs = self.get_cell_kwargs(datacn, key="dropdown") + if kwargs and kwargs["validate_input"] and value not in kwargs["values"]: + return False + return True + + def cell_equal_to(self, datacn, value): + self.fix_header(datacn) + if isinstance(self.MT._headers, list): + return self.MT._headers[datacn] == value + elif isinstance(self.MT._headers, int): + return self.MT.cell_equal_to(self.MT._headers, datacn, value) + + def get_cell_data(self, datacn, get_displayed=False, none_to_empty_str=False, redirect_int=False): + if get_displayed: + return self.get_valid_cell_data_as_str(datacn, fix=False) + if redirect_int and isinstance(self.MT._headers, int): # internal use + return self.MT.get_cell_data(self.MT._headers, datacn, none_to_empty_str=True) + if ( + isinstance(self.MT._headers, int) + or not self.MT._headers + or datacn >= len(self.MT._headers) + or (self.MT._headers[datacn] is None and none_to_empty_str) + ): + return "" + return self.MT._headers[datacn] + + def get_valid_cell_data_as_str(self, datacn, fix=True) -> str: + kwargs = self.get_cell_kwargs(datacn, key="dropdown") + if kwargs: + if kwargs["text"] is not None: + return f"{kwargs['text']}" + else: + kwargs = self.get_cell_kwargs(datacn, key="checkbox") + if kwargs: + return f"{kwargs['text']}" + if isinstance(self.MT._headers, int): + return self.MT.get_valid_cell_data_as_str(self.MT._headers, datacn, get_displayed=True) + if fix: + self.fix_header(datacn) + try: + return "" if self.MT._headers[datacn] is None else f"{self.MT._headers[datacn]}" + except Exception: + return "" + + def get_value_for_empty_cell(self, datacn, c_ops=True): + if self.get_cell_kwargs(datacn, key="checkbox", cell=c_ops): + return False + kwargs = self.get_cell_kwargs(datacn, key="dropdown", cell=c_ops) + if kwargs and kwargs["validate_input"] and kwargs["values"]: + return kwargs["values"][0] + return "" + + def get_empty_header_seq(self, end, start=0, c_ops=True): + return [self.get_value_for_empty_cell(datacn, c_ops=c_ops) for datacn in range(start, end)] + + def fix_header(self, datacn=None, fix_values=tuple()): + if isinstance(self.MT._headers, int): + return + if isinstance(self.MT._headers, float): + self.MT._headers = int(self.MT._headers) + return + if not isinstance(self.MT._headers, list): + try: + self.MT._headers = list(self.MT._headers) + except Exception: + self.MT._headers = [] + if isinstance(datacn, int) and datacn >= len(self.MT._headers): + self.MT._headers.extend(self.get_empty_header_seq(end=datacn + 1, start=len(self.MT._headers))) + if fix_values: + for cn, v in enumerate(islice(self.MT._headers, fix_values[0], fix_values[1])): + if not self.input_valid_for_cell(cn, v): + self.MT._headers[cn] = self.get_value_for_empty_cell(cn) + + # displayed indexes + def set_col_width_run_binding(self, c, width=None, only_set_if_too_small=True): + old_width = self.MT.col_positions[c + 1] - self.MT.col_positions[c] + new_width = self.set_col_width(c, width=width, only_set_if_too_small=only_set_if_too_small) + if self.column_width_resize_func is not None and old_width != new_width: + self.column_width_resize_func(ResizeEvent("column_width_resize", c, old_width, new_width)) + + # internal event use + def click_checkbox(self, c, datacn=None, undo=True, redraw=True): + if datacn is None: + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + kwargs = self.get_cell_kwargs(datacn, key="checkbox") + if kwargs["state"] == "normal": + if isinstance(self.MT._headers, list): + value = not self.MT._headers[datacn] if isinstance(self.MT._headers[datacn], bool) else False + elif isinstance(self.MT._headers, int): + value = ( + not self.MT.data[self.MT._headers][datacn] + if isinstance(self.MT.data[self.MT._headers][datacn], bool) + else False + ) + else: + value = False + self.set_cell_data_undo(c, datacn=datacn, value=value, cell_resize=False) + if kwargs["check_function"] is not None: + kwargs["check_function"]( + ( + 0, + c, + "HeaderCheckboxClicked", + self.MT._headers[datacn] + if isinstance(self.MT._headers, list) + else self.MT.get_cell_data(self.MT._headers, datacn), + ) + ) + if self.extra_end_edit_cell_func is not None: + self.extra_end_edit_cell_func( + EditHeaderEvent( + c, + "Return", + self.MT._headers[datacn] + if isinstance(self.MT._headers, list) + else self.MT.get_cell_data(self.MT._headers, datacn), + "end_edit_header", + ) + ) + if redraw: + self.MT.refresh() + + def checkbox_header(self, **kwargs): + self.destroy_opened_dropdown_window() + if "dropdown" in self.options or "checkbox" in self.options: + self.delete_options_dropdown_and_checkbox() + if "checkbox" not in self.options: + self.options["checkbox"] = {} + self.options["checkbox"] = get_checkbox_dict(**kwargs) + total_cols = self.MT.total_data_cols() + if isinstance(self.MT._headers, int): + for datacn in range(total_cols): + self.MT.set_cell_data(datarn=self.MT._headers, datacn=datacn, value=kwargs["checked"]) + else: + for datacn in range(total_cols): + self.set_cell_data(datacn=datacn, value=kwargs["checked"]) + + def dropdown_header(self, **kwargs): + self.destroy_opened_dropdown_window() + if "dropdown" in self.options or "checkbox" in self.options: + self.delete_options_dropdown_and_checkbox() + if "dropdown" not in self.options: + self.options["dropdown"] = {} + self.options["dropdown"] = get_dropdown_dict(**kwargs) + total_cols = self.MT.total_data_cols() + value = ( + kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" + ) + if isinstance(self.MT._headers, int): + for datacn in range(total_cols): + self.MT.set_cell_data(datarn=self.MT._headers, datacn=datacn, value=value) + else: + for datacn in range(total_cols): + self.set_cell_data(datacn=datacn, value=value) + + def create_checkbox(self, datacn=0, **kwargs): + if datacn in self.cell_options and ( + "dropdown" in self.cell_options[datacn] or "checkbox" in self.cell_options[datacn] + ): + self.delete_cell_options_dropdown_and_checkbox(datacn) + if datacn not in self.cell_options: + self.cell_options[datacn] = {} + self.cell_options[datacn]["checkbox"] = get_checkbox_dict(**kwargs) + self.set_cell_data(datacn=datacn, value=kwargs["checked"]) + + def create_dropdown(self, datacn=0, **kwargs): + if datacn in self.cell_options and ( + "dropdown" in self.cell_options[datacn] or "checkbox" in self.cell_options[datacn] + ): + self.delete_cell_options_dropdown_and_checkbox(datacn) + if datacn not in self.cell_options: + self.cell_options[datacn] = {} + self.cell_options[datacn]["dropdown"] = get_dropdown_dict(**kwargs) + self.set_cell_data( + datacn=datacn, + value=kwargs["set_value"] + if kwargs["set_value"] is not None + else kwargs["values"][0] + if kwargs["values"] + else "", + ) + + def get_dropdown_height_anchor(self, c, text_editor_h=None): + win_h = 5 + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + for i, v in enumerate(self.get_cell_kwargs(datacn, key="dropdown")["values"]): + v_numlines = len(v.split("\n") if isinstance(v, str) else f"{v}".split("\n")) + if v_numlines > 1: + win_h += self.MT.header_fl_ins + (v_numlines * self.MT.header_xtra_lines_increment) + 5 # end of cell + else: + win_h += self.MT.min_header_height + if i == 5: + break + if win_h > 500: + win_h = 500 + space_bot = self.MT.get_space_bot(0, text_editor_h) + win_h2 = int(win_h) + if win_h > space_bot: + win_h = space_bot - 1 + if win_h < self.MT.header_txt_h + 5: + win_h = self.MT.header_txt_h + 5 + elif win_h > win_h2: + win_h = win_h2 + return win_h, "nw" + + def open_dropdown_window(self, c, datacn=None, event=None): + self.destroy_text_editor("Escape") + self.destroy_opened_dropdown_window() + if datacn is None: + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + kwargs = self.get_cell_kwargs(datacn, key="dropdown") + if kwargs["state"] == "normal": + if not self.open_text_editor(event=event, c=c, dropdown=True): + return + win_h, anchor = self.get_dropdown_height_anchor(c) + window = self.MT.parentframe.dropdown_class( + self.MT.winfo_toplevel(), + 0, + c, + width=self.MT.col_positions[c + 1] - self.MT.col_positions[c] + 1, + height=win_h, + font=self.MT.header_font, + colors={ + "bg": self.MT.popup_menu_bg, + "fg": self.MT.popup_menu_fg, + "highlight_bg": self.MT.popup_menu_highlight_bg, + "highlight_fg": self.MT.popup_menu_highlight_fg, + }, + outline_color=self.MT.popup_menu_fg, + values=kwargs["values"], + close_dropdown_window=self.close_dropdown_window, + search_function=kwargs["search_function"], + arrowkey_RIGHT=self.MT.arrowkey_RIGHT, + arrowkey_LEFT=self.MT.arrowkey_LEFT, + align="w", + single_index="c", + ) + ypos = self.current_height - 1 + kwargs["canvas_id"] = self.create_window((self.MT.col_positions[c], ypos), window=window, anchor=anchor) + if kwargs["state"] == "normal": + self.text_editor.textedit.bind( + "<>", + lambda x: window.search_and_see( + DropDownModifiedEvent("HeaderComboboxModified", 0, c, self.text_editor.get()) + ), + ) + if kwargs["modified_function"] is not None: + window.modified_function = kwargs["modified_function"] + self.update_idletasks() + try: + self.after(1, lambda: self.text_editor.textedit.focus()) + self.after(2, self.text_editor.scroll_to_bottom()) + except Exception: + return + redraw = False + else: + window.bind("", lambda x: self.close_dropdown_window(c)) + self.update_idletasks() + window.focus_set() + redraw = True + self.existing_dropdown_window = window + kwargs["window"] = window + self.existing_dropdown_canvas_id = kwargs["canvas_id"] + if redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False, redraw_table=False) + + def close_dropdown_window(self, c=None, selection=None, redraw=True): + if c is not None and selection is not None: + datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + kwargs = self.get_cell_kwargs(datacn, key="dropdown") + if kwargs["select_function"] is not None: # user has specified a selection function + kwargs["select_function"]( + EditHeaderEvent(c, "HeaderComboboxSelected", f"{selection}", "end_edit_header") + ) + if self.extra_end_edit_cell_func is None: + self.set_cell_data_undo(c, datacn=datacn, value=selection, redraw=not redraw) + elif self.extra_end_edit_cell_func is not None and self.MT.edit_cell_validation: + validation = self.extra_end_edit_cell_func( + EditHeaderEvent(c, "HeaderComboboxSelected", f"{selection}", "end_edit_header") + ) + if validation is not None: + selection = validation + self.set_cell_data_undo(c, datacn=datacn, value=selection, redraw=not redraw) + elif self.extra_end_edit_cell_func is not None and not self.MT.edit_cell_validation: + self.set_cell_data_undo(c, datacn=datacn, value=selection, redraw=not redraw) + self.extra_end_edit_cell_func( + EditHeaderEvent(c, "HeaderComboboxSelected", f"{selection}", "end_edit_header") + ) + self.focus_set() + self.MT.recreate_all_selection_boxes() + self.destroy_text_editor("Escape") + self.destroy_opened_dropdown_window(c) + if redraw: + self.MT.refresh() + + def get_existing_dropdown_coords(self): + if self.existing_dropdown_window is not None: + return int(self.existing_dropdown_window.c) + return None + + def mouseclick_outside_editor_or_dropdown(self): + closed_dd_coords = self.get_existing_dropdown_coords() + if self.text_editor_loc is not None and self.text_editor is not None: + self.close_text_editor(editor_info=(self.text_editor_loc, "ButtonPress-1")) + else: + self.destroy_text_editor("Escape") + if closed_dd_coords is not None: + self.destroy_opened_dropdown_window( + closed_dd_coords + ) # displayed coords not data, necessary for b1 function + return closed_dd_coords + + def mouseclick_outside_editor_or_dropdown_all_canvases(self): + self.RI.mouseclick_outside_editor_or_dropdown() + self.MT.mouseclick_outside_editor_or_dropdown() + return self.mouseclick_outside_editor_or_dropdown() + + # function can receive two None args + def destroy_opened_dropdown_window(self, c=None, datacn=None): + if c is None and datacn is None and self.existing_dropdown_window is not None: + c = self.get_existing_dropdown_coords() + if c is not None or datacn is not None: + if datacn is None: + datacn_ = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] + else: + datacn_ = datacn + else: + datacn_ = None + try: + self.delete(self.existing_dropdown_canvas_id) + except Exception: + pass + self.existing_dropdown_canvas_id = None + try: + self.existing_dropdown_window.destroy() + except Exception: + pass + kwargs = self.get_cell_kwargs(datacn_, key="dropdown") + if kwargs: + kwargs["canvas_id"] = "no dropdown open" + kwargs["window"] = "no dropdown open" + try: + self.delete(kwargs["canvas_id"]) + except Exception: + pass + self.existing_dropdown_window = None + + def get_cell_kwargs(self, datacn, key="dropdown", cell=True, entire=True): + if cell and datacn in self.cell_options and key in self.cell_options[datacn]: + return self.cell_options[datacn][key] + if entire and key in self.options: + return self.options[key] + return {} + + def delete_options_dropdown(self): + self.destroy_opened_dropdown_window() + if "dropdown" in self.options: + del self.options["dropdown"] + + def delete_options_checkbox(self): + if "checkbox" in self.options: + del self.options["checkbox"] + + def delete_options_dropdown_and_checkbox(self): + self.delete_options_dropdown() + self.delete_options_checkbox() + + def delete_cell_options_dropdown(self, datacn): + self.destroy_opened_dropdown_window(datacn=datacn) + if datacn in self.cell_options and "dropdown" in self.cell_options[datacn]: + del self.cell_options[datacn]["dropdown"] + + def delete_cell_options_checkbox(self, datacn): + if datacn in self.cell_options and "checkbox" in self.cell_options[datacn]: + del self.cell_options[datacn]["checkbox"] + + def delete_cell_options_dropdown_and_checkbox(self, datacn): + self.delete_cell_options_dropdown(datacn) + self.delete_cell_options_checkbox(datacn) diff --git a/thirdparty/tksheet/_tksheet_formatters.py b/thirdparty/tksheet/_tksheet_formatters.py new file mode 100644 index 0000000..6521c03 --- /dev/null +++ b/thirdparty/tksheet/_tksheet_formatters.py @@ -0,0 +1,331 @@ +from __future__ import annotations + +from typing import Any, Union, Dict + +from ._tksheet_vars import ( + falsy, + nonelike, + truthy, +) + + +def is_none_like(n: Any): + if (isinstance(n, str) and n.lower().replace(" ", "") in nonelike) or n in nonelike: + return True + return False + + +def to_int(x: Any, **kwargs): + if isinstance(x, int): + return x + return int(float(x)) + + +def to_float(x: Any, **kwargs): + if isinstance(x, float): + return x + if isinstance(x, str) and x.endswith("%"): + return float(x.replace("%", "")) / 100 + return float(x) + + +def to_bool(val: Any, **kwargs): + if isinstance(val, bool): + return val + if isinstance(val, str): + v = val.lower() + else: + v = val + if "truthy" in kwargs: + _truthy = kwargs["truthy"] + else: + _truthy = truthy + if "falsy" in kwargs: + _falsy = kwargs["falsy"] + else: + _falsy = falsy + if v in _truthy: + return True + elif v in _falsy: + return False + raise ValueError(f'Cannot map "{val}" to bool.') + + +def try_to_bool(val: Any, **kwargs): + try: + return to_bool(val) + except Exception: + return val + + +def is_bool_like(v: Any, **kwargs): + try: + to_bool(v) + return True + except Exception: + return False + + +def to_str(v: Any, **kwargs: Dict) -> str: + return f"{v}" + + +def float_to_str(v: Union[int, float], **kwargs: Dict) -> str: + if isinstance(v, float): + if v.is_integer(): + return f"{int(v)}" + if "decimals" in kwargs and isinstance(kwargs["decimals"], int): + if kwargs["decimals"]: + return f"{round(v, kwargs['decimals'])}" + return f"{int(round(v, kwargs['decimals']))}" + return f"{v}" + + +def percentage_to_str(v: Union[int, float], **kwargs: Dict) -> str: + if isinstance(v, (int, float)): + x = v * 100 + if isinstance(x, float): + if x.is_integer(): + return f"{int(x)}%" + if "decimals" in kwargs and isinstance(kwargs["decimals"], int): + if kwargs["decimals"]: + return f"{round(x, kwargs['decimals'])}%" + return f"{int(round(x, kwargs['decimals']))}%" + return f"{x}%" + + +def bool_to_str(v: Any, **kwargs: Dict) -> str: + return f"{v}" + + +def int_formatter( + datatypes=int, + format_function=to_int, + to_str_function=to_str, + **kwargs, +) -> Dict: + return formatter( + datatypes=datatypes, + format_function=format_function, + to_str_function=to_str_function, + **kwargs, + ) + + +def float_formatter( + datatypes=float, + format_function=to_float, + to_str_function=float_to_str, + decimals=2, + **kwargs, +) -> Dict: + return formatter( + datatypes=datatypes, + format_function=format_function, + to_str_function=to_str_function, + decimals=decimals, + **kwargs, + ) + + +def percentage_formatter( + datatypes=float, + format_function=to_float, + to_str_function=percentage_to_str, + decimals=2, + **kwargs, +) -> Dict: + return formatter( + datatypes=datatypes, + format_function=format_function, + to_str_function=to_str_function, + decimals=decimals, + **kwargs, + ) + + +def bool_formatter( + datatypes=bool, + format_function=to_bool, + to_str_function=bool_to_str, + invalid_value="NA", + truthy_values=truthy, + falsy_values=falsy, + **kwargs, +) -> Dict: + return formatter( + datatypes=datatypes, + format_function=format_function, + to_str_function=to_str_function, + invalid_value=invalid_value, + truthy_values=truthy_values, + falsy_values=falsy_values, + **kwargs, + ) + + +def formatter( + datatypes, + format_function, + to_str_function=to_str, + invalid_value="NaN", + nullable=True, + pre_format_function=None, + post_format_function=None, + clipboard_function=None, + **kwargs, +) -> Dict: + return { + **dict( + datatypes=datatypes, + format_function=format_function, + to_str_function=to_str_function, + invalid_value=invalid_value, + nullable=nullable, + pre_format_function=pre_format_function, + post_format_function=post_format_function, + clipboard_function=clipboard_function, + ), + **kwargs, + } + + +def format_data( + value="", + datatypes=int, + nullable=True, + pre_format_function=None, + format_function=to_int, + post_format_function=None, + **kwargs, +) -> Any: + if pre_format_function: + value = pre_format_function(value) + if nullable and is_none_like(value): + value = None + else: + try: + value = format_function(value, **kwargs) + except Exception: + pass + if post_format_function and isinstance(value, datatypes): + value = post_format_function(value) + return value + + +def data_to_str( + value="", + datatypes=int, + nullable=True, + invalid_value="NaN", + to_str_function=None, + **kwargs, +) -> str: + if not isinstance(value, datatypes): + return invalid_value + if value is None and nullable: + return "" + return to_str_function(value, **kwargs) + + +def get_data_with_valid_check(value="", datatypes=tuple(), invalid_value="NA"): + if isinstance(value, datatypes): + return value + return invalid_value + + +def get_clipboard_data(value="", clipboard_function=None, **kwargs): + if clipboard_function is not None: + return clipboard_function(value, **kwargs) + if isinstance(value, (str, int, float, bool)): + return value + return data_to_str(value, **kwargs) + + +class Formatter: + def __init__( + self, + value, + datatypes=int, + format_function=to_int, + to_str_function=to_str, + nullable=True, + invalid_value="NaN", + pre_format_function=None, + post_format_function=None, + clipboard_function=None, + **kwargs, + ): + if nullable: + if isinstance(datatypes, (list, tuple)): + datatypes = tuple({type_ for type_ in datatypes} | {type(None)}) + else: + datatypes = (datatypes, type(None)) + elif isinstance(datatypes, (list, tuple)) and type(None) in datatypes: + raise TypeError("Non-nullable cells cannot have NoneType as a datatype.") + elif datatypes is type(None): + raise TypeError("Non-nullable cells cannot have NoneType as a datatype.") + self.kwargs = kwargs + self.valid_datatypes = datatypes + self.format_function = format_function + self.to_str_function = to_str_function + self.nullable = nullable + self.invalid_value = invalid_value + self.pre_format_function = pre_format_function + self.post_format_function = post_format_function + self.clipboard_function = clipboard_function + try: + self.value = self.format_data(value) + except Exception: + self.value = f"{value}" + + def __str__(self): + if not self.valid(): + return self.invalid_value + if self.value is None and self.nullable: + return "" + return self.to_str_function(self.value, **self.kwargs) + + def valid(self, value=None) -> bool: + if value is None: + value = self.value + if isinstance(value, self.valid_datatypes): + return True + return False + + def format_data(self, value): + if self.pre_format_function: + value = self.pre_format_function(value) + value = None if (self.nullable and is_none_like(value)) else self.format_function(value, **self.kwargs) + if self.post_format_function and self.valid(value): + value = self.post_format_function(value) + return value + + def get_data_with_valid_check(self): + if self.valid(): + return self.value + return self.invalid_value + + def get_clipboard_data(self): + if self.clipboard_function is not None: + return self.clipboard_function(self.value, **self.kwargs) + if isinstance(self.value, (int, float, bool)): + return self.value + return self.__str__() + + def __eq__(self, __value) -> bool: + # in case of custom formatter class + # compare the values + try: + if hasattr(__value, "value"): + return self.value == __value.value + except Exception: + pass + # if comparing to a string, format the string and compare + if isinstance(__value, str): + try: + return self.value == self.format_data(__value) + except Exception: + pass + # if comparing to anything else, compare the values + return self.value == __value diff --git a/thirdparty/tksheet/_tksheet_main_table.py b/thirdparty/tksheet/_tksheet_main_table.py new file mode 100644 index 0000000..4118524 --- /dev/null +++ b/thirdparty/tksheet/_tksheet_main_table.py @@ -0,0 +1,7246 @@ +from __future__ import annotations + +import bisect +import csv as csv +import io +import pickle +import tkinter as tk +import zlib +from collections import defaultdict, deque +from itertools import accumulate, chain, cycle, islice, product, repeat +from math import ceil, floor +from tkinter import TclError +from typing import Any, Union +from ._tksheet_formatters import ( + data_to_str, + format_data, + get_clipboard_data, + get_data_with_valid_check, + is_bool_like, + try_to_bool, +) +from ._tksheet_other_classes import ( + CtrlKeyEvent, + CurrentlySelectedClass, + DeleteRowColumnEvent, + DeselectionEvent, + DropDownModifiedEvent, + EditCellEvent, + InsertEvent, + PasteEvent, + ResizeEvent, + SelectCellEvent, + SelectionBoxEvent, + TextEditor, + UndoEvent, + get_checkbox_dict, + get_dropdown_dict, + get_seq_without_gaps_at_index, + is_iterable, +) +from ._tksheet_vars import ( + USER_OS, + Color_Map_, + arrowkey_bindings_helper, + ctrl_key, + rc_binding, + symbols_set, +) + + +class MainTable(tk.Canvas): + def __init__(self, *args, **kwargs): + tk.Canvas.__init__( + self, + kwargs["parentframe"], + background=kwargs["table_bg"], + highlightthickness=0, + ) + self.parentframe = kwargs["parentframe"] + self.parentframe_width = 0 + self.parentframe_height = 0 + self.b1_pressed_loc = None + self.existing_dropdown_canvas_id = None + self.existing_dropdown_window = None + self.closed_dropdown = None + self.last_selected = tuple() + self.centre_alignment_text_mod_indexes = (slice(1, None), slice(None, -1)) + self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) + self.synced_scrolls = set() + self.set_cell_sizes_on_zoom = kwargs["set_cell_sizes_on_zoom"] + + self.disp_ctrl_outline = {} + self.disp_text = {} + self.disp_high = {} + self.disp_grid = {} + self.disp_fill_sels = {} + self.disp_bord_sels = {} + self.disp_resize_lines = {} + self.disp_dropdown = {} + self.disp_checkbox = {} + self.hidd_ctrl_outline = {} + self.hidd_text = {} + self.hidd_high = {} + self.hidd_grid = {} + self.hidd_fill_sels = {} + self.hidd_bord_sels = {} + self.hidd_resize_lines = {} + self.hidd_dropdown = {} + self.hidd_checkbox = {} + + self.cell_options = {} + self.col_options = {} + self.row_options = {} + self.options = {} + + self.arrowkey_binding_functions = { + "tab": self.tab_key, + "up": self.arrowkey_UP, + "right": self.arrowkey_RIGHT, + "down": self.arrowkey_DOWN, + "left": self.arrowkey_LEFT, + "prior": self.page_UP, + "next": self.page_DOWN, + } + self.extra_table_rc_menu_funcs = {} + self.extra_index_rc_menu_funcs = {} + self.extra_header_rc_menu_funcs = {} + self.extra_empty_space_rc_menu_funcs = {} + + self.max_undos = kwargs["max_undos"] + self.undo_storage = deque(maxlen=kwargs["max_undos"]) + + self.to_clipboard_delimiter = kwargs["to_clipboard_delimiter"] + self.to_clipboard_quotechar = kwargs["to_clipboard_quotechar"] + self.to_clipboard_lineterminator = kwargs["to_clipboard_lineterminator"] + self.from_clipboard_delimiters = ( + kwargs["from_clipboard_delimiters"] + if isinstance(kwargs["from_clipboard_delimiters"], str) + else "".join(kwargs["from_clipboard_delimiters"]) + ) + self.page_up_down_select_row = kwargs["page_up_down_select_row"] + self.expand_sheet_if_paste_too_big = kwargs["expand_sheet_if_paste_too_big"] + self.paste_insert_column_limit = kwargs["paste_insert_column_limit"] + self.paste_insert_row_limit = kwargs["paste_insert_row_limit"] + self.arrow_key_down_right_scroll_page = kwargs["arrow_key_down_right_scroll_page"] + self.cell_auto_resize_enabled = kwargs["enable_edit_cell_auto_resize"] + self.auto_resize_columns = kwargs["auto_resize_columns"] + self.auto_resize_rows = kwargs["auto_resize_rows"] + self.allow_auto_resize_columns = True + self.allow_auto_resize_rows = True + self.edit_cell_validation = kwargs["edit_cell_validation"] + self.display_selected_fg_over_highlights = kwargs["display_selected_fg_over_highlights"] + self.show_index = kwargs["show_index"] + self.show_header = kwargs["show_header"] + self.selected_rows_to_end_of_window = kwargs["selected_rows_to_end_of_window"] + self.horizontal_grid_to_end_of_window = kwargs["horizontal_grid_to_end_of_window"] + self.vertical_grid_to_end_of_window = kwargs["vertical_grid_to_end_of_window"] + self.empty_horizontal = kwargs["empty_horizontal"] + self.empty_vertical = kwargs["empty_vertical"] + self.show_vertical_grid = kwargs["show_vertical_grid"] + self.show_horizontal_grid = kwargs["show_horizontal_grid"] + self.min_row_height = 0 + self.min_header_height = 0 + self.being_drawn_rect = None + self.extra_motion_func = None + self.extra_b1_press_func = None + self.extra_b1_motion_func = None + self.extra_b1_release_func = None + self.extra_double_b1_func = None + self.extra_rc_func = None + + self.extra_begin_ctrl_c_func = None + self.extra_end_ctrl_c_func = None + + self.extra_begin_ctrl_x_func = None + self.extra_end_ctrl_x_func = None + + self.extra_begin_ctrl_v_func = None + self.extra_end_ctrl_v_func = None + + self.extra_begin_ctrl_z_func = None + self.extra_end_ctrl_z_func = None + + self.extra_begin_delete_key_func = None + self.extra_end_delete_key_func = None + + self.extra_begin_edit_cell_func = None + self.extra_end_edit_cell_func = None + + self.extra_begin_del_rows_rc_func = None + self.extra_end_del_rows_rc_func = None + + self.extra_begin_del_cols_rc_func = None + self.extra_end_del_cols_rc_func = None + + self.extra_begin_insert_cols_rc_func = None + self.extra_end_insert_cols_rc_func = None + + self.extra_begin_insert_rows_rc_func = None + self.extra_end_insert_rows_rc_func = None + + self.text_editor_user_bound_keys = {} + + self.selection_binding_func = None + self.deselection_binding_func = None + self.drag_selection_binding_func = None + self.shift_selection_binding_func = None + self.ctrl_selection_binding_func = None + self.select_all_binding_func = None + + self.single_selection_enabled = False + # with this mode every left click adds the cell to selected cells + self.toggle_selection_enabled = False + self.show_dropdown_borders = kwargs["show_dropdown_borders"] + self.drag_selection_enabled = False + self.select_all_enabled = False + self.undo_enabled = False + self.cut_enabled = False + self.copy_enabled = False + self.paste_enabled = False + self.delete_key_enabled = False + self.rc_select_enabled = False + self.ctrl_select_enabled = False + self.rc_delete_column_enabled = False + self.rc_insert_column_enabled = False + self.rc_delete_row_enabled = False + self.rc_insert_row_enabled = False + self.rc_popup_menus_enabled = False + self.edit_cell_enabled = False + self.text_editor_loc = None + self.show_selected_cells_border = kwargs["show_selected_cells_border"] + self.new_row_width = 0 + self.new_header_height = 0 + self.row_width_resize_bbox = tuple() + self.header_height_resize_bbox = tuple() + self.CH = kwargs["column_headers_canvas"] + self.CH.MT = self + self.CH.RI = kwargs["row_index_canvas"] + self.RI = kwargs["row_index_canvas"] + self.RI.MT = self + self.RI.CH = kwargs["column_headers_canvas"] + self.TL = None # is set from within TopLeftRectangle() __init__ + self.all_columns_displayed = True + self.all_rows_displayed = True + self.align = kwargs["align"] + + self.table_font = [ + kwargs["font"][0], + int(kwargs["font"][1] * kwargs["zoom"] / 100), + kwargs["font"][2], + ] + + self.index_font = [ + kwargs["index_font"][0], + int(kwargs["index_font"][1] * kwargs["zoom"] / 100), + kwargs["index_font"][2], + ] + + self.header_font = [ + kwargs["header_font"][0], + int(kwargs["header_font"][1] * kwargs["zoom"] / 100), + kwargs["header_font"][2], + ] + for fnt in (self.table_font, self.index_font, self.header_font): + if fnt[1] < 1: + fnt[1] = 1 + self.table_font = tuple(self.table_font) + self.index_font = tuple(self.index_font) + self.header_font = tuple(self.header_font) + + self.txt_measure_canvas = tk.Canvas(self) + self.txt_measure_canvas_text = self.txt_measure_canvas.create_text(0, 0, text="", font=self.table_font) + self.text_editor = None + self.text_editor_id = None + + self.max_row_height = float(kwargs["max_row_height"]) + self.max_index_width = float(kwargs["max_index_width"]) + self.max_column_width = float(kwargs["max_column_width"]) + self.max_header_height = float(kwargs["max_header_height"]) + if kwargs["row_index_width"] is None: + self.RI.set_width(70) + self.default_index_width = 70 + else: + self.RI.set_width(kwargs["row_index_width"]) + self.default_index_width = kwargs["row_index_width"] + self.default_header_height = ( + kwargs["header_height"] if isinstance(kwargs["header_height"], str) else "pixels", + kwargs["header_height"] + if isinstance(kwargs["header_height"], int) + else self.get_lines_cell_height(int(kwargs["header_height"]), font=self.header_font), + ) + self.default_column_width = kwargs["column_width"] + self.default_row_height = ( + kwargs["row_height"] if isinstance(kwargs["row_height"], str) else "pixels", + kwargs["row_height"] + if isinstance(kwargs["row_height"], int) + else self.get_lines_cell_height(int(kwargs["row_height"])), + ) + self.set_table_font_help() + self.set_header_font_help() + self.set_index_font_help() + self.data = kwargs["data_reference"] + if isinstance(self.data, (list, tuple)): + self.data = kwargs["data_reference"] + else: + self.data = [] + if not self.data: + if ( + isinstance(kwargs["total_rows"], int) + and isinstance(kwargs["total_cols"], int) + and kwargs["total_rows"] > 0 + and kwargs["total_cols"] > 0 + ): + self.data = [list(repeat("", kwargs["total_cols"])) for i in range(kwargs["total_rows"])] + _header = kwargs["header"] if kwargs["header"] is not None else kwargs["headers"] + if isinstance(_header, int): + self._headers = _header + else: + if _header: + self._headers = _header + else: + self._headers = [] + _row_index = kwargs["index"] if kwargs["index"] is not None else kwargs["row_index"] + if isinstance(_row_index, int): + self._row_index = _row_index + else: + if _row_index: + self._row_index = _row_index + else: + self._row_index = [] + self.displayed_columns = [] + self.displayed_rows = [] + self.col_positions = [0] + self.row_positions = [0] + self.display_rows( + rows=kwargs["displayed_rows"], + all_rows_displayed=kwargs["all_rows_displayed"], + reset_row_positions=False, + deselect_all=False, + ) + self.reset_row_positions() + self.display_columns( + columns=kwargs["displayed_columns"], + all_columns_displayed=kwargs["all_columns_displayed"], + reset_col_positions=False, + deselect_all=False, + ) + self.reset_col_positions() + self.table_grid_fg = kwargs["table_grid_fg"] + self.table_fg = kwargs["table_fg"] + self.table_selected_cells_border_fg = kwargs["table_selected_cells_border_fg"] + self.table_selected_cells_bg = kwargs["table_selected_cells_bg"] + self.table_selected_cells_fg = kwargs["table_selected_cells_fg"] + self.table_selected_rows_border_fg = kwargs["table_selected_rows_border_fg"] + self.table_selected_rows_bg = kwargs["table_selected_rows_bg"] + self.table_selected_rows_fg = kwargs["table_selected_rows_fg"] + self.table_selected_columns_border_fg = kwargs["table_selected_columns_border_fg"] + self.table_selected_columns_bg = kwargs["table_selected_columns_bg"] + self.table_selected_columns_fg = kwargs["table_selected_columns_fg"] + self.table_bg = kwargs["table_bg"] + self.popup_menu_font = kwargs["popup_menu_font"] + self.popup_menu_fg = kwargs["popup_menu_fg"] + self.popup_menu_bg = kwargs["popup_menu_bg"] + self.popup_menu_highlight_bg = kwargs["popup_menu_highlight_bg"] + self.popup_menu_highlight_fg = kwargs["popup_menu_highlight_fg"] + self.rc_popup_menu = None + self.empty_rc_popup_menu = None + self.basic_bindings() + self.create_rc_menus() + + def refresh(self, event=None): + self.main_table_redraw_grid_and_text(True, True) + + def window_configured(self, event): + w = self.parentframe.winfo_width() + if w != self.parentframe_width: + self.parentframe_width = w + self.allow_auto_resize_columns = True + h = self.parentframe.winfo_height() + if h != self.parentframe_height: + self.parentframe_height = h + self.allow_auto_resize_rows = True + self.main_table_redraw_grid_and_text(True, True) + + def basic_bindings(self, enable: bool = True) -> None: + bindings = ( + ("", self, self.window_configured), + ("", self, self.mouse_motion), + ("", self, self.b1_press), + ("", self, self.b1_motion), + ("", self, self.b1_release), + ("", self, self.double_b1), + ("", self, self.mousewheel), + ("", self, self.shift_b1_press), + ("", self.CH, self.CH.shift_b1_press), + ("", self.RI, self.RI.shift_b1_press), + ("", self.RI, self.mousewheel), + (rc_binding, self, self.rc), + (f"<{ctrl_key}-ButtonPress-1>", self, self.ctrl_b1_press), + (f"<{ctrl_key}-ButtonPress-1>", self.CH, self.CH.ctrl_b1_press), + (f"<{ctrl_key}-ButtonPress-1>", self.RI, self.RI.ctrl_b1_press), + (f"<{ctrl_key}-Shift-ButtonPress-1>", self, self.ctrl_shift_b1_press), + (f"<{ctrl_key}-Shift-ButtonPress-1>", self.CH, self.CH.ctrl_shift_b1_press), + (f"<{ctrl_key}-Shift-ButtonPress-1>", self.RI, self.RI.ctrl_shift_b1_press), + (f"<{ctrl_key}-B1-Motion>", self, self.ctrl_b1_motion), + (f"<{ctrl_key}-B1-Motion>", self.CH, self.CH.ctrl_b1_motion), + (f"<{ctrl_key}-B1-Motion>", self.RI, self.RI.ctrl_b1_motion), + ) + all_canvas_bindings = ( + ("", self.shift_mousewheel), + (f"<{ctrl_key}-MouseWheel>", self.ctrl_mousewheel), + (f"<{ctrl_key}-plus>", self.zoom_in), + (f"<{ctrl_key}-equal>", self.zoom_in), + (f"<{ctrl_key}-minus>", self.zoom_out), + ) + all_canvas_linux_bindings = { + ("", self.mousewheel), + ("", self.mousewheel), + ("", self.shift_mousewheel), + ("", self.shift_mousewheel), + (f"<{ctrl_key}-Button-4>", self.ctrl_mousewheel), + (f"<{ctrl_key}-Button-5>", self.ctrl_mousewheel), + } + if enable: + for b in bindings: + b[1].bind(b[0], b[2]) + for b in all_canvas_bindings: + for canvas in (self, self.RI, self.CH): + canvas.bind(b[0], b[1]) + if USER_OS == "linux": + for b in all_canvas_linux_bindings: + for canvas in (self, self.RI, self.CH): + canvas.bind(b[0], b[1]) + else: + for b in bindings: + b[1].unbind(b[0]) + for b in all_canvas_bindings: + for canvas in (self, self.RI, self.CH): + canvas.unbind(b[0]) + if USER_OS == "linux": + for b in all_canvas_linux_bindings: + for canvas in (self, self.RI, self.CH): + canvas.unbind(b[0]) + + def show_ctrl_outline( + self, + canvas="table", + start_cell=(0, 0), + end_cell=(0, 0), + dash=(20, 20), + outline=None, + delete_on_timer=True, + ): + self.create_ctrl_outline( + self.col_positions[start_cell[0]] + 1, + self.row_positions[start_cell[1]] + 1, + self.col_positions[end_cell[0]] - 1, + self.row_positions[end_cell[1]] - 1, + fill="", + dash=dash, + width=3, + outline=self.RI.resizing_line_fg if outline is None else outline, + tag="ctrl", + ) + if delete_on_timer: + self.after(1500, self.delete_ctrl_outlines) + + def create_ctrl_outline(self, x1, y1, x2, y2, fill, dash, width, outline, tag): + if self.hidd_ctrl_outline: + t, sh = self.hidd_ctrl_outline.popitem() + self.coords(t, x1, y1, x2, y2) + if sh: + self.itemconfig(t, fill=fill, dash=dash, width=width, outline=outline, tag=tag) + else: + self.itemconfig( + t, + fill=fill, + dash=dash, + width=width, + outline=outline, + tag=tag, + state="normal", + ) + self.lift(t) + else: + t = self.create_rectangle( + x1, + y1, + x2, + y2, + fill=fill, + dash=dash, + width=width, + outline=outline, + tag=tag, + ) + self.disp_ctrl_outline[t] = True + + def delete_ctrl_outlines(self): + self.hidd_ctrl_outline.update(self.disp_ctrl_outline) + self.disp_ctrl_outline = {} + for t, sh in self.hidd_ctrl_outline.items(): + if sh: + self.itemconfig(t, state="hidden") + self.hidd_ctrl_outline[t] = False + + def get_ctrl_x_c_boxes(self): + currently_selected = self.currently_selected() + boxes = {} + if currently_selected.type_ in ("cell", "column"): + for item in chain(self.find_withtag("cells"), self.find_withtag("columns")): + alltags = self.gettags(item) + boxes[tuple(int(e) for e in alltags[1].split("_") if e)] = alltags[0] + curr_box = self.find_last_selected_box_with_current_from_boxes(currently_selected, boxes) + maxrows = curr_box[2] - curr_box[0] + for box in tuple(boxes): + if box[2] - box[0] != maxrows: + del boxes[box] + return boxes, maxrows + else: + for item in self.find_withtag("rows"): + boxes[tuple(int(e) for e in self.gettags(item)[1].split("_") if e)] = "rows" + return boxes + + def ctrl_c(self, event=None): + currently_selected = self.currently_selected() + if currently_selected: + s = io.StringIO() + writer = csv.writer( + s, + dialect=csv.excel_tab, + delimiter=self.to_clipboard_delimiter, + quotechar=self.to_clipboard_quotechar, + lineterminator=self.to_clipboard_lineterminator, + ) + rows = [] + if currently_selected.type_ in ("cell", "column"): + boxes, maxrows = self.get_ctrl_x_c_boxes() + if self.extra_begin_ctrl_c_func is not None: + try: + self.extra_begin_ctrl_c_func(CtrlKeyEvent("begin_ctrl_c", boxes, currently_selected, tuple())) + except Exception: + return + for rn in range(maxrows): + row = [] + for r1, c1, r2, c2 in boxes: + if r2 - r1 < maxrows: + continue + datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] + for c in range(c1, c2): + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + row.append(self.get_cell_clipboard(datarn, datacn)) + writer.writerow(row) + rows.append(row) + else: + boxes = self.get_ctrl_x_c_boxes() + if self.extra_begin_ctrl_c_func is not None: + try: + self.extra_begin_ctrl_c_func(CtrlKeyEvent("begin_ctrl_c", boxes, currently_selected, tuple())) + except Exception: + return + for r1, c1, r2, c2 in boxes: + for rn in range(r2 - r1): + row = [] + datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] + for c in range(c1, c2): + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + row.append(self.get_cell_clipboard(datarn, datacn)) + writer.writerow(row) + rows.append(row) + for r1, c1, r2, c2 in boxes: + self.show_ctrl_outline(canvas="table", start_cell=(c1, r1), end_cell=(c2, r2)) + self.clipboard_clear() + self.clipboard_append(s.getvalue()) + self.update_idletasks() + if self.extra_end_ctrl_c_func is not None: + self.extra_end_ctrl_c_func(CtrlKeyEvent("end_ctrl_c", boxes, currently_selected, rows)) + + def ctrl_x(self, event=None): + if not self.anything_selected(): + return + undo_storage = {} + s = io.StringIO() + writer = csv.writer( + s, + dialect=csv.excel_tab, + delimiter=self.to_clipboard_delimiter, + quotechar=self.to_clipboard_quotechar, + lineterminator=self.to_clipboard_lineterminator, + ) + currently_selected = self.currently_selected() + rows = [] + changes = 0 + if currently_selected.type_ in ("cell", "column"): + boxes, maxrows = self.get_ctrl_x_c_boxes() + if self.extra_begin_ctrl_x_func is not None: + try: + self.extra_begin_ctrl_x_func(CtrlKeyEvent("begin_ctrl_x", boxes, currently_selected, tuple())) + except Exception: + return + for rn in range(maxrows): + row = [] + for r1, c1, r2, c2 in boxes: + if r2 - r1 < maxrows: + continue + datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] + for c in range(c1, c2): + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + row.append(self.get_cell_clipboard(datarn, datacn)) + writer.writerow(row) + rows.append(row) + for rn in range(maxrows): + for r1, c1, r2, c2 in boxes: + if r2 - r1 < maxrows: + continue + datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] + for c in range(c1, c2): + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + if self.input_valid_for_cell(datarn, datacn, ""): + if self.undo_enabled: + undo_storage[(datarn, datacn)] = self.get_cell_data(datarn, datacn) + self.set_cell_data(datarn, datacn, "") + changes += 1 + else: + boxes = self.get_ctrl_x_c_boxes() + if self.extra_begin_ctrl_x_func is not None: + try: + self.extra_begin_ctrl_x_func(CtrlKeyEvent("begin_ctrl_x", boxes, currently_selected, tuple())) + except Exception: + return + for r1, c1, r2, c2 in boxes: + for rn in range(r2 - r1): + row = [] + datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] + for c in range(c1, c2): + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + row.append(self.get_cell_data(datarn, datacn)) + writer.writerow(row) + rows.append(row) + for r1, c1, r2, c2 in boxes: + for rn in range(r2 - r1): + datarn = (r1 + rn) if self.all_rows_displayed else self.displayed_rows[r1 + rn] + for c in range(c1, c2): + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + if self.input_valid_for_cell(datarn, datacn, ""): + if self.undo_enabled: + undo_storage[(datarn, datacn)] = self.get_cell_data(datarn, datacn) + self.set_cell_data(datarn, datacn, "") + changes += 1 + if changes and self.undo_enabled: + self.undo_storage.append( + zlib.compress(pickle.dumps(("edit_cells", undo_storage, boxes, currently_selected))) + ) + self.clipboard_clear() + self.clipboard_append(s.getvalue()) + self.update_idletasks() + self.refresh() + for r1, c1, r2, c2 in boxes: + self.show_ctrl_outline(canvas="table", start_cell=(c1, r1), end_cell=(c2, r2)) + if self.extra_end_ctrl_x_func is not None: + self.extra_end_ctrl_x_func(CtrlKeyEvent("end_ctrl_x", boxes, currently_selected, rows)) + self.parentframe.emit_event("<>") + + def find_last_selected_box_with_current(self, currently_selected): + if currently_selected.type_ in ("cell", "column"): + boxes, maxrows = self.get_ctrl_x_c_boxes() + else: + boxes = self.get_ctrl_x_c_boxes() + return self.find_last_selected_box_with_current_from_boxes(currently_selected, boxes) + + def find_last_selected_box_with_current_from_boxes(self, currently_selected, boxes): + for (r1, c1, r2, c2), type_ in boxes.items(): + if ( + type_[:2] == currently_selected.type_[:2] + and currently_selected.row >= r1 + and currently_selected.row <= r2 + and currently_selected.column >= c1 + and currently_selected.column <= c2 + ): + if (self.last_selected and self.last_selected == (r1, c1, r2, c2, type_)) or not self.last_selected: + return (r1, c1, r2, c2) + return ( + currently_selected.row, + currently_selected.column, + currently_selected.row + 1, + currently_selected.column + 1, + ) + + def ctrl_v(self, event=None): + if not self.expand_sheet_if_paste_too_big and (len(self.col_positions) == 1 or len(self.row_positions) == 1): + return + currently_selected = self.currently_selected() + if currently_selected: + selected_r = currently_selected[0] + selected_c = currently_selected[1] + elif not currently_selected and not self.expand_sheet_if_paste_too_big: + return + else: + if not self.data: + selected_c, selected_r = 0, 0 + else: + if len(self.col_positions) == 1 and len(self.row_positions) > 1: + selected_c, selected_r = 0, len(self.row_positions) - 1 + elif len(self.row_positions) == 1 and len(self.col_positions) > 1: + selected_c, selected_r = len(self.col_positions) - 1, 0 + elif len(self.row_positions) > 1 and len(self.col_positions) > 1: + selected_c, selected_r = 0, len(self.row_positions) - 1 + try: + data = self.clipboard_get() + except Exception: + return + try: + dialect = csv.Sniffer().sniff(data, delimiters=self.from_clipboard_delimiters) + except Exception: + dialect = csv.excel_tab + data = list(csv.reader(io.StringIO(data), dialect=dialect, skipinitialspace=True)) + if not data: + return + numcols = len(max(data, key=len)) + numrows = len(data) + for rn, r in enumerate(data): + if len(r) < numcols: + data[rn].extend(list(repeat("", numcols - len(r)))) + ( + lastbox_r1, + lastbox_c1, + lastbox_r2, + lastbox_c2, + ) = self.find_last_selected_box_with_current(currently_selected) + lastbox_numrows = lastbox_r2 - lastbox_r1 + lastbox_numcols = lastbox_c2 - lastbox_c1 + if lastbox_numrows > numrows and lastbox_numrows % numrows == 0: + nd = [] + for times in range(int(lastbox_numrows / numrows)): + nd.extend([r.copy() for r in data]) + data.extend(nd) + numrows *= int(lastbox_numrows / numrows) + if lastbox_numcols > numcols and lastbox_numcols % numcols == 0: + for rn, r in enumerate(data): + for times in range(int(lastbox_numcols / numcols)): + data[rn].extend(r.copy()) + numcols *= int(lastbox_numcols / numcols) + undo_storage = {} + if self.expand_sheet_if_paste_too_big: + added_rows = 0 + added_cols = 0 + if selected_c + numcols > len(self.col_positions) - 1: + added_cols = selected_c + numcols - len(self.col_positions) + 1 + if ( + isinstance(self.paste_insert_column_limit, int) + and self.paste_insert_column_limit < len(self.col_positions) - 1 + added_cols + ): + added_cols = self.paste_insert_column_limit - len(self.col_positions) - 1 + if added_cols > 0: + self.insert_col_positions(widths=int(added_cols)) + if not self.all_columns_displayed: + total_data_cols = self.total_data_cols() + self.displayed_columns.extend(list(range(total_data_cols, total_data_cols + added_cols))) + if selected_r + numrows > len(self.row_positions) - 1: + added_rows = selected_r + numrows - len(self.row_positions) + 1 + if ( + isinstance(self.paste_insert_row_limit, int) + and self.paste_insert_row_limit < len(self.row_positions) - 1 + added_rows + ): + added_rows = self.paste_insert_row_limit - len(self.row_positions) - 1 + if added_rows > 0: + self.insert_row_positions(heights=int(added_rows)) + if not self.all_rows_displayed: + total_data_rows = self.total_data_rows() + self.displayed_rows.extend(list(range(total_data_rows, total_data_rows + added_rows))) + added_rows_cols = (added_rows, added_cols) + else: + added_rows_cols = (0, 0) + if selected_c + numcols > len(self.col_positions) - 1: + numcols = len(self.col_positions) - 1 - selected_c + if selected_r + numrows > len(self.row_positions) - 1: + numrows = len(self.row_positions) - 1 - selected_r + if self.extra_begin_ctrl_v_func is not None or self.extra_end_ctrl_v_func is not None: + rows = [ + [data[ndr][ndc] for ndc, c in enumerate(range(selected_c, selected_c + numcols))] + for ndr, r in enumerate(range(selected_r, selected_r + numrows)) + ] + if self.extra_begin_ctrl_v_func is not None: + try: + self.extra_begin_ctrl_v_func(PasteEvent("begin_ctrl_v", currently_selected, rows)) + except Exception: + return + changes = 0 + for ndr, r in enumerate(range(selected_r, selected_r + numrows)): + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + for ndc, c in enumerate(range(selected_c, selected_c + numcols)): + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + if self.input_valid_for_cell(datarn, datacn, data[ndr][ndc]): + if self.undo_enabled: + undo_storage[(datarn, datacn)] = self.get_cell_data(datarn, datacn) + self.set_cell_data(datarn, datacn, data[ndr][ndc]) + changes += 1 + if self.expand_sheet_if_paste_too_big and self.undo_enabled: + self.equalize_data_row_lengths() + self.deselect("all") + if changes and self.undo_enabled: + self.undo_storage.append( + zlib.compress( + pickle.dumps( + ( + "edit_cells_paste", + undo_storage, + { + ( + selected_r, + selected_c, + selected_r + numrows, + selected_c + numcols, + ): "cells" + }, # boxes + currently_selected, + added_rows_cols, + ) + ) + ) + ) + self.create_selected(selected_r, selected_c, selected_r + numrows, selected_c + numcols, "cells") + self.set_currently_selected(selected_r, selected_c, type_="cell") + self.see( + r=selected_r, + c=selected_c, + keep_yscroll=False, + keep_xscroll=False, + bottom_right_corner=False, + check_cell_visibility=True, + redraw=False, + ) + self.refresh() + if self.extra_end_ctrl_v_func is not None: + self.extra_end_ctrl_v_func(PasteEvent("end_ctrl_v", currently_selected, rows)) + self.parentframe.emit_event("<>") + + def delete_key(self, event=None): + if not self.anything_selected(): + return + currently_selected = self.currently_selected() + undo_storage = {} + boxes = {} + for item in chain( + self.find_withtag("cells"), + self.find_withtag("rows"), + self.find_withtag("columns"), + ): + alltags = self.gettags(item) + box = tuple(int(e) for e in alltags[1].split("_") if e) + boxes[box] = alltags[0] + if self.extra_begin_delete_key_func is not None: + try: + self.extra_begin_delete_key_func(CtrlKeyEvent("begin_delete_key", boxes, currently_selected, tuple())) + except Exception: + return + changes = 0 + for r1, c1, r2, c2 in boxes: + for r in range(r1, r2): + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + for c in range(c1, c2): + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + if self.input_valid_for_cell(datarn, datacn, ""): + if self.undo_enabled: + undo_storage[(datarn, datacn)] = self.get_cell_data(datarn, datacn) + self.set_cell_data(datarn, datacn, "") + changes += 1 + if self.extra_end_delete_key_func is not None: + self.extra_end_delete_key_func(CtrlKeyEvent("end_delete_key", boxes, currently_selected, undo_storage)) + if changes and self.undo_enabled: + self.undo_storage.append( + zlib.compress(pickle.dumps(("edit_cells", undo_storage, boxes, currently_selected))) + ) + self.refresh() + self.parentframe.emit_event("<>") + + def move_columns_adjust_options_dict( + self, + col, + to_move_min, + num_cols, + move_data=True, + create_selections=True, + index_type="displayed", + ): + c = int(col) + to_move_max = to_move_min + num_cols + to_del = to_move_max + num_cols + orig_selected = list(range(to_move_min, to_move_min + num_cols)) + if index_type == "displayed": + self.deselect("all", redraw=False) + cws = list(self.diff_gen(self.col_positions)) + if to_move_min > c: + cws[c:c] = cws[to_move_min:to_move_max] + cws[to_move_max:to_del] = [] + else: + cws[c + 1 : c + 1] = cws[to_move_min:to_move_max] + cws[to_move_min:to_move_max] = [] + self.col_positions = list(accumulate(chain([0], (width for width in cws)))) + if c + num_cols > len(self.col_positions): + new_selected = tuple( + range( + len(self.col_positions) - 1 - num_cols, + len(self.col_positions) - 1, + ) + ) + if create_selections: + self.create_selected( + 0, + len(self.col_positions) - 1 - num_cols, + len(self.row_positions) - 1, + len(self.col_positions) - 1, + "columns", + ) + else: + if to_move_min > c: + new_selected = tuple(range(c, c + num_cols)) + if create_selections: + self.create_selected(0, c, len(self.row_positions) - 1, c + num_cols, "columns") + else: + new_selected = tuple(range(c + 1 - num_cols, c + 1)) + if create_selections: + self.create_selected( + 0, + c + 1 - num_cols, + len(self.row_positions) - 1, + c + 1, + "columns", + ) + elif index_type == "data": + if to_move_min > c: + new_selected = tuple(range(c, c + num_cols)) + else: + new_selected = tuple(range(c + 1 - num_cols, c + 1)) + newcolsdct = {t1: t2 for t1, t2 in zip(orig_selected, new_selected)} + if self.all_columns_displayed or index_type != "displayed": + dispset = {} + if to_move_min > c: + if move_data: + extend_idx = to_move_max - 1 + for rn in range(len(self.data)): + if to_move_max > len(self.data[rn]): + self.fix_row_len(rn, extend_idx) + self.data[rn][c:c] = self.data[rn][to_move_min:to_move_max] + self.data[rn][to_move_max:to_del] = [] + self.CH.fix_header(extend_idx) + if isinstance(self._headers, list) and self._headers: + self._headers[c:c] = self._headers[to_move_min:to_move_max] + self._headers[to_move_max:to_del] = [] + self.CH.cell_options = { + newcolsdct[k] if k in newcolsdct else k + num_cols if k < to_move_min and k >= c else k: v + for k, v in self.CH.cell_options.items() + } + self.cell_options = { + (k[0], newcolsdct[k[1]]) + if k[1] in newcolsdct + else (k[0], k[1] + num_cols) + if k[1] < to_move_min and k[1] >= c + else k: v + for k, v in self.cell_options.items() + } + self.col_options = { + newcolsdct[k] if k in newcolsdct else k + num_cols if k < to_move_min and k >= c else k: v + for k, v in self.col_options.items() + } + if index_type != "displayed": + self.displayed_columns = sorted( + int(newcolsdct[k]) + if k in newcolsdct + else k + num_cols + if k < to_move_min and k >= c + else int(k) + for k in self.displayed_columns + ) + else: + c += 1 + if move_data: + extend_idx = c - 1 + for rn in range(len(self.data)): + if c > len(self.data[rn]): + self.fix_row_len(rn, extend_idx) + self.data[rn][c:c] = self.data[rn][to_move_min:to_move_max] + self.data[rn][to_move_min:to_move_max] = [] + self.CH.fix_header(extend_idx) + if isinstance(self._headers, list) and self._headers: + self._headers[c:c] = self._headers[to_move_min:to_move_max] + self._headers[to_move_min:to_move_max] = [] + self.CH.cell_options = { + newcolsdct[k] if k in newcolsdct else k - num_cols if k < c and k > to_move_min else k: v + for k, v in self.CH.cell_options.items() + } + self.cell_options = { + (k[0], newcolsdct[k[1]]) + if k[1] in newcolsdct + else (k[0], k[1] - num_cols) + if k[1] < c and k[1] > to_move_min + else k: v + for k, v in self.cell_options.items() + } + self.col_options = { + newcolsdct[k] if k in newcolsdct else k - num_cols if k < c and k > to_move_min else k: v + for k, v in self.col_options.items() + } + if index_type != "displayed": + self.displayed_columns = sorted( + int(newcolsdct[k]) if k in newcolsdct else k - num_cols if k < c and k > to_move_min else int(k) + for k in self.displayed_columns + ) + else: + # moves data around, not displayed columns indexes + # which remain sorted and the same after drop and drop + if to_move_min > c: + dispset = { + a: b + for a, b in zip( + self.displayed_columns, + ( + self.displayed_columns[:c] + + self.displayed_columns[to_move_min : to_move_min + num_cols] + + self.displayed_columns[c:to_move_min] + + self.displayed_columns[to_move_min + num_cols :] + ), + ) + } + else: + dispset = { + a: b + for a, b in zip( + self.displayed_columns, + ( + self.displayed_columns[:to_move_min] + + self.displayed_columns[to_move_min + num_cols : c + 1] + + self.displayed_columns[to_move_min : to_move_min + num_cols] + + self.displayed_columns[c + 1 :] + ), + ) + } + # has to pick up elements from all over the place in the original row + # building an entirely new row is best due to permutations of hidden columns + if move_data: + max_len = max(chain(dispset, dispset.values())) + 1 + max_idx = max_len - 1 + for rn in range(len(self.data)): + if max_len > len(self.data[rn]): + self.fix_row_len(rn, max_idx) + new = [] + idx = 0 + done = set() + while len(new) < len(self.data[rn]): + if idx in dispset and idx not in done: + new.append(self.data[rn][dispset[idx]]) + done.add(idx) + elif idx not in done: + new.append(self.data[rn][idx]) + idx += 1 + else: + idx += 1 + self.data[rn] = new + self.CH.fix_header(max_idx) + if isinstance(self._headers, list) and self._headers: + new = [] + idx = 0 + done = set() + while len(new) < len(self._headers): + if idx in dispset and idx not in done: + new.append(self._headers[dispset[idx]]) + done.add(idx) + elif idx not in done: + new.append(self._headers[idx]) + idx += 1 + else: + idx += 1 + self._headers = new + dispset = {b: a for a, b in dispset.items()} + self.CH.cell_options = {dispset[k] if k in dispset else k: v for k, v in self.CH.cell_options.items()} + self.cell_options = { + (k[0], dispset[k[1]]) if k[1] in dispset else k: v for k, v in self.cell_options.items() + } + self.col_options = {dispset[k] if k in dispset else k: v for k, v in self.col_options.items()} + return new_selected, {b: a for a, b in dispset.items()} + + def move_rows_adjust_options_dict( + self, + row, + to_move_min, + num_rows, + move_data=True, + create_selections=True, + index_type="displayed", + ): + r = int(row) + to_move_max = to_move_min + num_rows + to_del = to_move_max + num_rows + orig_selected = list(range(to_move_min, to_move_min + num_rows)) + if index_type == "displayed": + self.deselect("all", redraw=False) + rhs = list(self.diff_gen(self.row_positions)) + if to_move_min > r: + rhs[r:r] = rhs[to_move_min:to_move_max] + rhs[to_move_max:to_del] = [] + else: + rhs[r + 1 : r + 1] = rhs[to_move_min:to_move_max] + rhs[to_move_min:to_move_max] = [] + self.row_positions = list(accumulate(chain([0], (height for height in rhs)))) + if r + num_rows > len(self.row_positions): + new_selected = tuple( + range( + len(self.row_positions) - 1 - num_rows, + len(self.row_positions) - 1, + ) + ) + if create_selections: + self.create_selected( + len(self.row_positions) - 1 - num_rows, + 0, + len(self.row_positions) - 1, + len(self.col_positions) - 1, + "rows", + ) + else: + if to_move_min > r: + new_selected = tuple(range(r, r + num_rows)) + if create_selections: + self.create_selected(r, 0, r + num_rows, len(self.col_positions) - 1, "rows") + else: + new_selected = tuple(range(r + 1 - num_rows, r + 1)) + if create_selections: + self.create_selected( + r + 1 - num_rows, + 0, + r + 1, + len(self.col_positions) - 1, + "rows", + ) + elif index_type == "data": + if to_move_min > r: + new_selected = tuple(range(r, r + num_rows)) + else: + new_selected = tuple(range(r + 1 - num_rows, r + 1)) + newrowsdct = {t1: t2 for t1, t2 in zip(orig_selected, new_selected)} + if self.all_rows_displayed or index_type != "displayed": + dispset = {} + if to_move_min > r: + if move_data: + extend_idx = to_move_max - 1 + if to_move_max > len(self.data): + self.fix_data_len(extend_idx) + self.data[r:r] = self.data[to_move_min:to_move_max] + self.data[to_move_max:to_del] = [] + self.RI.fix_index(extend_idx) + if isinstance(self._row_index, list) and self._row_index: + self._row_index[r:r] = self._row_index[to_move_min:to_move_max] + self._row_index[to_move_max:to_del] = [] + self.RI.cell_options = { + newrowsdct[k] if k in newrowsdct else k + num_rows if k < to_move_min and k >= r else k: v + for k, v in self.RI.cell_options.items() + } + self.cell_options = { + (newrowsdct[k[0]], k[1]) + if k[0] in newrowsdct + else (k[0] + num_rows, k[1]) + if k[0] < to_move_min and k[0] >= r + else k: v + for k, v in self.cell_options.items() + } + self.row_options = { + newrowsdct[k] if k in newrowsdct else k + num_rows if k < to_move_min and k >= r else k: v + for k, v in self.row_options.items() + } + if index_type != "displayed": + self.displayed_rows = sorted( + int(newrowsdct[k]) + if k in newrowsdct + else k + num_rows + if k < to_move_min and k >= r + else int(k) + for k in self.displayed_rows + ) + else: + r += 1 + if move_data: + extend_idx = r - 1 + if r > len(self.data): + self.fix_data_len(extend_idx) + self.data[r:r] = self.data[to_move_min:to_move_max] + self.data[to_move_min:to_move_max] = [] + self.RI.fix_index(extend_idx) + if isinstance(self._row_index, list) and self._row_index: + self._row_index[r:r] = self._row_index[to_move_min:to_move_max] + self._row_index[to_move_min:to_move_max] = [] + self.RI.cell_options = { + newrowsdct[k] if k in newrowsdct else k - num_rows if k < r and k > to_move_min else k: v + for k, v in self.RI.cell_options.items() + } + self.cell_options = { + (newrowsdct[k[0]], k[1]) + if k[0] in newrowsdct + else (k[0] - num_rows, k[1]) + if k[0] < r and k[0] > to_move_min + else k: v + for k, v in self.cell_options.items() + } + self.row_options = { + newrowsdct[k] if k in newrowsdct else k - num_rows if k < r and k > to_move_min else k: v + for k, v in self.row_options.items() + } + if index_type != "displayed": + self.displayed_rows = sorted( + int(newrowsdct[k]) if k in newrowsdct else k - num_rows if k < r and k > to_move_min else int(k) + for k in self.displayed_rows + ) + else: + # moves data around, not displayed rows indexes + # which remain sorted and the same after drop and drop + if to_move_min > r: + dispset = { + a: b + for a, b in zip( + self.displayed_rows, + ( + self.displayed_rows[:r] + + self.displayed_rows[to_move_min : to_move_min + num_rows] + + self.displayed_rows[r:to_move_min] + + self.displayed_rows[to_move_min + num_rows :] + ), + ) + } + else: + dispset = { + a: b + for a, b in zip( + self.displayed_rows, + ( + self.displayed_rows[:to_move_min] + + self.displayed_rows[to_move_min + num_rows : r + 1] + + self.displayed_rows[to_move_min : to_move_min + num_rows] + + self.displayed_rows[r + 1 :] + ), + ) + } + # has to pick up rows from all over the place in the original sheet + # building an entirely new sheet is best due to permutations of hidden rows + if move_data: + max_len = max(chain(dispset, dispset.values())) + 1 + if len(self.data) < max_len: + self.fix_data_len(max_len - 1) + new = [] + idx = 0 + done = set() + while len(new) < len(self.data): + if idx in dispset and idx not in done: + new.append(self.data[dispset[idx]]) + done.add(idx) + elif idx not in done: + new.append(self.data[idx]) + idx += 1 + else: + idx += 1 + self.data = new + self.RI.fix_index(max_len - 1) + if isinstance(self._row_index, list) and self._row_index: + new = [] + idx = 0 + done = set() + while len(new) < len(self._row_index): + if idx in dispset and idx not in done: + new.append(self._row_index[dispset[idx]]) + done.add(idx) + elif idx not in done: + new.append(self._row_index[idx]) + idx += 1 + else: + idx += 1 + self._row_index = new + dispset = {b: a for a, b in dispset.items()} + self.RI.cell_options = {dispset[k] if k in dispset else k: v for k, v in self.RI.cell_options.items()} + self.cell_options = { + (dispset[k[0]], k[1]) if k[0] in dispset else k: v for k, v in self.cell_options.items() + } + self.row_options = {dispset[k] if k in dispset else k: v for k, v in self.row_options.items()} + return new_selected, {b: a for a, b in dispset.items()} + + def ctrl_z(self, event=None): + if not self.undo_storage: + return + if not isinstance(self.undo_storage[-1], (tuple, dict)): + undo_storage = pickle.loads(zlib.decompress(self.undo_storage[-1])) + else: + undo_storage = self.undo_storage[-1] + self.deselect("all") + if self.extra_begin_ctrl_z_func is not None: + try: + self.extra_begin_ctrl_z_func(UndoEvent("begin_ctrl_z", undo_storage[0], undo_storage)) + except Exception: + return + self.undo_storage.pop() + if undo_storage[0] in ("edit_header",): + for c, v in undo_storage[1].items(): + self._headers[c] = v + self.reselect_from_get_boxes(undo_storage[2]) + self.set_currently_selected(0, undo_storage[3][1], type_="column") + + if undo_storage[0] in ("edit_index",): + for r, v in undo_storage[1].items(): + self._row_index[r] = v + self.reselect_from_get_boxes(undo_storage[2]) + self.set_currently_selected(0, undo_storage[3][1], type_="row") + + if undo_storage[0] in ("edit_cells", "edit_cells_paste"): + for (datarn, datacn), v in undo_storage[1].items(): + self.set_cell_data(datarn, datacn, v) + if undo_storage[0] == "edit_cells_paste" and self.expand_sheet_if_paste_too_big: + if undo_storage[4][0] > 0: + self.del_row_positions( + len(self.row_positions) - 1 - undo_storage[4][0], + undo_storage[4][0], + ) + self.data[:] = self.data[: -undo_storage[4][0]] + if not self.all_rows_displayed: + self.displayed_rows[:] = self.displayed_rows[: -undo_storage[4][0]] + if undo_storage[4][1] > 0: + quick_added_cols = undo_storage[4][1] + self.del_col_positions(len(self.col_positions) - 1 - quick_added_cols, quick_added_cols) + for rn in range(len(self.data)): + self.data[rn][:] = self.data[rn][:-quick_added_cols] + if not self.all_columns_displayed: + self.displayed_columns[:] = self.displayed_columns[:-quick_added_cols] + self.reselect_from_get_boxes(undo_storage[2]) + if undo_storage[3]: + self.set_currently_selected( + undo_storage[3].row, + undo_storage[3].column, + type_=undo_storage[3].type_, + ) + self.see( + r=undo_storage[3].row, + c=undo_storage[3].column, + keep_yscroll=False, + keep_xscroll=False, + bottom_right_corner=False, + check_cell_visibility=True, + redraw=False, + ) + + elif undo_storage[0] == "move_cols": + c = undo_storage[1][0] + to_move_min = undo_storage[2][0] + totalcols = len(undo_storage[2]) + if to_move_min < c: + c += totalcols - 1 + self.move_columns_adjust_options_dict(c, to_move_min, totalcols) + self.see( + r=0, + c=c, + keep_yscroll=False, + keep_xscroll=False, + bottom_right_corner=False, + check_cell_visibility=True, + redraw=False, + ) + + elif undo_storage[0] == "move_rows": + r = undo_storage[1][0] + to_move_min = undo_storage[2][0] + totalrows = len(undo_storage[2]) + if to_move_min < r: + r += totalrows - 1 + self.move_rows_adjust_options_dict(r, to_move_min, totalrows) + self.see( + r=r, + c=0, + keep_yscroll=False, + keep_xscroll=False, + bottom_right_corner=False, + check_cell_visibility=True, + redraw=False, + ) + + elif undo_storage[0] == "insert_rows": + self.displayed_rows = undo_storage[1]["displayed_rows"] + self.data[ + undo_storage[1]["data_row_num"] : undo_storage[1]["data_row_num"] + undo_storage[1]["numrows"] + ] = [] + try: + self._row_index[ + undo_storage[1]["data_row_num"] : undo_storage[1]["data_row_num"] + undo_storage[1]["numrows"] + ] = [] + except Exception: + pass + self.del_row_positions( + undo_storage[1]["sheet_row_num"], + undo_storage[1]["numrows"], + deselect_all=False, + ) + to_del = set( + range( + undo_storage[1]["sheet_row_num"], + undo_storage[1]["sheet_row_num"] + undo_storage[1]["numrows"], + ) + ) + numrows = undo_storage[1]["numrows"] + idx = undo_storage[1]["sheet_row_num"] + undo_storage[1]["numrows"] + self.cell_options = { + (rn if rn < idx else rn - numrows, cn): t2 + for (rn, cn), t2 in self.cell_options.items() + if rn not in to_del + } + self.row_options = { + rn if rn < idx else rn - numrows: t for rn, t in self.row_options.items() if rn not in to_del + } + self.RI.cell_options = { + rn if rn < idx else rn - numrows: t for rn, t in self.RI.cell_options.items() if rn not in to_del + } + if len(self.row_positions) > 1: + start_row = ( + undo_storage[1]["sheet_row_num"] + if undo_storage[1]["sheet_row_num"] < len(self.row_positions) - 1 + else undo_storage[1]["sheet_row_num"] - 1 + ) + self.RI.select_row(start_row) + self.see( + r=start_row, + c=0, + keep_yscroll=False, + keep_xscroll=False, + bottom_right_corner=False, + check_cell_visibility=True, + redraw=False, + ) + + elif undo_storage[0] == "insert_cols": + self.displayed_columns = undo_storage[1]["displayed_columns"] + qx = undo_storage[1]["data_col_num"] + qnum = undo_storage[1]["numcols"] + for rn in range(len(self.data)): + self.data[rn][qx : qx + qnum] = [] + try: + self._headers[qx : qx + qnum] = [] + except Exception: + pass + self.del_col_positions( + undo_storage[1]["sheet_col_num"], + undo_storage[1]["numcols"], + deselect_all=False, + ) + to_del = set( + range( + undo_storage[1]["sheet_col_num"], + undo_storage[1]["sheet_col_num"] + undo_storage[1]["numcols"], + ) + ) + numcols = undo_storage[1]["numcols"] + idx = undo_storage[1]["sheet_col_num"] + undo_storage[1]["numcols"] + self.cell_options = { + (rn, cn if cn < idx else cn - numcols): t2 + for (rn, cn), t2 in self.cell_options.items() + if cn not in to_del + } + self.col_options = { + cn if cn < idx else cn - numcols: t for cn, t in self.col_options.items() if cn not in to_del + } + self.CH.cell_options = { + cn if cn < idx else cn - numcols: t for cn, t in self.CH.cell_options.items() if cn not in to_del + } + if len(self.col_positions) > 1: + start_col = ( + undo_storage[1]["sheet_col_num"] + if undo_storage[1]["sheet_col_num"] < len(self.col_positions) - 1 + else undo_storage[1]["sheet_col_num"] - 1 + ) + self.CH.select_col(start_col) + self.see( + r=0, + c=start_col, + keep_yscroll=False, + keep_xscroll=False, + bottom_right_corner=False, + check_cell_visibility=True, + redraw=False, + ) + + elif undo_storage[0] == "delete_rows": + self.displayed_rows = undo_storage[1]["displayed_rows"] + for rn, r in reversed(undo_storage[1]["deleted_rows"]): + self.data.insert(rn, r) + for rn, h in reversed(tuple(undo_storage[1]["rowheights"].items())): + self.insert_row_position(idx=rn, height=h) + self.cell_options = undo_storage[1]["cell_options"] + self.row_options = undo_storage[1]["row_options"] + self.RI.cell_options = undo_storage[1]["RI_cell_options"] + for rn, r in reversed(undo_storage[1]["deleted_index_values"]): + try: + self._row_index.insert(rn, r) + except Exception: + continue + self.reselect_from_get_boxes(undo_storage[1]["selection_boxes"]) + + elif undo_storage[0] == "delete_cols": + self.displayed_columns = undo_storage[1]["displayed_columns"] + self.cell_options = undo_storage[1]["cell_options"] + self.col_options = undo_storage[1]["col_options"] + self.CH.cell_options = undo_storage[1]["CH_cell_options"] + for cn, w in reversed(tuple(undo_storage[1]["colwidths"].items())): + self.insert_col_position(idx=cn, width=w) + for cn, rowdict in reversed(tuple(undo_storage[1]["deleted_cols"].items())): + for rn, v in rowdict.items(): + try: + self.data[rn].insert(cn, v) + except Exception: + continue + for cn, v in reversed(tuple(undo_storage[1]["deleted_header_values"].items())): + try: + self._headers.insert(cn, v) + except Exception: + continue + self.reselect_from_get_boxes(undo_storage[1]["selection_boxes"]) + self.refresh() + if self.extra_end_ctrl_z_func is not None: + self.extra_end_ctrl_z_func(UndoEvent("end_ctrl_z", undo_storage[0], undo_storage)) + self.parentframe.emit_event("<>") + + def bind_arrowkeys(self, keys: dict = {}): + for canvas in (self, self.parentframe, self.CH, self.RI, self.TL): + for k, func in keys.items(): + canvas.bind(f"<{arrowkey_bindings_helper[k.lower()]}>", func) + + def unbind_arrowkeys(self, keys: dict = {}): + for canvas in (self, self.parentframe, self.CH, self.RI, self.TL): + for k, func in keys.items(): + canvas.unbind(f"<{arrowkey_bindings_helper[k.lower()]}>") + + def see( + self, + r=None, + c=None, + keep_yscroll=False, + keep_xscroll=False, + bottom_right_corner=False, + check_cell_visibility=True, + redraw=True, + r_pc=0.0, + c_pc=0.0, + ): + need_redraw = False + yvis, xvis = False, False + if check_cell_visibility: + yvis, xvis = self.cell_completely_visible(r=r, c=c, separate_axes=True) + if not yvis and len(self.row_positions) > 1: + if bottom_right_corner: + if r is not None and not keep_yscroll: + winfo_height = self.winfo_height() + if self.row_positions[r + 1] - self.row_positions[r] > winfo_height: + y = self.row_positions[r] + else: + y = self.row_positions[r + 1] + 1 - winfo_height + args = [ + "moveto", + y / (self.row_positions[-1] + self.empty_vertical), + ] + if args[1] > 1: + args[1] = args[1] - 1 + self.set_yviews(*args, redraw=False) + need_redraw = True + else: + if r is not None and not keep_yscroll: + y = self.row_positions[r] + ((self.row_positions[r + 1] - self.row_positions[r]) * r_pc) + args = [ + "moveto", + y / (self.row_positions[-1] + self.empty_vertical), + ] + if args[1] > 1: + args[1] = args[1] - 1 + self.set_yviews(*args, redraw=False) + need_redraw = True + if not xvis and len(self.col_positions) > 1: + if bottom_right_corner: + if c is not None and not keep_xscroll: + winfo_width = self.winfo_width() + if self.col_positions[c + 1] - self.col_positions[c] > winfo_width: + x = self.col_positions[c] + else: + x = self.col_positions[c + 1] + 1 - winfo_width + args = [ + "moveto", + x / (self.col_positions[-1] + self.empty_horizontal), + ] + self.set_xviews(*args, redraw=False) + need_redraw = True + else: + if c is not None and not keep_xscroll: + x = self.col_positions[c] + ((self.col_positions[c + 1] - self.col_positions[c]) * c_pc) + args = [ + "moveto", + x / (self.col_positions[-1] + self.empty_horizontal), + ] + self.set_xviews(*args, redraw=False) + need_redraw = True + if redraw and need_redraw: + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + return True + return False + + def get_cell_coords(self, r=None, c=None): + return ( + 0 if not c else self.col_positions[c] + 1, + 0 if not r else self.row_positions[r] + 1, + 0 if not c else self.col_positions[c + 1], + 0 if not r else self.row_positions[r + 1], + ) + + def cell_completely_visible(self, r=0, c=0, separate_axes=False): + cx1, cy1, cx2, cy2 = self.get_canvas_visible_area() + x1, y1, x2, y2 = self.get_cell_coords(r, c) + x_vis = True + y_vis = True + if cx1 > x1 or cx2 < x2: + x_vis = False + if cy1 > y1 or cy2 < y2: + y_vis = False + if separate_axes: + return y_vis, x_vis + else: + if not y_vis or not x_vis: + return False + else: + return True + + def cell_visible(self, r=0, c=0): + cx1, cy1, cx2, cy2 = self.get_canvas_visible_area() + x1, y1, x2, y2 = self.get_cell_coords(r, c) + if x1 <= cx2 or y1 <= cy2 or x2 >= cx1 or y2 >= cy1: + return True + return False + + def select_all(self, redraw=True, run_binding_func=True): + currently_selected = self.currently_selected() + self.deselect("all") + if len(self.row_positions) > 1 and len(self.col_positions) > 1: + if currently_selected: + self.set_currently_selected(currently_selected.row, currently_selected.column, type_="cell") + else: + self.set_currently_selected(0, 0, type_="cell") + self.create_selected(0, 0, len(self.row_positions) - 1, len(self.col_positions) - 1) + if redraw: + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.select_all_binding_func is not None and run_binding_func: + self.select_all_binding_func( + SelectionBoxEvent( + "select_all_cells", + ( + 0, + 0, + len(self.row_positions) - 1, + len(self.col_positions) - 1, + ), + ) + ) + + def select_cell(self, r, c, redraw=False): + self.delete_selection_rects() + self.create_selected(r, c, r + 1, c + 1, state="hidden") + self.set_currently_selected(r, c, type_="cell") + if redraw: + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.selection_binding_func is not None: + self.selection_binding_func(SelectCellEvent("select_cell", r, c)) + + def add_selection(self, r, c, redraw=False, run_binding_func=True, set_as_current=False): + self.create_selected(r, c, r + 1, c + 1, state="hidden") + if set_as_current: + self.set_currently_selected(r, c, type_="cell") + if redraw: + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.selection_binding_func is not None and run_binding_func: + self.selection_binding_func(SelectCellEvent("select_cell", r, c)) + + def toggle_select_cell( + self, + row, + column, + add_selection=True, + redraw=True, + run_binding_func=True, + set_as_current=True, + ): + if add_selection: + if self.cell_selected(row, column, inc_rows=True, inc_cols=True): + self.deselect(r=row, c=column, redraw=redraw) + else: + self.add_selection( + r=row, + c=column, + redraw=redraw, + run_binding_func=run_binding_func, + set_as_current=set_as_current, + ) + else: + if self.cell_selected(row, column, inc_rows=True, inc_cols=True): + self.deselect(r=row, c=column, redraw=redraw) + else: + self.select_cell(row, column, redraw=redraw) + + def align_rows(self, rows=[], align="global", align_index=False): # "center", "w", "e" or "global" + if isinstance(rows, str) and rows.lower() == "all" and align == "global": + for r in self.row_options: + if "align" in self.row_options[r]: + del self.row_options[r]["align"] + if align_index: + for r in self.RI.cell_options: + if r in self.RI.cell_options and "align" in self.RI.cell_options[r]: + del self.RI.cell_options[r]["align"] + return + if isinstance(rows, int): + rows_ = [rows] + elif isinstance(rows, str) and rows.lower() == "all": + rows_ = (r for r in range(self.total_data_rows())) + else: + rows_ = rows + if align == "global": + for r in rows_: + if r in self.row_options and "align" in self.row_options[r]: + del self.row_options[r]["align"] + if align_index and r in self.RI.cell_options and "align" in self.RI.cell_options[r]: + del self.RI.cell_options[r]["align"] + else: + for r in rows_: + if r not in self.row_options: + self.row_options[r] = {} + self.row_options[r]["align"] = align + if align_index: + if r not in self.RI.cell_options: + self.RI.cell_options[r] = {} + self.RI.cell_options[r]["align"] = align + + def align_columns(self, columns=[], align="global", align_header=False): # "center", "w", "e" or "global" + if isinstance(columns, str) and columns.lower() == "all" and align == "global": + for c in self.col_options: + if "align" in self.col_options[c]: + del self.col_options[c]["align"] + if align_header: + for c in self.CH.cell_options: + if c in self.CH.cell_options and "align" in self.CH.cell_options[c]: + del self.CH.cell_options[c]["align"] + return + if isinstance(columns, int): + cols_ = [columns] + elif isinstance(columns, str) and columns.lower() == "all": + cols_ = (c for c in range(self.total_data_cols())) + else: + cols_ = columns + if align == "global": + for c in cols_: + if c in self.col_options and "align" in self.col_options[c]: + del self.col_options[c]["align"] + if align_header and c in self.CH.cell_options and "align" in self.CH.cell_options[c]: + del self.CH.cell_options[c]["align"] + else: + for c in cols_: + if c not in self.col_options: + self.col_options[c] = {} + self.col_options[c]["align"] = align + if align_header: + if c not in self.CH.cell_options: + self.CH.cell_options[c] = {} + self.CH.cell_options[c]["align"] = align + + def align_cells(self, row=0, column=0, cells=[], align="global"): # "center", "w", "e" or "global" + if isinstance(row, str) and row.lower() == "all" and align == "global": + for r, c in self.cell_options: + if "align" in self.cell_options[(r, c)]: + del self.cell_options[(r, c)]["align"] + return + if align == "global": + if cells: + for r, c in cells: + if (r, c) in self.cell_options and "align" in self.cell_options[(r, c)]: + del self.cell_options[(r, c)]["align"] + else: + if (row, column) in self.cell_options and "align" in self.cell_options[(row, column)]: + del self.cell_options[(row, column)]["align"] + else: + if cells: + for r, c in cells: + if (r, c) not in self.cell_options: + self.cell_options[(r, c)] = {} + self.cell_options[(r, c)]["align"] = align + else: + if (row, column) not in self.cell_options: + self.cell_options[(row, column)] = {} + self.cell_options[(row, column)]["align"] = align + + def deselect(self, r=None, c=None, cell=None, redraw=True): + deselected = tuple() + deleted_boxes = {} + if r == "all": + deselected = ("deselect_all", self.delete_selection_rects()) + elif r == "allrows": + for item in self.find_withtag("rows"): + alltags = self.gettags(item) + if alltags: + r1, c1, r2, c2 = tuple(int(e) for e in alltags[1].split("_") if e) + deleted_boxes[r1, c1, r2, c2] = "rows" + self.delete(alltags[1]) + self.RI.delete(alltags[1]) + self.CH.delete(alltags[1]) + current = self.currently_selected() + if current and current.type_ == "row": + deleted_boxes[tuple(int(e) for e in self.get_tags_of_current()[1].split("_") if e)] = "cell" + self.delete_current() + deselected = ("deselect_all_rows", deleted_boxes) + elif r == "allcols": + for item in self.find_withtag("columns"): + alltags = self.gettags(item) + if alltags: + r1, c1, r2, c2 = tuple(int(e) for e in alltags[1].split("_") if e) + deleted_boxes[r1, c1, r2, c2] = "columns" + self.delete(alltags[1]) + self.RI.delete(alltags[1]) + self.CH.delete(alltags[1]) + current = self.currently_selected() + if current and current.type_ == "column": + deleted_boxes[tuple(int(e) for e in self.get_tags_of_current()[1].split("_") if e)] = "cell" + self.delete_current() + deselected = ("deselect_all_cols", deleted_boxes) + elif r is not None and c is None and cell is None: + current = self.find_withtag("selected") + current_tags = self.gettags(current[0]) if current else tuple() + if current: + curr_r1, curr_c1, curr_r2, curr_c2 = tuple(int(e) for e in current_tags[1].split("_") if e) + reset_current = False + for item in self.find_withtag("rows"): + alltags = self.gettags(item) + if alltags: + r1, c1, r2, c2 = tuple(int(e) for e in alltags[1].split("_") if e) + if r >= r1 and r < r2: + self.delete(f"{r1}_{c1}_{r2}_{c2}") + self.RI.delete(f"{r1}_{c1}_{r2}_{c2}") + self.CH.delete(f"{r1}_{c1}_{r2}_{c2}") + if not reset_current and current and curr_r1 >= r1 and curr_r1 < r2: + reset_current = True + deleted_boxes[curr_r1, curr_c1, curr_r2, curr_c2] = "cell" + deleted_boxes[r1, c1, r2, c2] = "rows" + if reset_current: + self.delete_current() + self.set_current_to_last() + deselected = ("deselect_row", deleted_boxes) + elif c is not None and r is None and cell is None: + current = self.find_withtag("selected") + current_tags = self.gettags(current[0]) if current else tuple() + if current: + curr_r1, curr_c1, curr_r2, curr_c2 = tuple(int(e) for e in current_tags[1].split("_") if e) + reset_current = False + for item in self.find_withtag("columns"): + alltags = self.gettags(item) + if alltags: + r1, c1, r2, c2 = tuple(int(e) for e in alltags[1].split("_") if e) + if c >= c1 and c < c2: + self.delete(f"{r1}_{c1}_{r2}_{c2}") + self.RI.delete(f"{r1}_{c1}_{r2}_{c2}") + self.CH.delete(f"{r1}_{c1}_{r2}_{c2}") + if not reset_current and current and curr_c1 >= c1 and curr_c1 < c2: + reset_current = True + deleted_boxes[curr_r1, curr_c1, curr_r2, curr_c2] = "cell" + deleted_boxes[r1, c1, r2, c2] = "columns" + if reset_current: + self.delete_current() + self.set_current_to_last() + deselected = ("deselect_column", deleted_boxes) + elif (r is not None and c is not None and cell is None) or cell is not None: + set_curr = False + if cell is not None: + r, c = cell[0], cell[1] + for item in chain( + self.find_withtag("cells"), + self.find_withtag("rows"), + self.find_withtag("columns"), + self.find_withtag("selected"), + ): + alltags = self.gettags(item) + if alltags: + r1, c1, r2, c2 = tuple(int(e) for e in alltags[1].split("_") if e) + if r >= r1 and c >= c1 and r < r2 and c < c2: + current = self.currently_selected() + if ( + not set_curr + and current + and r2 - r1 == 1 + and c2 - c1 == 1 + and r == current[0] + and c == current[1] + ): + set_curr = True + if current and not set_curr: + if current[0] >= r1 and current[0] < r2 and current[1] >= c1 and current[1] < c2: + set_curr = True + self.delete(f"{r1}_{c1}_{r2}_{c2}") + self.RI.delete(f"{r1}_{c1}_{r2}_{c2}") + self.CH.delete(f"{r1}_{c1}_{r2}_{c2}") + deleted_boxes[(r1, c1, r2, c2)] = "cells" + if set_curr: + try: + deleted_boxes[tuple(int(e) for e in self.get_tags_of_current()[1].split("_") if e)] = "cells" + except Exception: + pass + self.delete_current() + self.set_current_to_last() + deselected = ("deselect_cell", deleted_boxes) + if redraw: + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.deselection_binding_func is not None: + self.deselection_binding_func(DeselectionEvent(*deselected)) + + def page_UP(self, event=None): + height = self.winfo_height() + top = self.canvasy(0) + scrollto = top - height + if scrollto < 0: + scrollto = 0 + if self.page_up_down_select_row: + r = bisect.bisect_left(self.row_positions, scrollto) + current = self.currently_selected() + if current and current[0] == r: + r -= 1 + if r < 0: + r = 0 + if self.RI.row_selection_enabled and ( + self.anything_selected(exclude_columns=True, exclude_cells=True) or not self.anything_selected() + ): + self.RI.select_row(r) + self.see(r, 0, keep_xscroll=True, check_cell_visibility=False) + elif (self.single_selection_enabled or self.toggle_selection_enabled) and self.anything_selected( + exclude_columns=True, exclude_rows=True + ): + box = self.get_all_selection_boxes_with_types()[0][0] + self.see(r, box[1], keep_xscroll=True, check_cell_visibility=False) + self.select_cell(r, box[1]) + else: + args = ("moveto", scrollto / (self.row_positions[-1] + 100)) + self.yview(*args) + self.RI.yview(*args) + self.main_table_redraw_grid_and_text(redraw_row_index=True) + + def page_DOWN(self, event=None): + height = self.winfo_height() + top = self.canvasy(0) + scrollto = top + height + if self.page_up_down_select_row and self.RI.row_selection_enabled: + r = bisect.bisect_left(self.row_positions, scrollto) - 1 + current = self.currently_selected() + if current and current[0] == r: + r += 1 + if r > len(self.row_positions) - 2: + r = len(self.row_positions) - 2 + if self.RI.row_selection_enabled and ( + self.anything_selected(exclude_columns=True, exclude_cells=True) or not self.anything_selected() + ): + self.RI.select_row(r) + self.see(r, 0, keep_xscroll=True, check_cell_visibility=False) + elif (self.single_selection_enabled or self.toggle_selection_enabled) and self.anything_selected( + exclude_columns=True, exclude_rows=True + ): + box = self.get_all_selection_boxes_with_types()[0][0] + self.see(r, box[1], keep_xscroll=True, check_cell_visibility=False) + self.select_cell(r, box[1]) + else: + end = self.row_positions[-1] + if scrollto > end + 100: + scrollto = end + args = ("moveto", scrollto / (end + 100)) + self.yview(*args) + self.RI.yview(*args) + self.main_table_redraw_grid_and_text(redraw_row_index=True) + + def arrowkey_UP(self, event=None): + currently_selected = self.currently_selected() + if not currently_selected: + return + if currently_selected.type_ == "row": + r = currently_selected.row + if r != 0 and self.RI.row_selection_enabled: + if self.cell_completely_visible(r=r - 1, c=0): + self.RI.select_row(r - 1, redraw=True) + else: + self.RI.select_row(r - 1) + self.see(r - 1, 0, keep_xscroll=True, check_cell_visibility=False) + elif currently_selected.type_ in ("cell", "column"): + r = currently_selected[0] + c = currently_selected[1] + if r == 0 and self.CH.col_selection_enabled: + if not self.cell_completely_visible(r=r, c=0): + self.see(r, c, keep_xscroll=True, check_cell_visibility=False) + elif r != 0 and (self.single_selection_enabled or self.toggle_selection_enabled): + if self.cell_completely_visible(r=r - 1, c=c): + self.select_cell(r - 1, c, redraw=True) + else: + self.select_cell(r - 1, c) + self.see(r - 1, c, keep_xscroll=True, check_cell_visibility=False) + + def arrowkey_RIGHT(self, event=None): + currently_selected = self.currently_selected() + if not currently_selected: + return + if currently_selected.type_ == "row": + r = currently_selected.row + if self.single_selection_enabled or self.toggle_selection_enabled: + if self.cell_completely_visible(r=r, c=0): + self.select_cell(r, 0, redraw=True) + else: + self.select_cell(r, 0) + self.see( + r, + 0, + keep_yscroll=True, + bottom_right_corner=True, + check_cell_visibility=False, + ) + elif currently_selected.type_ == "column": + c = currently_selected.column + if c < len(self.col_positions) - 2 and self.CH.col_selection_enabled: + if self.cell_completely_visible(r=0, c=c + 1): + self.CH.select_col(c + 1, redraw=True) + else: + self.CH.select_col(c + 1) + self.see( + 0, + c + 1, + keep_yscroll=True, + bottom_right_corner=False if self.arrow_key_down_right_scroll_page else True, + check_cell_visibility=False, + ) + else: + r = currently_selected[0] + c = currently_selected[1] + if c < len(self.col_positions) - 2 and (self.single_selection_enabled or self.toggle_selection_enabled): + if self.cell_completely_visible(r=r, c=c + 1): + self.select_cell(r, c + 1, redraw=True) + else: + self.select_cell(r, c + 1) + self.see( + r, + c + 1, + keep_yscroll=True, + bottom_right_corner=False if self.arrow_key_down_right_scroll_page else True, + check_cell_visibility=False, + ) + + def arrowkey_DOWN(self, event=None): + currently_selected = self.currently_selected() + if not currently_selected: + return + if currently_selected.type_ == "row": + r = currently_selected.row + if r < len(self.row_positions) - 2 and self.RI.row_selection_enabled: + if self.cell_completely_visible(r=min(r + 2, len(self.row_positions) - 2), c=0): + self.RI.select_row(r + 1, redraw=True) + else: + self.RI.select_row(r + 1) + if ( + r + 2 < len(self.row_positions) - 2 + and (self.row_positions[r + 3] - self.row_positions[r + 2]) + + (self.row_positions[r + 2] - self.row_positions[r + 1]) + + 5 + < self.winfo_height() + ): + self.see( + r + 2, + 0, + keep_xscroll=True, + bottom_right_corner=True, + check_cell_visibility=False, + ) + elif not self.cell_completely_visible(r=r + 1, c=0): + self.see( + r + 1, + 0, + keep_xscroll=True, + bottom_right_corner=False if self.arrow_key_down_right_scroll_page else True, + check_cell_visibility=False, + ) + elif currently_selected.type_ == "column": + c = currently_selected.column + if self.single_selection_enabled or self.toggle_selection_enabled: + if self.cell_completely_visible(r=0, c=c): + self.select_cell(0, c, redraw=True) + else: + self.select_cell(0, c) + self.see( + 0, + c, + keep_xscroll=True, + bottom_right_corner=True, + check_cell_visibility=False, + ) + else: + r = currently_selected[0] + c = currently_selected[1] + if r < len(self.row_positions) - 2 and (self.single_selection_enabled or self.toggle_selection_enabled): + if self.cell_completely_visible(r=min(r + 2, len(self.row_positions) - 2), c=c): + self.select_cell(r + 1, c, redraw=True) + else: + self.select_cell(r + 1, c) + if ( + r + 2 < len(self.row_positions) - 2 + and (self.row_positions[r + 3] - self.row_positions[r + 2]) + + (self.row_positions[r + 2] - self.row_positions[r + 1]) + + 5 + < self.winfo_height() + ): + self.see( + r + 2, + c, + keep_xscroll=True, + bottom_right_corner=True, + check_cell_visibility=False, + ) + elif not self.cell_completely_visible(r=r + 1, c=c): + self.see( + r + 1, + c, + keep_xscroll=True, + bottom_right_corner=False if self.arrow_key_down_right_scroll_page else True, + check_cell_visibility=False, + ) + + def arrowkey_LEFT(self, event=None): + currently_selected = self.currently_selected() + if not currently_selected: + return + if currently_selected.type_ == "column": + c = currently_selected.column + if c != 0 and self.CH.col_selection_enabled: + if self.cell_completely_visible(r=0, c=c - 1): + self.CH.select_col(c - 1, redraw=True) + else: + self.CH.select_col(c - 1) + self.see( + 0, + c - 1, + keep_yscroll=True, + bottom_right_corner=True, + check_cell_visibility=False, + ) + elif currently_selected.type_ == "cell": + r = currently_selected.row + c = currently_selected.column + if c == 0 and self.RI.row_selection_enabled: + if not self.cell_completely_visible(r=r, c=0): + self.see(r, c, keep_yscroll=True, check_cell_visibility=False) + elif c != 0 and (self.single_selection_enabled or self.toggle_selection_enabled): + if self.cell_completely_visible(r=r, c=c - 1): + self.select_cell(r, c - 1, redraw=True) + else: + self.select_cell(r, c - 1) + self.see(r, c - 1, keep_yscroll=True, check_cell_visibility=False) + + def edit_bindings(self, enable=True, key=None): + if key is None or key == "copy": + if enable: + for s2 in ("c", "C"): + for widget in (self, self.RI, self.CH, self.TL): + widget.bind(f"<{ctrl_key}-{s2}>", self.ctrl_c) + self.copy_enabled = True + else: + for s1 in ("Control", "Command"): + for s2 in ("c", "C"): + for widget in (self, self.RI, self.CH, self.TL): + widget.unbind(f"<{s1}-{s2}>") + self.copy_enabled = False + if key is None or key == "cut": + if enable: + for s2 in ("x", "X"): + for widget in (self, self.RI, self.CH, self.TL): + widget.bind(f"<{ctrl_key}-{s2}>", self.ctrl_x) + self.cut_enabled = True + else: + for s1 in ("Control", "Command"): + for s2 in ("x", "X"): + for widget in (self, self.RI, self.CH, self.TL): + widget.unbind(f"<{s1}-{s2}>") + self.cut_enabled = False + if key is None or key == "paste": + if enable: + for s2 in ("v", "V"): + for widget in (self, self.RI, self.CH, self.TL): + widget.bind(f"<{ctrl_key}-{s2}>", self.ctrl_v) + self.paste_enabled = True + else: + for s1 in ("Control", "Command"): + for s2 in ("v", "V"): + for widget in (self, self.RI, self.CH, self.TL): + widget.unbind(f"<{s1}-{s2}>") + self.paste_enabled = False + if key is None or key == "undo": + if enable: + for s2 in ("z", "Z"): + for widget in (self, self.RI, self.CH, self.TL): + widget.bind(f"<{ctrl_key}-{s2}>", self.ctrl_z) + self.undo_enabled = True + else: + for s1 in ("Control", "Command"): + for s2 in ("z", "Z"): + for widget in (self, self.RI, self.CH, self.TL): + widget.unbind(f"<{s1}-{s2}>") + self.undo_enabled = False + if key is None or key == "delete": + if enable: + for widget in (self, self.RI, self.CH, self.TL): + widget.bind("", self.delete_key) + self.delete_key_enabled = True + else: + for widget in (self, self.RI, self.CH, self.TL): + widget.unbind("") + self.delete_key_enabled = False + if key is None or key == "edit_cell": + if enable: + self.bind_cell_edit(True) + else: + self.bind_cell_edit(False) + # edit header with text editor (dropdowns and checkboxes not included) + # this will not by enabled by using enable_bindings() to enable all bindings + # must be enabled directly using enable_bindings("edit_header") + # the same goes for "edit_index" + if key is None or key == "edit_header": + if enable: + self.CH.bind_cell_edit(True) + else: + self.CH.bind_cell_edit(False) + if key is None or key == "edit_index": + if enable: + self.RI.bind_cell_edit(True) + else: + self.RI.bind_cell_edit(False) + + def menu_add_command(self, menu: tk.Menu, **kwargs): + if "label" not in kwargs: + return + try: + index = menu.index(kwargs["label"]) + menu.delete(index) + except TclError: + pass + menu.add_command(**kwargs) + + def create_rc_menus(self): + if not self.rc_popup_menu: + self.rc_popup_menu = tk.Menu(self, tearoff=0, background=self.popup_menu_bg) + if not self.CH.ch_rc_popup_menu: + self.CH.ch_rc_popup_menu = tk.Menu(self.CH, tearoff=0, background=self.popup_menu_bg) + if not self.RI.ri_rc_popup_menu: + self.RI.ri_rc_popup_menu = tk.Menu(self.RI, tearoff=0, background=self.popup_menu_bg) + if not self.empty_rc_popup_menu: + self.empty_rc_popup_menu = tk.Menu(self, tearoff=0, background=self.popup_menu_bg) + for menu in ( + self.rc_popup_menu, + self.CH.ch_rc_popup_menu, + self.RI.ri_rc_popup_menu, + self.empty_rc_popup_menu, + ): + menu.delete(0, "end") + if self.rc_popup_menus_enabled and self.CH.edit_cell_enabled: + self.menu_add_command( + self.CH.ch_rc_popup_menu, + label="Edit header", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=lambda: self.CH.open_cell(event="rc"), + ) + if self.rc_popup_menus_enabled and self.RI.edit_cell_enabled: + self.menu_add_command( + self.RI.ri_rc_popup_menu, + label="Edit index", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=lambda: self.RI.open_cell(event="rc"), + ) + if self.rc_popup_menus_enabled and self.edit_cell_enabled: + self.menu_add_command( + self.rc_popup_menu, + label="Edit cell", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=lambda: self.open_cell(event="rc"), + ) + if self.cut_enabled: + self.menu_add_command( + self.rc_popup_menu, + label="Cut", + accelerator="Ctrl+X", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_x, + ) + self.menu_add_command( + self.CH.ch_rc_popup_menu, + label="Cut contents", + accelerator="Ctrl+X", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_x, + ) + self.menu_add_command( + self.RI.ri_rc_popup_menu, + label="Cut contents", + accelerator="Ctrl+X", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_x, + ) + if self.copy_enabled: + self.menu_add_command( + self.rc_popup_menu, + label="Copy", + accelerator="Ctrl+C", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_c, + ) + self.menu_add_command( + self.CH.ch_rc_popup_menu, + label="Copy contents", + accelerator="Ctrl+C", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_c, + ) + self.menu_add_command( + self.RI.ri_rc_popup_menu, + label="Copy contents", + accelerator="Ctrl+C", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_c, + ) + if self.paste_enabled: + self.menu_add_command( + self.rc_popup_menu, + label="Paste", + accelerator="Ctrl+V", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_v, + ) + self.menu_add_command( + self.CH.ch_rc_popup_menu, + label="Paste", + accelerator="Ctrl+V", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_v, + ) + self.menu_add_command( + self.RI.ri_rc_popup_menu, + label="Paste", + accelerator="Ctrl+V", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_v, + ) + if self.expand_sheet_if_paste_too_big: + self.menu_add_command( + self.empty_rc_popup_menu, + label="Paste", + accelerator="Ctrl+V", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.ctrl_v, + ) + if self.delete_key_enabled: + self.menu_add_command( + self.rc_popup_menu, + label="Delete", + accelerator="Del", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.delete_key, + ) + self.menu_add_command( + self.CH.ch_rc_popup_menu, + label="Clear contents", + accelerator="Del", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.delete_key, + ) + self.menu_add_command( + self.RI.ri_rc_popup_menu, + label="Clear contents", + accelerator="Del", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.delete_key, + ) + if self.rc_delete_column_enabled: + self.menu_add_command( + self.CH.ch_rc_popup_menu, + label="Delete columns", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.del_cols_rc, + ) + if self.rc_insert_column_enabled: + self.menu_add_command( + self.CH.ch_rc_popup_menu, + label="Insert columns left", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=lambda: self.insert_cols_rc("left"), + ) + self.menu_add_command( + self.empty_rc_popup_menu, + label="Insert column", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=lambda: self.insert_cols_rc("left"), + ) + self.menu_add_command( + self.CH.ch_rc_popup_menu, + label="Insert columns right", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=lambda: self.insert_cols_rc("right"), + ) + if self.rc_delete_row_enabled: + self.menu_add_command( + self.RI.ri_rc_popup_menu, + label="Delete rows", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=self.del_rows_rc, + ) + if self.rc_insert_row_enabled: + self.menu_add_command( + self.RI.ri_rc_popup_menu, + label="Insert rows above", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=lambda: self.insert_rows_rc("above"), + ) + self.menu_add_command( + self.RI.ri_rc_popup_menu, + label="Insert rows below", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=lambda: self.insert_rows_rc("below"), + ) + self.menu_add_command( + self.empty_rc_popup_menu, + label="Insert row", + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=lambda: self.insert_rows_rc("below"), + ) + for label, func in self.extra_table_rc_menu_funcs.items(): + self.menu_add_command( + self.rc_popup_menu, + label=label, + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=func, + ) + for label, func in self.extra_index_rc_menu_funcs.items(): + self.menu_add_command( + self.RI.ri_rc_popup_menu, + label=label, + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=func, + ) + for label, func in self.extra_header_rc_menu_funcs.items(): + self.menu_add_command( + self.CH.ch_rc_popup_menu, + label=label, + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=func, + ) + for label, func in self.extra_empty_space_rc_menu_funcs.items(): + self.menu_add_command( + self.empty_rc_popup_menu, + label=label, + font=self.popup_menu_font, + foreground=self.popup_menu_fg, + background=self.popup_menu_bg, + activebackground=self.popup_menu_highlight_bg, + activeforeground=self.popup_menu_highlight_fg, + command=func, + ) + + def bind_cell_edit(self, enable=True, keys=[]): + if enable: + self.edit_cell_enabled = True + for w in (self, self.RI, self.CH): + w.bind("", self.open_cell) + else: + self.edit_cell_enabled = False + for w in (self, self.RI, self.CH): + w.unbind("") + + def enable_bindings(self, bindings): + if not bindings: + self.enable_bindings_internal("all") + elif isinstance(bindings, (list, tuple)): + for binding in bindings: + if isinstance(binding, (list, tuple)): + for bind in binding: + self.enable_bindings_internal(bind.lower()) + elif isinstance(binding, str): + self.enable_bindings_internal(binding.lower()) + elif isinstance(bindings, str): + self.enable_bindings_internal(bindings.lower()) + + def disable_bindings(self, bindings): + if not bindings: + self.disable_bindings_internal("all") + elif isinstance(bindings, (list, tuple)): + for binding in bindings: + if isinstance(binding, (list, tuple)): + for bind in binding: + self.disable_bindings_internal(bind.lower()) + elif isinstance(binding, str): + self.disable_bindings_internal(binding.lower()) + elif isinstance(bindings, str): + self.disable_bindings_internal(bindings) + + def enable_disable_select_all(self, enable=True): + self.select_all_enabled = bool(enable) + for s in ("A", "a"): + binding = f"<{ctrl_key}-{s}>" + for widget in (self, self.RI, self.CH, self.TL): + if enable: + widget.bind(binding, self.select_all) + else: + widget.unbind(binding) + + def enable_bindings_internal(self, binding): + if binding in ("enable_all", "all"): + self.single_selection_enabled = True + self.toggle_selection_enabled = False + self.drag_selection_enabled = True + self.enable_disable_select_all(True) + self.CH.enable_bindings("column_width_resize") + self.CH.enable_bindings("column_select") + self.CH.enable_bindings("column_height_resize") + self.CH.enable_bindings("drag_and_drop") + self.CH.enable_bindings("double_click_column_resize") + self.RI.enable_bindings("row_height_resize") + self.RI.enable_bindings("double_click_row_resize") + self.RI.enable_bindings("row_width_resize") + self.RI.enable_bindings("row_select") + self.RI.enable_bindings("drag_and_drop") + self.bind_arrowkeys(self.arrowkey_binding_functions) + self.edit_bindings(True) + self.rc_delete_column_enabled = True + self.rc_delete_row_enabled = True + self.rc_insert_column_enabled = True + self.rc_insert_row_enabled = True + self.rc_popup_menus_enabled = True + self.rc_select_enabled = True + self.TL.rh_state() + self.TL.rw_state() + elif binding in ("single", "single_selection_mode", "single_select"): + self.single_selection_enabled = True + self.toggle_selection_enabled = False + elif binding in ("toggle", "toggle_selection_mode", "toggle_select"): + self.toggle_selection_enabled = True + self.single_selection_enabled = False + elif binding == "drag_select": + self.drag_selection_enabled = True + elif binding == "select_all": + self.enable_disable_select_all(True) + elif binding == "column_width_resize": + self.CH.enable_bindings("column_width_resize") + elif binding == "column_select": + self.CH.enable_bindings("column_select") + elif binding == "column_height_resize": + self.CH.enable_bindings("column_height_resize") + self.TL.rh_state() + elif binding == "column_drag_and_drop": + self.CH.enable_bindings("drag_and_drop") + elif binding == "double_click_column_resize": + self.CH.enable_bindings("double_click_column_resize") + elif binding == "row_height_resize": + self.RI.enable_bindings("row_height_resize") + elif binding == "double_click_row_resize": + self.RI.enable_bindings("double_click_row_resize") + elif binding == "row_width_resize": + self.RI.enable_bindings("row_width_resize") + self.TL.rw_state() + elif binding == "row_select": + self.RI.enable_bindings("row_select") + elif binding == "row_drag_and_drop": + self.RI.enable_bindings("drag_and_drop") + elif binding == "arrowkeys": + self.bind_arrowkeys(self.arrowkey_binding_functions) + elif binding in ("tab", "up", "right", "down", "left", "prior", "next"): + self.bind_arrowkeys(keys={binding: self.arrowkey_binding_functions[binding]}) + elif binding == "edit_bindings": + self.edit_bindings(True) + elif binding == "rc_delete_column": + self.rc_delete_column_enabled = True + self.rc_popup_menus_enabled = True + self.rc_select_enabled = True + elif binding == "rc_delete_row": + self.rc_delete_row_enabled = True + self.rc_popup_menus_enabled = True + self.rc_select_enabled = True + elif binding == "rc_insert_column": + self.rc_insert_column_enabled = True + self.rc_popup_menus_enabled = True + self.rc_select_enabled = True + elif binding == "rc_insert_row": + self.rc_insert_row_enabled = True + self.rc_popup_menus_enabled = True + self.rc_select_enabled = True + elif binding == "copy": + self.edit_bindings(True, "copy") + elif binding == "cut": + self.edit_bindings(True, "cut") + elif binding == "paste": + self.edit_bindings(True, "paste") + elif binding == "delete": + self.edit_bindings(True, "delete") + elif binding in ("right_click_popup_menu", "rc_popup_menu"): + self.rc_popup_menus_enabled = True + self.rc_select_enabled = True + elif binding in ("right_click_select", "rc_select"): + self.rc_select_enabled = True + elif binding in ("ctrl_click_select", "ctrl_select"): + self.ctrl_select_enabled = True + elif binding == "undo": + self.edit_bindings(True, "undo") + elif binding == "edit_cell": + self.edit_bindings(True, "edit_cell") + elif binding == "edit_header": + self.edit_bindings(True, "edit_header") + elif binding == "edit_index": + self.edit_bindings(True, "edit_index") + self.create_rc_menus() + + def disable_bindings_internal(self, binding): + if binding in ("all", "disable_all"): + self.single_selection_enabled = False + self.toggle_selection_enabled = False + self.drag_selection_enabled = False + self.enable_disable_select_all(False) + self.CH.disable_bindings("column_width_resize") + self.CH.disable_bindings("column_select") + self.CH.disable_bindings("column_height_resize") + self.CH.disable_bindings("drag_and_drop") + self.CH.disable_bindings("double_click_column_resize") + self.RI.disable_bindings("row_height_resize") + self.RI.disable_bindings("double_click_row_resize") + self.RI.disable_bindings("row_width_resize") + self.RI.disable_bindings("row_select") + self.RI.disable_bindings("drag_and_drop") + self.unbind_arrowkeys(self.arrowkey_binding_functions) + self.edit_bindings(False) + self.rc_delete_column_enabled = False + self.rc_delete_row_enabled = False + self.rc_insert_column_enabled = False + self.rc_insert_row_enabled = False + self.rc_popup_menus_enabled = False + self.rc_select_enabled = False + self.ctrl_select_enabled = False + self.TL.rh_state("hidden") + self.TL.rw_state("hidden") + elif binding in ("single", "single_selection_mode", "single_select"): + self.single_selection_enabled = False + elif binding in ("toggle", "toggle_selection_mode", "toggle_select"): + self.toggle_selection_enabled = False + elif binding == "drag_select": + self.drag_selection_enabled = False + elif binding == "select_all": + self.enable_disable_select_all(False) + elif binding == "column_width_resize": + self.CH.disable_bindings("column_width_resize") + elif binding == "column_select": + self.CH.disable_bindings("column_select") + elif binding == "column_height_resize": + self.CH.disable_bindings("column_height_resize") + self.TL.rh_state("hidden") + elif binding == "column_drag_and_drop": + self.CH.disable_bindings("drag_and_drop") + elif binding == "double_click_column_resize": + self.CH.disable_bindings("double_click_column_resize") + elif binding == "row_height_resize": + self.RI.disable_bindings("row_height_resize") + elif binding == "double_click_row_resize": + self.RI.disable_bindings("double_click_row_resize") + elif binding == "row_width_resize": + self.RI.disable_bindings("row_width_resize") + self.TL.rw_state("hidden") + elif binding == "row_select": + self.RI.disable_bindings("row_select") + elif binding == "row_drag_and_drop": + self.RI.disable_bindings("drag_and_drop") + elif binding == "arrowkeys": + self.unbind_arrowkeys(self.arrowkey_binding_functions) + elif binding in ("tab", "up", "right", "down", "left", "prior", "next"): + self.unbind_arrowkeys(keys={binding: self.arrowkey_binding_functions[binding]}) + elif binding == "rc_delete_column": + self.rc_delete_column_enabled = False + elif binding == "rc_delete_row": + self.rc_delete_row_enabled = False + elif binding == "rc_insert_column": + self.rc_insert_column_enabled = False + elif binding == "rc_insert_row": + self.rc_insert_row_enabled = False + elif binding == "edit_bindings": + self.edit_bindings(False) + elif binding == "copy": + self.edit_bindings(False, "copy") + elif binding == "cut": + self.edit_bindings(False, "cut") + elif binding == "paste": + self.edit_bindings(False, "paste") + elif binding == "delete": + self.edit_bindings(False, "delete") + elif binding in ("right_click_popup_menu", "rc_popup_menu"): + self.rc_popup_menus_enabled = False + elif binding in ("right_click_select", "rc_select"): + self.rc_select_enabled = False + elif binding in ("ctrl_click_select", "ctrl_select"): + self.ctrl_select_enabled = False + elif binding == "undo": + self.edit_bindings(False, "undo") + elif binding == "edit_cell": + self.edit_bindings(False, "edit_cell") + elif binding == "edit_header": + self.edit_bindings(False, "edit_header") + elif binding == "edit_index": + self.edit_bindings(False, "edit_index") + self.create_rc_menus() + + def reset_mouse_motion_creations(self, event=None): + self.config(cursor="") + self.RI.config(cursor="") + self.CH.config(cursor="") + self.RI.rsz_w = None + self.RI.rsz_h = None + self.CH.rsz_w = None + self.CH.rsz_h = None + + def mouse_motion(self, event): + if ( + not self.RI.currently_resizing_height + and not self.RI.currently_resizing_width + and not self.CH.currently_resizing_height + and not self.CH.currently_resizing_width + ): + mouse_over_resize = False + x = self.canvasx(event.x) + y = self.canvasy(event.y) + if self.RI.width_resizing_enabled and self.show_index and not mouse_over_resize: + try: + x1, y1, x2, y2 = ( + self.row_width_resize_bbox[0], + self.row_width_resize_bbox[1], + self.row_width_resize_bbox[2], + self.row_width_resize_bbox[3], + ) + if x >= x1 and y >= y1 and x <= x2 and y <= y2: + self.config(cursor="sb_h_double_arrow") + self.RI.config(cursor="sb_h_double_arrow") + self.RI.rsz_w = True + mouse_over_resize = True + except Exception: + pass + if self.CH.height_resizing_enabled and self.show_header and not mouse_over_resize: + try: + x1, y1, x2, y2 = ( + self.header_height_resize_bbox[0], + self.header_height_resize_bbox[1], + self.header_height_resize_bbox[2], + self.header_height_resize_bbox[3], + ) + if x >= x1 and y >= y1 and x <= x2 and y <= y2: + self.config(cursor="sb_v_double_arrow") + self.CH.config(cursor="sb_v_double_arrow") + self.CH.rsz_h = True + mouse_over_resize = True + except Exception: + pass + if not mouse_over_resize: + self.reset_mouse_motion_creations() + if self.extra_motion_func is not None: + self.extra_motion_func(event) + + def rc(self, event=None): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + popup_menu = None + if self.single_selection_enabled and all( + v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w) + ): + r = self.identify_row(y=event.y) + c = self.identify_col(x=event.x) + if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: + if self.col_selected(c): + if self.rc_popup_menus_enabled: + popup_menu = self.CH.ch_rc_popup_menu + elif self.row_selected(r): + if self.rc_popup_menus_enabled: + popup_menu = self.RI.ri_rc_popup_menu + elif self.cell_selected(r, c): + if self.rc_popup_menus_enabled: + popup_menu = self.rc_popup_menu + else: + if self.rc_select_enabled: + self.select_cell(r, c, redraw=True) + if self.rc_popup_menus_enabled: + popup_menu = self.rc_popup_menu + else: + popup_menu = self.empty_rc_popup_menu + elif self.toggle_selection_enabled and all( + v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w) + ): + r = self.identify_row(y=event.y) + c = self.identify_col(x=event.x) + if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: + if self.col_selected(c): + if self.rc_popup_menus_enabled: + popup_menu = self.CH.ch_rc_popup_menu + elif self.row_selected(r): + if self.rc_popup_menus_enabled: + popup_menu = self.RI.ri_rc_popup_menu + elif self.cell_selected(r, c): + if self.rc_popup_menus_enabled: + popup_menu = self.rc_popup_menu + else: + if self.rc_select_enabled: + self.toggle_select_cell(r, c, redraw=True) + if self.rc_popup_menus_enabled: + popup_menu = self.rc_popup_menu + else: + popup_menu = self.empty_rc_popup_menu + if self.extra_rc_func is not None: + self.extra_rc_func(event) + if popup_menu is not None: + popup_menu.tk_popup(event.x_root, event.y_root) + + def b1_press(self, event=None): + self.closed_dropdown = self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + x1, y1, x2, y2 = self.get_canvas_visible_area() + if ( + self.identify_col(x=event.x, allow_end=False) is None + or self.identify_row(y=event.y, allow_end=False) is None + ): + self.deselect("all") + r = self.identify_row(y=event.y) + c = self.identify_col(x=event.x) + if self.single_selection_enabled and all( + v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w) + ): + if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: + self.select_cell(r, c, redraw=True) + elif self.toggle_selection_enabled and all( + v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w) + ): + r = self.identify_row(y=event.y) + c = self.identify_col(x=event.x) + if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: + self.toggle_select_cell(r, c, redraw=True) + elif self.RI.width_resizing_enabled and self.show_index and self.RI.rsz_h is None and self.RI.rsz_w: + self.RI.currently_resizing_width = True + self.new_row_width = self.RI.current_width + event.x + x = self.canvasx(event.x) + self.create_resize_line(x, y1, x, y2, width=1, fill=self.RI.resizing_line_fg, tag="rwl") + elif self.CH.height_resizing_enabled and self.show_header and self.CH.rsz_w is None and self.CH.rsz_h: + self.CH.currently_resizing_height = True + self.new_header_height = self.CH.current_height + event.y + y = self.canvasy(event.y) + self.create_resize_line(x1, y, x2, y, width=1, fill=self.RI.resizing_line_fg, tag="rhl") + self.b1_pressed_loc = (r, c) + if self.extra_b1_press_func is not None: + self.extra_b1_press_func(event) + + def create_resize_line(self, x1, y1, x2, y2, width, fill, tag): + if self.hidd_resize_lines: + t, sh = self.hidd_resize_lines.popitem() + self.coords(t, x1, y1, x2, y2) + if sh: + self.itemconfig(t, width=width, fill=fill, tag=tag) + else: + self.itemconfig(t, width=width, fill=fill, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_line(x1, y1, x2, y2, width=width, fill=fill, tag=tag) + self.disp_resize_lines[t] = True + + def delete_resize_lines(self): + self.hidd_resize_lines.update(self.disp_resize_lines) + self.disp_resize_lines = {} + for t, sh in self.hidd_resize_lines.items(): + if sh: + self.itemconfig(t, state="hidden") + self.hidd_resize_lines[t] = False + + def ctrl_b1_press(self, event=None): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + if self.ctrl_select_enabled and all( + v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w) + ): + self.b1_pressed_loc = None + rowsel = int(self.identify_row(y=event.y)) + colsel = int(self.identify_col(x=event.x)) + if rowsel < len(self.row_positions) - 1 and colsel < len(self.col_positions) - 1: + currently_selected = self.currently_selected() + if not currently_selected or currently_selected.row != rowsel or currently_selected.column != colsel: + self.add_selection(rowsel, colsel, set_as_current=True) + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) + if self.ctrl_selection_binding_func is not None: + self.ctrl_selection_binding_func( + SelectionBoxEvent( + "ctrl_select_cells", + (rowsel, colsel, rowsel + 1, colsel + 1), + ) + ) + elif not self.ctrl_select_enabled: + self.b1_press(event) + + def ctrl_shift_b1_press(self, event=None): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + if ( + self.ctrl_select_enabled + and self.drag_selection_enabled + and all(v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w)) + ): + self.b1_pressed_loc = None + rowsel = int(self.identify_row(y=event.y)) + colsel = int(self.identify_col(x=event.x)) + if rowsel < len(self.row_positions) - 1 and colsel < len(self.col_positions) - 1: + currently_selected = self.currently_selected() + if currently_selected and currently_selected.type_ == "cell": + min_r = currently_selected[0] + min_c = currently_selected[1] + if rowsel >= min_r and colsel >= min_c: + last_selected = (min_r, min_c, rowsel + 1, colsel + 1) + elif rowsel >= min_r and min_c >= colsel: + last_selected = (min_r, colsel, rowsel + 1, min_c + 1) + elif min_r >= rowsel and colsel >= min_c: + last_selected = (rowsel, min_c, min_r + 1, colsel + 1) + elif min_r >= rowsel and min_c >= colsel: + last_selected = (rowsel, colsel, min_r + 1, min_c + 1) + self.create_selected(*last_selected) + else: + self.add_selection(rowsel, colsel, set_as_current=True) + last_selected = (rowsel, colsel, rowsel + 1, colsel + 1) + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) + if self.shift_selection_binding_func is not None: + self.shift_selection_binding_func(SelectionBoxEvent("shift_select_cells", last_selected)) + elif not self.ctrl_select_enabled: + self.shift_b1_press(event) + + def shift_b1_press(self, event=None): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + if self.drag_selection_enabled and all( + v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w) + ): + self.b1_pressed_loc = None + rowsel = int(self.identify_row(y=event.y)) + colsel = int(self.identify_col(x=event.x)) + if rowsel < len(self.row_positions) - 1 and colsel < len(self.col_positions) - 1: + currently_selected = self.currently_selected() + if currently_selected and currently_selected.type_ == "cell": + min_r = currently_selected[0] + min_c = currently_selected[1] + self.delete_selection_rects(delete_current=False) + if rowsel >= min_r and colsel >= min_c: + self.create_selected(min_r, min_c, rowsel + 1, colsel + 1) + elif rowsel >= min_r and min_c >= colsel: + self.create_selected(min_r, colsel, rowsel + 1, min_c + 1) + elif min_r >= rowsel and colsel >= min_c: + self.create_selected(rowsel, min_c, min_r + 1, colsel + 1) + elif min_r >= rowsel and min_c >= colsel: + self.create_selected(rowsel, colsel, min_r + 1, min_c + 1) + last_selected = tuple(int(e) for e in self.gettags(self.find_withtag("cells"))[1].split("_") if e) + else: + self.select_cell(rowsel, colsel, redraw=False) + last_selected = tuple( + int(e) for e in self.gettags(self.find_withtag("selected"))[1].split("_") if e + ) + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) + if self.shift_selection_binding_func is not None: + self.shift_selection_binding_func(SelectionBoxEvent("shift_select_cells", last_selected)) + + def get_b1_motion_rect(self, start_row, start_col, end_row, end_col): + if end_row >= start_row and end_col >= start_col and (end_row - start_row or end_col - start_col): + return (start_row, start_col, end_row + 1, end_col + 1, "cells") + elif end_row >= start_row and end_col < start_col and (end_row - start_row or start_col - end_col): + return (start_row, end_col, end_row + 1, start_col + 1, "cells") + elif end_row < start_row and end_col >= start_col and (start_row - end_row or end_col - start_col): + return (end_row, start_col, start_row + 1, end_col + 1, "cells") + elif end_row < start_row and end_col < start_col and (start_row - end_row or start_col - end_col): + return (end_row, end_col, start_row + 1, start_col + 1, "cells") + else: + return (start_row, start_col, start_row + 1, start_col + 1, "cells") + return None + + def b1_motion(self, event): + x1, y1, x2, y2 = self.get_canvas_visible_area() + if self.drag_selection_enabled and all( + v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w) + ): + need_redraw = False + end_row = self.identify_row(y=event.y) + end_col = self.identify_col(x=event.x) + currently_selected = self.currently_selected() + if ( + end_row < len(self.row_positions) - 1 + and end_col < len(self.col_positions) - 1 + and currently_selected + and currently_selected.type_ == "cell" + ): + rect = self.get_b1_motion_rect(*(currently_selected[0], currently_selected[1], end_row, end_col)) + if rect is not None and self.being_drawn_rect != rect: + self.delete_selection_rects(delete_current=False) + if rect[2] - rect[0] == 1 and rect[3] - rect[1] == 1: + self.being_drawn_rect = rect + else: + self.being_drawn_rect = rect + self.create_selected(*rect) + if self.drag_selection_binding_func is not None: + self.drag_selection_binding_func(SelectionBoxEvent("drag_select_cells", rect[:-1])) + need_redraw = True + if self.scroll_if_event_offscreen(event): + need_redraw = True + if need_redraw: + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) + elif self.RI.width_resizing_enabled and self.RI.rsz_w is not None and self.RI.currently_resizing_width: + self.RI.delete_resize_lines() + self.delete_resize_lines() + if event.x >= 0: + x = self.canvasx(event.x) + self.new_row_width = self.RI.current_width + event.x + self.create_resize_line(x, y1, x, y2, width=1, fill=self.RI.resizing_line_fg, tag="rwl") + else: + x = self.RI.current_width + event.x + if x < self.min_column_width: + x = int(self.min_column_width) + self.new_row_width = x + self.RI.create_resize_line(x, y1, x, y2, width=1, fill=self.RI.resizing_line_fg, tag="rwl") + elif self.CH.height_resizing_enabled and self.CH.rsz_h is not None and self.CH.currently_resizing_height: + self.CH.delete_resize_lines() + self.delete_resize_lines() + if event.y >= 0: + y = self.canvasy(event.y) + self.new_header_height = self.CH.current_height + event.y + self.create_resize_line(x1, y, x2, y, width=1, fill=self.RI.resizing_line_fg, tag="rhl") + else: + y = self.CH.current_height + event.y + if y < self.min_header_height: + y = int(self.min_header_height) + self.new_header_height = y + self.CH.create_resize_line(x1, y, x2, y, width=1, fill=self.RI.resizing_line_fg, tag="rhl") + if self.extra_b1_motion_func is not None: + self.extra_b1_motion_func(event) + + def ctrl_b1_motion(self, event): + x1, y1, x2, y2 = self.get_canvas_visible_area() + if ( + self.ctrl_select_enabled + and self.drag_selection_enabled + and all(v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w)) + ): + need_redraw = False + end_row = self.identify_row(y=event.y) + end_col = self.identify_col(x=event.x) + currently_selected = self.currently_selected() + if ( + end_row < len(self.row_positions) - 1 + and end_col < len(self.col_positions) - 1 + and currently_selected + and currently_selected.type_ == "cell" + ): + rect = self.get_b1_motion_rect(*(currently_selected[0], currently_selected[1], end_row, end_col)) + if rect is not None and self.being_drawn_rect != rect: + if rect[2] - rect[0] == 1 and rect[3] - rect[1] == 1: + self.being_drawn_rect = rect + else: + if self.being_drawn_rect is not None: + self.delete_selected(*self.being_drawn_rect) + self.being_drawn_rect = rect + self.create_selected(*rect) + if self.drag_selection_binding_func is not None: + self.drag_selection_binding_func(SelectionBoxEvent("drag_select_cells", rect[:-1])) + need_redraw = True + if self.scroll_if_event_offscreen(event): + need_redraw = True + if need_redraw: + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True, redraw_table=True) + elif not self.ctrl_select_enabled: + self.b1_motion(event) + + def b1_release(self, event=None): + if self.being_drawn_rect is not None and ( + self.being_drawn_rect[2] - self.being_drawn_rect[0] > 1 + or self.being_drawn_rect[3] - self.being_drawn_rect[1] > 1 + ): + self.delete_selected(*self.being_drawn_rect) + to_sel = tuple(self.being_drawn_rect) + self.being_drawn_rect = None + self.create_selected(*to_sel) + else: + self.being_drawn_rect = None + if self.RI.width_resizing_enabled and self.RI.rsz_w is not None and self.RI.currently_resizing_width: + self.delete_resize_lines() + self.RI.delete_resize_lines() + self.RI.currently_resizing_width = False + self.RI.set_width(self.new_row_width, set_TL=True) + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + self.b1_pressed_loc = None + elif self.CH.height_resizing_enabled and self.CH.rsz_h is not None and self.CH.currently_resizing_height: + self.delete_resize_lines() + self.CH.delete_resize_lines() + self.CH.currently_resizing_height = False + self.CH.set_height(self.new_header_height, set_TL=True) + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + self.b1_pressed_loc = None + self.RI.rsz_w = None + self.CH.rsz_h = None + if self.b1_pressed_loc is not None: + r = self.identify_row(y=event.y, allow_end=False) + c = self.identify_col(x=event.x, allow_end=False) + if r is not None and c is not None and (r, c) == self.b1_pressed_loc: + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + if self.get_cell_kwargs(datarn, datacn, key="dropdown") or self.get_cell_kwargs( + datarn, datacn, key="checkbox" + ): + canvasx = self.canvasx(event.x) + if ( + self.closed_dropdown != self.b1_pressed_loc + and self.get_cell_kwargs(datarn, datacn, key="dropdown") + and canvasx > self.col_positions[c + 1] - self.txt_h - 4 + and canvasx < self.col_positions[c + 1] - 1 + ) or ( + self.get_cell_kwargs(datarn, datacn, key="checkbox") + and canvasx < self.col_positions[c] + self.txt_h + 4 + and self.canvasy(event.y) < self.row_positions[r] + self.txt_h + 4 + ): + self.open_cell(event) + else: + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.b1_pressed_loc = None + self.closed_dropdown = None + if self.extra_b1_release_func is not None: + self.extra_b1_release_func(event) + + def double_b1(self, event=None): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + x1, y1, x2, y2 = self.get_canvas_visible_area() + if ( + self.identify_col(x=event.x, allow_end=False) is None + or self.identify_row(y=event.y, allow_end=False) is None + ): + self.deselect("all") + elif self.single_selection_enabled and all( + v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w) + ): + r = self.identify_row(y=event.y) + c = self.identify_col(x=event.x) + if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: + self.select_cell(r, c, redraw=True) + if self.edit_cell_enabled: + self.open_cell(event) + elif self.toggle_selection_enabled and all( + v is None for v in (self.RI.rsz_h, self.RI.rsz_w, self.CH.rsz_h, self.CH.rsz_w) + ): + r = self.identify_row(y=event.y) + c = self.identify_col(x=event.x) + if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: + self.toggle_select_cell(r, c, redraw=True) + if self.edit_cell_enabled: + self.open_cell(event) + if self.extra_double_b1_func is not None: + self.extra_double_b1_func(event) + + def identify_row(self, event=None, y=None, allow_end=True): + if event is None: + y2 = self.canvasy(y) + elif y is None: + y2 = self.canvasy(event.y) + r = bisect.bisect_left(self.row_positions, y2) + if r != 0: + r -= 1 + if not allow_end and r >= len(self.row_positions) - 1: + return None + return r + + def identify_col(self, event=None, x=None, allow_end=True): + if event is None: + x2 = self.canvasx(x) + elif x is None: + x2 = self.canvasx(event.x) + c = bisect.bisect_left(self.col_positions, x2) + if c != 0: + c -= 1 + if not allow_end and c >= len(self.col_positions) - 1: + return None + return c + + def fix_views(self): + xcheck = self.xview() + ycheck = self.yview() + if xcheck and xcheck[0] <= 0: + self.xview("moveto", 0) + if self.show_header: + self.CH.xview("moveto", 0) + elif len(xcheck) > 1 and xcheck[1] >= 1: + self.xview("moveto", 1) + if self.show_header: + self.CH.xview("moveto", 1) + if ycheck and ycheck[0] <= 0: + self.yview("moveto", 0) + if self.show_index: + self.RI.yview("moveto", 0) + elif len(ycheck) > 1 and ycheck[1] >= 1: + self.yview("moveto", 1) + if self.show_index: + self.RI.yview("moveto", 1) + + def scroll_if_event_offscreen(self, event): + need_redraw = False + if self.data: + xcheck = self.xview() + ycheck = self.yview() + if len(xcheck) > 1 and xcheck[0] > 0 and event.x < 0: + try: + self.xview_scroll(-1, "units") + self.CH.xview_scroll(-1, "units") + except Exception: + pass + need_redraw = True + if len(ycheck) > 1 and ycheck[0] > 0 and event.y < 0: + try: + self.yview_scroll(-1, "units") + self.RI.yview_scroll(-1, "units") + except Exception: + pass + need_redraw = True + if len(xcheck) > 1 and xcheck[1] < 1 and event.x > self.winfo_width(): + try: + self.xview_scroll(1, "units") + self.CH.xview_scroll(1, "units") + except Exception: + pass + need_redraw = True + if len(ycheck) > 1 and ycheck[1] < 1 and event.y > self.winfo_height(): + try: + self.yview_scroll(1, "units") + self.RI.yview_scroll(1, "units") + except Exception: + pass + need_redraw = True + if need_redraw: + self.fix_views() + self.x_move_synced_scrolls("moveto", self.xview()[0]) + self.y_move_synced_scrolls("moveto", self.yview()[0]) + return need_redraw + + def x_move_synced_scrolls(self, *args, redraw=True): + for widget in self.synced_scrolls: + # try: + if hasattr(widget, "MT"): + widget.MT.set_xviews(*args, move_synced=False, redraw=redraw) + else: + widget.xview(*args) + # except Exception: + # continue + + def y_move_synced_scrolls(self, *args, redraw=True): + for widget in self.synced_scrolls: + # try: + if hasattr(widget, "MT"): + widget.MT.set_yviews(*args, move_synced=False, redraw=redraw) + else: + widget.yview(*args) + # except Exception: + # continue + + def set_xviews(self, *args, move_synced=True, redraw=True): + self.xview(*args) + if self.show_header: + self.CH.xview(*args) + if move_synced: + self.x_move_synced_scrolls(*args) + self.fix_views() + if redraw: + self.main_table_redraw_grid_and_text(redraw_header=True if self.show_header else False) + + def set_yviews(self, *args, move_synced=True, redraw=True): + self.yview(*args) + if self.show_index: + self.RI.yview(*args) + if move_synced: + self.y_move_synced_scrolls(*args) + self.fix_views() + if redraw: + self.main_table_redraw_grid_and_text(redraw_row_index=True if self.show_index else False) + + def set_view(self, x_args, y_args): + self.xview(*x_args) + if self.show_header: + self.CH.xview(*x_args) + self.yview(*y_args) + if self.show_index: + self.RI.yview(*y_args) + self.x_move_synced_scrolls(*x_args) + self.y_move_synced_scrolls(*y_args) + self.fix_views() + self.main_table_redraw_grid_and_text( + redraw_row_index=True if self.show_index else False, + redraw_header=True if self.show_header else False, + ) + + def mousewheel(self, event=None): + if event.delta < 0 or event.num == 5: + self.yview_scroll(1, "units") + self.RI.yview_scroll(1, "units") + self.y_move_synced_scrolls("moveto", self.yview()[0]) + elif event.delta >= 0 or event.num == 4: + if self.canvasy(0) <= 0: + return + self.yview_scroll(-1, "units") + self.RI.yview_scroll(-1, "units") + self.y_move_synced_scrolls("moveto", self.yview()[0]) + self.main_table_redraw_grid_and_text(redraw_row_index=True) + + def shift_mousewheel(self, event=None): + if event.delta < 0 or event.num == 5: + self.xview_scroll(1, "units") + self.CH.xview_scroll(1, "units") + self.x_move_synced_scrolls("moveto", self.xview()[0]) + elif event.delta >= 0 or event.num == 4: + if self.canvasx(0) <= 0: + return + self.xview_scroll(-1, "units") + self.CH.xview_scroll(-1, "units") + self.x_move_synced_scrolls("moveto", self.xview()[0]) + self.main_table_redraw_grid_and_text(redraw_header=True) + + def ctrl_mousewheel(self, event): + if event.delta < 0 or event.num == 5: + if self.table_font[1] < 2 or self.index_font[1] < 2 or self.header_font[1] < 2: + return + self.zoom_out() + elif event.delta >= 0 or event.num == 4: + self.zoom_in() + + def zoom_in(self, event=None): + self.zoom_font( + (self.table_font[0], self.table_font[1] + 1, self.table_font[2]), + (self.header_font[0], self.header_font[1] + 1, self.header_font[2]), + ) + + def zoom_out(self, event=None): + self.zoom_font( + (self.table_font[0], self.table_font[1] - 1, self.table_font[2]), + (self.header_font[0], self.header_font[1] - 1, self.header_font[2]), + ) + + def zoom_font(self, table_font: tuple, header_font: tuple): + # should record position prior to change and then see after change + y = self.canvasy(0) + x = self.canvasx(0) + r = self.identify_row(y=0) + c = self.identify_col(x=0) + try: + r_pc = (y - self.row_positions[r]) / (self.row_positions[r + 1] - self.row_positions[r]) + except Exception: + r_pc = 0.0 + try: + c_pc = (x - self.col_positions[c]) / (self.col_positions[c + 1] - self.col_positions[c]) + except Exception: + c_pc = 0.0 + old_min_row_height = int(self.min_row_height) + old_default_row_height = int(self.default_row_height[1]) + self.set_table_font( + table_font, + reset_row_positions=False, + ) + self.set_index_font(table_font) + self.set_header_font(header_font) + if self.set_cell_sizes_on_zoom: + self.set_all_cell_sizes_to_text() + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + elif not self.set_cell_sizes_on_zoom: + self.row_positions = list( + accumulate( + chain( + [0], + ( + self.min_row_height + if h == old_min_row_height + else self.default_row_height[1] + if h == old_default_row_height + else self.min_row_height + if h < self.min_row_height + else h + for h in self.diff_gen(self.row_positions) + ), + ) + ) + ) + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + self.recreate_all_selection_boxes() + self.refresh_open_window_positions() + self.RI.refresh_open_window_positions() + self.CH.refresh_open_window_positions() + self.see( + r, + c, + check_cell_visibility=False, + redraw=False, + r_pc=r_pc, + c_pc=c_pc, + ) + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + + def diff_gen(self, seq): + return ( + int(b - a) + for a, b in zip( + seq, + islice(seq, 1, None), + ) + ) + + def get_txt_w(self, txt, font=None): + self.txt_measure_canvas.itemconfig( + self.txt_measure_canvas_text, + text=txt, + font=self.table_font if font is None else font, + ) + b = self.txt_measure_canvas.bbox(self.txt_measure_canvas_text) + return b[2] - b[0] + + def get_txt_h(self, txt, font=None): + self.txt_measure_canvas.itemconfig( + self.txt_measure_canvas_text, + text=txt, + font=self.table_font if font is None else font, + ) + b = self.txt_measure_canvas.bbox(self.txt_measure_canvas_text) + return b[3] - b[1] + + def get_txt_dimensions(self, txt, font=None): + self.txt_measure_canvas.itemconfig( + self.txt_measure_canvas_text, + text=txt, + font=self.table_font if font is None else font, + ) + b = self.txt_measure_canvas.bbox(self.txt_measure_canvas_text) + return b[2] - b[0], b[3] - b[1] + + def get_lines_cell_height(self, n, font=None): + return ( + self.get_txt_h( + txt="\n".join(["j^|" for lines in range(n)]) if n > 1 else "j^|", + font=self.table_font if font is None else font, + ) + + 5 + ) + + def set_min_column_width(self): + self.min_column_width = 5 + if self.min_column_width > self.max_column_width: + self.max_column_width = self.min_column_width + 20 + if self.min_column_width > self.default_column_width: + self.default_column_width = self.min_column_width + 20 + + def set_table_font(self, newfont=None, reset_row_positions=False): + if newfont: + if not isinstance(newfont, tuple): + raise ValueError("Argument must be tuple e.g. ('Carlito',12,'normal')") + if len(newfont) != 3: + raise ValueError("Argument must be three-tuple") + if not isinstance(newfont[0], str) or not isinstance(newfont[1], int) or not isinstance(newfont[2], str): + raise ValueError( + "Argument must be font, size and 'normal', 'bold' or 'italic' e.g. ('Carlito',12,'normal')" + ) + self.table_font = newfont + self.set_table_font_help() + if reset_row_positions: + self.reset_row_positions() + self.recreate_all_selection_boxes() + else: + return self.table_font + + def set_table_font_help(self): + self.txt_h = self.get_txt_h("|ZXjy*'^") + self.txt_w = self.get_txt_w("|") + self.half_txt_h = ceil(self.txt_h / 2) + if self.half_txt_h % 2 == 0: + self.fl_ins = self.half_txt_h + 2 + else: + self.fl_ins = self.half_txt_h + 3 + self.xtra_lines_increment = int(self.txt_h) + self.min_row_height = self.txt_h + 5 + if self.min_row_height < 12: + self.min_row_height = 12 + if self.default_row_height[0] != "pixels": + self.default_row_height = ( + self.default_row_height[0] if self.default_row_height[0] != "pixels" else "pixels", + self.get_lines_cell_height(int(self.default_row_height[0])) + if self.default_row_height[0] != "pixels" + else self.default_row_height[1], + ) + self.set_min_column_width() + + def set_header_font(self, newfont=None): + if newfont: + if not isinstance(newfont, tuple): + raise ValueError("Argument must be tuple e.g. ('Carlito', 12, 'normal')") + if len(newfont) != 3: + raise ValueError("Argument must be three-tuple") + if not isinstance(newfont[0], str) or not isinstance(newfont[1], int) or not isinstance(newfont[2], str): + raise ValueError( + "Argument must be font, size and 'normal', 'bold' or 'italic' e.g. ('Carlito', 12, 'normal')" + ) + self.header_font = newfont + self.set_header_font_help() + self.recreate_all_selection_boxes() + else: + return self.header_font + + def set_header_font_help(self): + self.header_txt_w, self.header_txt_h = self.get_txt_dimensions("|", self.header_font) + self.header_half_txt_h = ceil(self.header_txt_h / 2) + if self.header_half_txt_h % 2 == 0: + self.header_fl_ins = self.header_half_txt_h + 2 + else: + self.header_fl_ins = self.header_half_txt_h + 3 + self.header_xtra_lines_increment = self.header_txt_h + self.min_header_height = self.header_txt_h + 5 + if self.default_header_height[0] != "pixels": + self.default_header_height = ( + self.default_header_height[0] if self.default_header_height[0] != "pixels" else "pixels", + self.get_lines_cell_height(int(self.default_header_height[0]), font=self.header_font) + if self.default_header_height[0] != "pixels" + else self.default_header_height[1], + ) + self.set_min_column_width() + self.CH.set_height(self.default_header_height[1], set_TL=True) + + def set_index_font(self, newfont=None): + if newfont: + if not isinstance(newfont, tuple): + raise ValueError("Argument must be tuple e.g. ('Carlito', 12, 'normal')") + if len(newfont) != 3: + raise ValueError("Argument must be three-tuple") + if not isinstance(newfont[0], str) or not isinstance(newfont[1], int) or not isinstance(newfont[2], str): + raise ValueError( + "Argument must be font, size and 'normal', 'bold' or" "'italic' e.g. ('Carlito',12,'normal')" + ) + self.index_font = newfont + self.set_index_font_help() + return self.index_font + + def set_index_font_help(self): + self.index_txt_width, self.index_txt_height = self.get_txt_dimensions("|", self.index_font) + self.index_half_txt_height = ceil(self.index_txt_height / 2) + if self.index_half_txt_height % 2 == 0: + self.index_first_ln_ins = self.index_half_txt_height + 2 + else: + self.index_first_ln_ins = self.index_half_txt_height + 3 + self.index_xtra_lines_increment = self.index_txt_height + self.min_index_width = 5 + + def data_reference( + self, + newdataref=None, + reset_col_positions=True, + reset_row_positions=True, + redraw=False, + return_id=True, + keep_formatting=True, + ): + if isinstance(newdataref, (list, tuple)): + self.data = newdataref + if keep_formatting: + self.reapply_formatting() + else: + self.delete_all_formatting(clear_values=False) + self.undo_storage = deque(maxlen=self.max_undos) + if reset_col_positions: + self.reset_col_positions() + if reset_row_positions: + self.reset_row_positions() + if redraw: + self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if return_id: + return id(self.data) + else: + return self.data + + def get_cell_dimensions(self, datarn, datacn): + txt = self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True) + if txt: + self.txt_measure_canvas.itemconfig(self.txt_measure_canvas_text, text=txt, font=self.table_font) + b = self.txt_measure_canvas.bbox(self.txt_measure_canvas_text) + w = b[2] - b[0] + 7 + h = b[3] - b[1] + 5 + else: + w = self.min_column_width + h = self.min_row_height + if self.get_cell_kwargs(datarn, datacn, key="dropdown") or self.get_cell_kwargs(datarn, datacn, key="checkbox"): + return w + self.txt_h, h + return w, h + + def set_cell_size_to_text(self, r, c, only_set_if_too_small=False, redraw=True, run_binding=False): + min_column_width = int(self.min_column_width) + min_rh = int(self.min_row_height) + w = min_column_width + h = min_rh + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + tw, h = self.get_cell_dimensions(datarn, datacn) + if tw > w: + w = tw + if h < min_rh: + h = int(min_rh) + elif h > self.max_row_height: + h = int(self.max_row_height) + if w < min_column_width: + w = int(min_column_width) + elif w > self.max_column_width: + w = int(self.max_column_width) + cell_needs_resize_w = False + cell_needs_resize_h = False + if only_set_if_too_small: + if w > self.col_positions[c + 1] - self.col_positions[c]: + cell_needs_resize_w = True + if h > self.row_positions[r + 1] - self.row_positions[r]: + cell_needs_resize_h = True + else: + if w != self.col_positions[c + 1] - self.col_positions[c]: + cell_needs_resize_w = True + if h != self.row_positions[r + 1] - self.row_positions[r]: + cell_needs_resize_h = True + if cell_needs_resize_w: + old_width = self.col_positions[c + 1] - self.col_positions[c] + new_col_pos = self.col_positions[c] + w + increment = new_col_pos - self.col_positions[c + 1] + self.col_positions[c + 2 :] = [ + e + increment for e in islice(self.col_positions, c + 2, len(self.col_positions)) + ] + self.col_positions[c + 1] = new_col_pos + new_width = self.col_positions[c + 1] - self.col_positions[c] + if run_binding and self.CH.column_width_resize_func is not None and old_width != new_width: + self.CH.column_width_resize_func(ResizeEvent("column_width_resize", c, old_width, new_width)) + if cell_needs_resize_h: + old_height = self.row_positions[r + 1] - self.row_positions[r] + new_row_pos = self.row_positions[r] + h + increment = new_row_pos - self.row_positions[r + 1] + self.row_positions[r + 2 :] = [ + e + increment for e in islice(self.row_positions, r + 2, len(self.row_positions)) + ] + self.row_positions[r + 1] = new_row_pos + new_height = self.row_positions[r + 1] - self.row_positions[r] + if run_binding and self.RI.row_height_resize_func is not None and old_height != new_height: + self.RI.row_height_resize_func(ResizeEvent("row_height_resize", r, old_height, new_height)) + if cell_needs_resize_w or cell_needs_resize_h: + self.recreate_all_selection_boxes() + self.allow_auto_resize_columns = not cell_needs_resize_w + self.allow_auto_resize_rows = not cell_needs_resize_h + if redraw: + self.refresh() + return True + else: + return False + + def set_all_cell_sizes_to_text(self, include_index=False): + min_column_width = int(self.min_column_width) + min_rh = int(self.min_row_height) + w = min_column_width + h = min_rh + rhs = defaultdict(lambda: int(min_rh)) + cws = [] + x = self.txt_measure_canvas.create_text(0, 0, text="", font=self.table_font) + x2 = self.txt_measure_canvas.create_text(0, 0, text="", font=self.header_font) + itmcon = self.txt_measure_canvas.itemconfig + itmbbx = self.txt_measure_canvas.bbox + if self.all_columns_displayed: + itercols = range(self.total_data_cols()) + else: + itercols = self.displayed_columns + if self.all_rows_displayed: + iterrows = range(self.total_data_rows()) + else: + iterrows = self.displayed_rows + if is_iterable(self._row_index): + for datarn in iterrows: + w_, h = self.RI.get_cell_dimensions(datarn) + if h < min_rh: + h = int(min_rh) + elif h > self.max_row_height: + h = int(self.max_row_height) + if h > rhs[datarn]: + rhs[datarn] = h + for datacn in itercols: + w, h_ = self.CH.get_cell_dimensions(datacn) + if self.all_rows_displayed: + # refresh range generator if needed + iterrows = range(self.total_data_rows()) + for datarn in iterrows: + txt = self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True) + if txt: + itmcon(x, text=txt) + b = itmbbx(x) + tw = b[2] - b[0] + 7 + h = b[3] - b[1] + 5 + else: + tw = min_column_width + h = min_rh + if self.get_cell_kwargs(datarn, datacn, key="dropdown") or self.get_cell_kwargs( + datarn, datacn, key="checkbox" + ): + tw += self.txt_h + if tw > w: + w = tw + if h < min_rh: + h = int(min_rh) + elif h > self.max_row_height: + h = int(self.max_row_height) + if h > rhs[datarn]: + rhs[datarn] = h + if w < min_column_width: + w = int(min_column_width) + elif w > self.max_column_width: + w = int(self.max_column_width) + cws.append(w) + self.txt_measure_canvas.delete(x) + self.txt_measure_canvas.delete(x2) + self.row_positions = list(accumulate(chain([0], (height for height in rhs.values())))) + self.col_positions = list(accumulate(chain([0], (width for width in cws)))) + self.recreate_all_selection_boxes() + return self.row_positions, self.col_positions + + def reset_col_positions(self, ncols=None): + colpos = int(self.default_column_width) + if self.all_columns_displayed: + self.col_positions = list( + accumulate( + chain( + [0], + (colpos for c in range(ncols if ncols is not None else self.total_data_cols())), + ) + ) + ) + else: + self.col_positions = list( + accumulate( + chain( + [0], + (colpos for c in range(ncols if ncols is not None else len(self.displayed_columns))), + ) + ) + ) + + def reset_row_positions(self, nrows=None): + rowpos = self.default_row_height[1] + if self.all_rows_displayed: + self.row_positions = list( + accumulate( + chain( + [0], + (rowpos for r in range(nrows if nrows is not None else self.total_data_rows())), + ) + ) + ) + else: + self.row_positions = list( + accumulate( + chain( + [0], + (rowpos for r in range(nrows if nrows is not None else len(self.displayed_rows))), + ) + ) + ) + + def del_col_position(self, idx, deselect_all=False): + if deselect_all: + self.deselect("all", redraw=False) + if idx == "end" or len(self.col_positions) <= idx + 1: + del self.col_positions[-1] + else: + w = self.col_positions[idx + 1] - self.col_positions[idx] + idx += 1 + del self.col_positions[idx] + self.col_positions[idx:] = [e - w for e in islice(self.col_positions, idx, len(self.col_positions))] + + def del_row_position(self, idx, deselect_all=False): + if deselect_all: + self.deselect("all", redraw=False) + if idx == "end" or len(self.row_positions) <= idx + 1: + del self.row_positions[-1] + else: + w = self.row_positions[idx + 1] - self.row_positions[idx] + idx += 1 + del self.row_positions[idx] + self.row_positions[idx:] = [e - w for e in islice(self.row_positions, idx, len(self.row_positions))] + + def del_col_positions(self, idx, num=1, deselect_all=False): + if deselect_all: + self.deselect("all", redraw=False) + if idx == "end" or len(self.col_positions) <= idx + 1: + del self.col_positions[-1] + else: + cws = list(self.diff_gen(self.col_positions)) + cws[idx : idx + num] = [] + self.col_positions = list(accumulate(chain([0], (width for width in cws)))) + + def del_row_positions(self, idx, numrows=1, deselect_all=False): + if deselect_all: + self.deselect("all", redraw=False) + if idx == "end" or len(self.row_positions) <= idx + 1: + del self.row_positions[-1] + else: + rhs = list(self.diff_gen(self.row_positions)) + rhs[idx : idx + numrows] = [] + self.row_positions = list(accumulate(chain([0], (height for height in rhs)))) + + def insert_col_position(self, idx="end", width=None, deselect_all=False): + if deselect_all: + self.deselect("all", redraw=False) + if width is None: + w = self.default_column_width + else: + w = width + if idx == "end" or len(self.col_positions) == idx + 1: + self.col_positions.append(self.col_positions[-1] + w) + else: + idx += 1 + self.col_positions.insert(idx, self.col_positions[idx - 1] + w) + idx += 1 + self.col_positions[idx:] = [e + w for e in islice(self.col_positions, idx, len(self.col_positions))] + + def insert_row_position(self, idx, height=None, deselect_all=False): + if deselect_all: + self.deselect("all", redraw=False) + if height is None: + h = self.default_row_height[1] + else: + h = height + if idx == "end" or len(self.row_positions) == idx + 1: + self.row_positions.append(self.row_positions[-1] + h) + else: + idx += 1 + self.row_positions.insert(idx, self.row_positions[idx - 1] + h) + idx += 1 + self.row_positions[idx:] = [e + h for e in islice(self.row_positions, idx, len(self.row_positions))] + + def insert_col_positions(self, idx="end", widths=None, deselect_all=False): + if deselect_all: + self.deselect("all", redraw=False) + if widths is None: + w = [self.default_column_width] + elif isinstance(widths, int): + w = list(repeat(self.default_column_width, widths)) + else: + w = widths + if idx == "end" or len(self.col_positions) == idx + 1: + if len(w) > 1: + self.col_positions += list(accumulate(chain([self.col_positions[-1] + w[0]], islice(w, 1, None)))) + else: + self.col_positions.append(self.col_positions[-1] + w[0]) + else: + if len(w) > 1: + idx += 1 + self.col_positions[idx:idx] = list( + accumulate(chain([self.col_positions[idx - 1] + w[0]], islice(w, 1, None))) + ) + idx += len(w) + sumw = sum(w) + self.col_positions[idx:] = [e + sumw for e in islice(self.col_positions, idx, len(self.col_positions))] + else: + w = w[0] + idx += 1 + self.col_positions.insert(idx, self.col_positions[idx - 1] + w) + idx += 1 + self.col_positions[idx:] = [e + w for e in islice(self.col_positions, idx, len(self.col_positions))] + + def insert_row_positions(self, idx="end", heights=None, deselect_all=False): + if deselect_all: + self.deselect("all", redraw=False) + if heights is None: + h = [self.default_row_height[1]] + elif isinstance(heights, int): + h = list(repeat(self.default_row_height[1], heights)) + else: + h = heights + if idx == "end" or len(self.row_positions) == idx + 1: + if len(h) > 1: + self.row_positions += list(accumulate(chain([self.row_positions[-1] + h[0]], islice(h, 1, None)))) + else: + self.row_positions.append(self.row_positions[-1] + h[0]) + else: + if len(h) > 1: + idx += 1 + self.row_positions[idx:idx] = list( + accumulate(chain([self.row_positions[idx - 1] + h[0]], islice(h, 1, None))) + ) + idx += len(h) + sumh = sum(h) + self.row_positions[idx:] = [e + sumh for e in islice(self.row_positions, idx, len(self.row_positions))] + else: + h = h[0] + idx += 1 + self.row_positions.insert(idx, self.row_positions[idx - 1] + h) + idx += 1 + self.row_positions[idx:] = [e + h for e in islice(self.row_positions, idx, len(self.row_positions))] + + def insert_cols_rc(self, event=None): + if self.anything_selected(exclude_rows=True, exclude_cells=True): + selcols = self.get_selected_cols() + numcols = len(selcols) + displayed_ins_col = min(selcols) if event == "left" else max(selcols) + 1 + if self.all_columns_displayed: + data_ins_col = int(displayed_ins_col) + else: + if displayed_ins_col == len(self.col_positions) - 1: + rowlen = len(max(self.data, key=len)) if self.data else 0 + data_ins_col = rowlen + else: + try: + data_ins_col = int(self.displayed_columns[displayed_ins_col]) + except Exception: + data_ins_col = int(self.displayed_columns[displayed_ins_col - 1]) + else: + numcols = 1 + displayed_ins_col = len(self.col_positions) - 1 + data_ins_col = int(displayed_ins_col) + if ( + isinstance(self.paste_insert_column_limit, int) + and self.paste_insert_column_limit < displayed_ins_col + numcols + ): + numcols = self.paste_insert_column_limit - len(self.col_positions) - 1 + if numcols < 1: + return + if self.extra_begin_insert_cols_rc_func is not None: + try: + self.extra_begin_insert_cols_rc_func( + InsertEvent("begin_insert_columns", data_ins_col, displayed_ins_col, numcols) + ) + except Exception: + return + saved_displayed_columns = list(self.displayed_columns) + if not self.all_columns_displayed: + if displayed_ins_col == len(self.col_positions) - 1: + self.displayed_columns += list(range(rowlen, rowlen + numcols)) + else: + if displayed_ins_col > len(self.displayed_columns) - 1: + adj_ins = displayed_ins_col - 1 + else: + adj_ins = displayed_ins_col + part1 = self.displayed_columns[:adj_ins] + part2 = list( + range( + self.displayed_columns[adj_ins], + self.displayed_columns[adj_ins] + numcols + 1, + ) + ) + part3 = ( + [] + if displayed_ins_col > len(self.displayed_columns) - 1 + else [cn + numcols for cn in islice(self.displayed_columns, adj_ins + 1, None)] + ) + self.displayed_columns = part1 + part2 + part3 + self.insert_col_positions(idx=displayed_ins_col, widths=numcols, deselect_all=True) + self.cell_options = { + (rn, cn if cn < data_ins_col else cn + numcols): t2 for (rn, cn), t2 in self.cell_options.items() + } + self.col_options = {cn if cn < data_ins_col else cn + numcols: t for cn, t in self.col_options.items()} + self.CH.cell_options = {cn if cn < data_ins_col else cn + numcols: t for cn, t in self.CH.cell_options.items()} + self.CH.fix_header() + if self._headers and isinstance(self._headers, list): + if data_ins_col >= len(self._headers): + self.CH.fix_header( + datacn=data_ins_col, + fix_values=(data_ins_col, data_ins_col + numcols), + ) + else: + self._headers[data_ins_col:data_ins_col] = self.CH.get_empty_header_seq( + end=data_ins_col + numcols, start=data_ins_col, c_ops=False + ) + if self.row_positions == [0] and not self.data: + self.insert_row_position(idx="end", height=int(self.min_row_height), deselect_all=False) + self.data.append(self.get_empty_row_seq(0, end=data_ins_col + numcols, start=data_ins_col, c_ops=False)) + else: + end = data_ins_col + numcols + for rn in range(len(self.data)): + self.data[rn][data_ins_col:data_ins_col] = self.get_empty_row_seq(rn, end, data_ins_col, c_ops=False) + self.create_selected( + 0, + displayed_ins_col, + len(self.row_positions) - 1, + displayed_ins_col + numcols, + "columns", + ) + self.set_currently_selected(0, displayed_ins_col, "column") + if self.undo_enabled: + self.undo_storage.append( + zlib.compress( + pickle.dumps( + ( + "insert_cols", + { + "data_col_num": data_ins_col, + "displayed_columns": saved_displayed_columns, + "sheet_col_num": displayed_ins_col, + "numcols": numcols, + }, + ) + ) + ) + ) + self.refresh() + if self.extra_end_insert_cols_rc_func is not None: + self.extra_end_insert_cols_rc_func( + InsertEvent("end_insert_columns", data_ins_col, displayed_ins_col, numcols) + ) + self.parentframe.emit_event("<>") + + def insert_rows_rc(self, event=None): + if self.anything_selected(exclude_columns=True, exclude_cells=True): + selrows = self.get_selected_rows() + numrows = len(selrows) + displayed_ins_row = min(selrows) if event == "above" else max(selrows) + 1 + if self.all_rows_displayed: + data_ins_row = int(displayed_ins_row) + else: + if displayed_ins_row == len(self.row_positions) - 1: + datalen = len(self.data) + data_ins_row = datalen + else: + try: + data_ins_row = int(self.displayed_rows[displayed_ins_row]) + except Exception: + data_ins_row = int(self.displayed_rows[displayed_ins_row - 1]) + else: + numrows = 1 + displayed_ins_row = len(self.row_positions) - 1 + data_ins_row = int(displayed_ins_row) + if isinstance(self.paste_insert_row_limit, int) and self.paste_insert_row_limit < displayed_ins_row + numrows: + numrows = self.paste_insert_row_limit - len(self.row_positions) - 1 + if numrows < 1: + return + if self.extra_begin_insert_rows_rc_func is not None: + try: + self.extra_begin_insert_rows_rc_func( + InsertEvent("begin_insert_rows", data_ins_row, displayed_ins_row, numrows) + ) + except Exception: + return + saved_displayed_rows = list(self.displayed_rows) + if not self.all_rows_displayed: + if displayed_ins_row == len(self.row_positions) - 1: + self.displayed_rows += list(range(datalen, datalen + numrows)) + else: + if displayed_ins_row > len(self.displayed_rows) - 1: + adj_ins = displayed_ins_row - 1 + else: + adj_ins = displayed_ins_row + part1 = self.displayed_rows[:adj_ins] + part2 = list( + range( + self.displayed_rows[adj_ins], + self.displayed_rows[adj_ins] + numrows + 1, + ) + ) + part3 = ( + [] + if displayed_ins_row > len(self.displayed_rows) - 1 + else [rn + numrows for rn in islice(self.displayed_rows, adj_ins + 1, None)] + ) + self.displayed_rows = part1 + part2 + part3 + self.insert_row_positions(idx=displayed_ins_row, heights=numrows, deselect_all=True) + self.cell_options = { + (rn if rn < data_ins_row else rn + numrows, cn): t2 for (rn, cn), t2 in self.cell_options.items() + } + self.row_options = {rn if rn < data_ins_row else rn + numrows: t for rn, t in self.row_options.items()} + self.RI.cell_options = {rn if rn < data_ins_row else rn + numrows: t for rn, t in self.RI.cell_options.items()} + self.RI.fix_index() + if self._row_index and isinstance(self._row_index, list): + if data_ins_row >= len(self._row_index): + self.RI.fix_index( + datacn=data_ins_row, + fix_values=(data_ins_row, data_ins_row + numrows), + ) + else: + self._row_index[data_ins_row:data_ins_row] = self.RI.get_empty_index_seq( + data_ins_row + numrows, data_ins_row, r_ops=False + ) + if self.col_positions == [0] and not self.data: + self.insert_col_position(idx="end", width=None, deselect_all=False) + self.data.append(self.get_empty_row_seq(0, end=data_ins_row + numrows, start=data_ins_row, r_ops=False)) + else: + total_data_cols = self.total_data_cols() + self.data[data_ins_row:data_ins_row] = [ + self.get_empty_row_seq(rn, total_data_cols, r_ops=False) + for rn in range(data_ins_row, data_ins_row + numrows) + ] + self.create_selected( + displayed_ins_row, + 0, + displayed_ins_row + numrows, + len(self.col_positions) - 1, + "rows", + ) + self.set_currently_selected(displayed_ins_row, 0, "row") + if self.undo_enabled: + self.undo_storage.append( + zlib.compress( + pickle.dumps( + ( + "insert_rows", + { + "data_row_num": data_ins_row, + "displayed_rows": saved_displayed_rows, + "sheet_row_num": displayed_ins_row, + "numrows": numrows, + }, + ) + ) + ) + ) + self.refresh() + if self.extra_end_insert_rows_rc_func is not None: + self.extra_end_insert_rows_rc_func(InsertEvent("end_insert_rows", data_ins_row, displayed_ins_row, numrows)) + self.parentframe.emit_event("<>") + + def del_cols_rc(self, event=None, c=None): + seld_cols = sorted(self.get_selected_cols()) + curr = self.currently_selected() + if not seld_cols or not curr: + return + if ( + self.CH.popup_menu_loc is None + or self.CH.popup_menu_loc < seld_cols[0] + or self.CH.popup_menu_loc > seld_cols[-1] + ): + c = seld_cols[0] + else: + c = self.CH.popup_menu_loc + seld_cols = get_seq_without_gaps_at_index(seld_cols, c) + self.deselect("all", redraw=False) + self.create_selected( + 0, + seld_cols[0], + len(self.row_positions) - 1, + seld_cols[-1] + 1, + "columns", + ) + self.set_currently_selected(0, seld_cols[0], type_="column") + seldmax = seld_cols[-1] if self.all_columns_displayed else self.displayed_columns[seld_cols[-1]] + if self.extra_begin_del_cols_rc_func is not None: + try: + self.extra_begin_del_cols_rc_func(DeleteRowColumnEvent("begin_delete_columns", seld_cols)) + except Exception: + return + seldset = set(seld_cols) if self.all_columns_displayed else set(self.displayed_columns[c] for c in seld_cols) + if self.undo_enabled: + undo_storage = { + "deleted_cols": {}, + "colwidths": {}, + "deleted_header_values": {}, + "selection_boxes": self.get_boxes(), + "displayed_columns": list(self.displayed_columns) + if not isinstance(self.displayed_columns, int) + else int(self.displayed_columns), + "cell_options": {k: v.copy() for k, v in self.cell_options.items()}, + "col_options": {k: v.copy() for k, v in self.col_options.items()}, + "CH_cell_options": {k: v.copy() for k, v in self.CH.cell_options.items()}, + } + for c in reversed(seld_cols): + undo_storage["colwidths"][c] = self.col_positions[c + 1] - self.col_positions[c] + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + for rn in range(len(self.data)): + if datacn not in undo_storage["deleted_cols"]: + undo_storage["deleted_cols"][datacn] = {} + try: + undo_storage["deleted_cols"][datacn][rn] = self.data[rn].pop(datacn) + except Exception: + continue + try: + undo_storage["deleted_header_values"][datacn] = self._headers.pop(datacn) + except Exception: + continue + else: + for c in reversed(seld_cols): + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + for rn in range(len(self.data)): + del self.data[rn][datacn] + try: + del self._headers[datacn] + except Exception: + continue + if self.undo_enabled: + self.undo_storage.append(("delete_cols", undo_storage)) + for c in reversed(seld_cols): + self.del_col_position(c, deselect_all=False) + numcols = len(seld_cols) + self.cell_options = { + (rn, cn if cn < seldmax else cn - numcols): t2 + for (rn, cn), t2 in self.cell_options.items() + if cn not in seldset + } + self.col_options = { + cn if cn < seldmax else cn - numcols: t for cn, t in self.col_options.items() if cn not in seldset + } + self.CH.cell_options = { + cn if cn < seldmax else cn - numcols: t for cn, t in self.CH.cell_options.items() if cn not in seldset + } + self.deselect("all", redraw=False) + if not self.all_columns_displayed: + self.displayed_columns = [c for c in self.displayed_columns if c not in seldset] + for c in sorted(seldset): + self.displayed_columns = [dc if c > dc else dc - 1 for dc in self.displayed_columns] + self.refresh() + if self.extra_end_del_cols_rc_func is not None: + self.extra_end_del_cols_rc_func(DeleteRowColumnEvent("end_delete_columns", seld_cols)) + self.parentframe.emit_event("<>") + + def del_rows_rc(self, event=None, r=None): + seld_rows = sorted(self.get_selected_rows()) + curr = self.currently_selected() + if not seld_rows or not curr: + return + if ( + self.RI.popup_menu_loc is None + or self.RI.popup_menu_loc < seld_rows[0] + or self.RI.popup_menu_loc > seld_rows[-1] + ): + r = seld_rows[0] + else: + r = self.RI.popup_menu_loc + seld_rows = get_seq_without_gaps_at_index(seld_rows, r) + self.deselect("all", redraw=False) + self.create_selected( + seld_rows[0], + 0, + seld_rows[-1] + 1, + len(self.col_positions) - 1, + "rows", + ) + self.set_currently_selected(seld_rows[0], 0, type_="row") + seldmax = seld_rows[-1] if self.all_rows_displayed else self.displayed_rows[seld_rows[-1]] + if self.extra_begin_del_rows_rc_func is not None: + try: + self.extra_begin_del_rows_rc_func(DeleteRowColumnEvent("begin_delete_rows", seld_rows)) + except Exception: + return + seldset = set(seld_rows) if self.all_rows_displayed else set(self.displayed_rows[r] for r in seld_rows) + if self.undo_enabled: + undo_storage = { + "deleted_rows": [], + "rowheights": {}, + "deleted_index_values": [], + "selection_boxes": self.get_boxes(), + "displayed_rows": list(self.displayed_rows) + if not isinstance(self.displayed_rows, int) + else int(self.displayed_rows), + "cell_options": {k: v.copy() for k, v in self.cell_options.items()}, + "row_options": {k: v.copy() for k, v in self.row_options.items()}, + "RI_cell_options": {k: v.copy() for k, v in self.RI.cell_options.items()}, + } + for r in reversed(seld_rows): + undo_storage["rowheights"][r] = self.row_positions[r + 1] - self.row_positions[r] + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + undo_storage["deleted_rows"].append((datarn, self.data.pop(datarn))) + try: + undo_storage["deleted_index_values"].append((datarn, self._row_index.pop(datarn))) + except Exception: + continue + else: + for r in reversed(seld_rows): + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + del self.data[datarn] + try: + del self._row_index[datarn] + except Exception: + continue + if self.undo_enabled: + self.undo_storage.append(("delete_rows", undo_storage)) + for r in reversed(seld_rows): + self.del_row_position(r, deselect_all=False) + numrows = len(seld_rows) + self.cell_options = { + (rn if rn < seldmax else rn - numrows, cn): t2 + for (rn, cn), t2 in self.cell_options.items() + if rn not in seldset + } + self.row_options = { + rn if rn < seldmax else rn - numrows: t for rn, t in self.row_options.items() if rn not in seldset + } + self.RI.cell_options = { + rn if rn < seldmax else rn - numrows: t for rn, t in self.RI.cell_options.items() if rn not in seldset + } + self.deselect("all", redraw=False) + if not self.all_rows_displayed: + self.displayed_rows = [r for r in self.displayed_rows if r not in seldset] + for r in sorted(seldset): + self.displayed_rows = [dr if r > dr else dr - 1 for dr in self.displayed_rows] + self.refresh() + if self.extra_end_del_rows_rc_func is not None: + self.extra_end_del_rows_rc_func(DeleteRowColumnEvent("end_delete_rows", seld_rows)) + self.parentframe.emit_event("<>") + + def move_row_position(self, idx1, idx2): + if not len(self.row_positions) <= 2: + if idx1 < idx2: + height = self.row_positions[idx1 + 1] - self.row_positions[idx1] + self.row_positions.insert(idx2 + 1, self.row_positions.pop(idx1 + 1)) + for i in range(idx1 + 1, idx2 + 1): + self.row_positions[i] -= height + self.row_positions[idx2 + 1] = self.row_positions[idx2] + height + else: + height = self.row_positions[idx1 + 1] - self.row_positions[idx1] + self.row_positions.insert(idx2 + 1, self.row_positions.pop(idx1 + 1)) + for i in range(idx2 + 2, idx1 + 2): + self.row_positions[i] += height + self.row_positions[idx2 + 1] = self.row_positions[idx2] + height + + def move_col_position(self, idx1, idx2): + if not len(self.col_positions) <= 2: + if idx1 < idx2: + width = self.col_positions[idx1 + 1] - self.col_positions[idx1] + self.col_positions.insert(idx2 + 1, self.col_positions.pop(idx1 + 1)) + for i in range(idx1 + 1, idx2 + 1): + self.col_positions[i] -= width + self.col_positions[idx2 + 1] = self.col_positions[idx2] + width + else: + width = self.col_positions[idx1 + 1] - self.col_positions[idx1] + self.col_positions.insert(idx2 + 1, self.col_positions.pop(idx1 + 1)) + for i in range(idx2 + 2, idx1 + 2): + self.col_positions[i] += width + self.col_positions[idx2 + 1] = self.col_positions[idx2] + width + + def display_rows( + self, + rows=None, + all_rows_displayed=None, + reset_row_positions=True, + deselect_all=True, + ): + if rows is None and all_rows_displayed is None: + return list(range(self.total_data_rows())) if self.all_rows_displayed else self.displayed_rows + total_data_rows = None + if (rows is not None and rows != self.displayed_rows) or (all_rows_displayed and not self.all_rows_displayed): + self.undo_storage = deque(maxlen=self.max_undos) + if rows is not None and rows != self.displayed_rows: + self.displayed_rows = sorted(rows) + if all_rows_displayed: + if not self.all_rows_displayed: + total_data_rows = self.total_data_rows() + self.displayed_rows = list(range(total_data_rows)) + self.all_rows_displayed = True + elif all_rows_displayed is not None and not all_rows_displayed: + self.all_rows_displayed = False + if reset_row_positions: + self.reset_row_positions(nrows=total_data_rows) + if deselect_all: + self.deselect("all", redraw=False) + + def display_columns( + self, + columns=None, + all_columns_displayed=None, + reset_col_positions=True, + deselect_all=True, + ): + if columns is None and all_columns_displayed is None: + return list(range(self.total_data_cols())) if self.all_columns_displayed else self.displayed_columns + total_data_cols = None + if (columns is not None and columns != self.displayed_columns) or ( + all_columns_displayed and not self.all_columns_displayed + ): + self.undo_storage = deque(maxlen=self.max_undos) + if columns is not None and columns != self.displayed_columns: + self.displayed_columns = sorted(columns) + if all_columns_displayed: + if not self.all_columns_displayed: + total_data_cols = self.total_data_cols() + self.displayed_columns = list(range(total_data_cols)) + self.all_columns_displayed = True + elif all_columns_displayed is not None and not all_columns_displayed: + self.all_columns_displayed = False + if reset_col_positions: + self.reset_col_positions(ncols=total_data_cols) + if deselect_all: + self.deselect("all", redraw=False) + + def headers( + self, + newheaders=None, + index=None, + reset_col_positions=False, + show_headers_if_not_sheet=True, + redraw=False, + ): + if newheaders is not None: + if isinstance(newheaders, (list, tuple)): + self._headers = list(newheaders) if isinstance(newheaders, tuple) else newheaders + elif isinstance(newheaders, int): + self._headers = int(newheaders) + elif isinstance(self._headers, list) and isinstance(index, int): + if index >= len(self._headers): + self.CH.fix_header(index) + self._headers[index] = f"{newheaders}" + elif not isinstance(newheaders, (list, tuple, int)) and index is None: + try: + self._headers = list(newheaders) + except Exception: + raise ValueError("New header must be iterable or int (use int to use a row as the header") + if reset_col_positions: + self.reset_col_positions() + elif ( + show_headers_if_not_sheet + and isinstance(self._headers, list) + and (self.col_positions == [0] or not self.col_positions) + ): + colpos = int(self.default_column_width) + if self.all_columns_displayed: + self.col_positions = list(accumulate(chain([0], (colpos for c in range(len(self._headers)))))) + else: + self.col_positions = list( + accumulate( + chain( + [0], + (colpos for c in range(len(self.displayed_columns))), + ) + ) + ) + if redraw: + self.refresh() + else: + if not isinstance(self._headers, int) and index is not None and isinstance(index, int): + return self._headers[index] + else: + return self._headers + + def row_index( + self, + newindex=None, + index=None, + reset_row_positions=False, + show_index_if_not_sheet=True, + redraw=False, + ): + if newindex is not None: + if not self._row_index and not isinstance(self._row_index, int): + self.RI.set_width(self.default_index_width, set_TL=True) + if isinstance(newindex, (list, tuple)): + self._row_index = list(newindex) if isinstance(newindex, tuple) else newindex + elif isinstance(newindex, int): + self._row_index = int(newindex) + elif isinstance(index, int): + if index >= len(self._row_index): + self.RI.fix_index(index) + self._row_index[index] = f"{newindex}" + elif not isinstance(newindex, (list, tuple, int)) and index is None: + try: + self._row_index = list(newindex) + except Exception: + raise ValueError("New index must be iterable or int (use int to use a column as the index") + if reset_row_positions: + self.reset_row_positions() + elif ( + show_index_if_not_sheet + and isinstance(self._row_index, list) + and (self.row_positions == [0] or not self.row_positions) + ): + rowpos = self.default_row_height[1] + if self.all_rows_displayed: + self.row_positions = list(accumulate(chain([0], (rowpos for r in range(len(self._row_index)))))) + else: + self.row_positions = list(accumulate(chain([0], (rowpos for r in range(len(self.displayed_rows)))))) + + if redraw: + self.refresh() + else: + if not isinstance(self._row_index, int) and index is not None and isinstance(index, int): + return self._row_index[index] + else: + return self._row_index + + def total_data_cols(self, include_header=True): + h_total = 0 + d_total = 0 + if include_header: + if isinstance(self._headers, (list, tuple)): + h_total = len(self._headers) + try: + d_total = len(max(self.data, key=len)) + except Exception: + pass + return h_total if h_total > d_total else d_total + + def total_data_rows(self, include_index=True): + i_total = 0 + d_total = 0 + if include_index: + if isinstance(self._row_index, (list, tuple)): + i_total = len(self._row_index) + d_total = len(self.data) + return i_total if i_total > d_total else d_total + + def data_dimensions(self, total_rows=None, total_columns=None): + if total_rows is None and total_columns is None: + return self.total_data_rows(), self.total_data_cols() + if total_rows is not None: + if len(self.data) < total_rows: + ncols = self.total_data_cols() if total_columns is None else total_columns + self.data.extend([self.get_empty_row_seq(r, ncols) for r in range(total_rows - len(self.data))]) + else: + self.data[total_rows:] = [] + if total_columns is not None: + self.data[:] = [ + r[:total_columns] + if len(r) > total_columns + else r + self.get_empty_row_seq(rn, end=len(r) + total_columns - len(r), start=len(r)) + for rn, r in enumerate(self.data) + ] + + def equalize_data_row_lengths(self, include_header=False, total_columns=None): + total_columns = self.total_data_cols() if total_columns is None else total_columns + if include_header and total_columns > len(self._headers): + self.CH.fix_header(total_columns) + self.data[:] = [ + (r + self.get_empty_row_seq(rn, end=len(r) + total_columns - len(r), start=len(r))) + if total_columns > len(r) + else r + for rn, r in enumerate(self.data) + ] + return total_columns + + def get_canvas_visible_area(self): + return ( + self.canvasx(0), + self.canvasy(0), + self.canvasx(self.winfo_width()), + self.canvasy(self.winfo_height()), + ) + + def get_visible_rows(self, y1, y2): + start_row = bisect.bisect_left(self.row_positions, y1) + end_row = bisect.bisect_right(self.row_positions, y2) + if not y2 >= self.row_positions[-1]: + end_row += 1 + return start_row, end_row + + def get_visible_columns(self, x1, x2): + start_col = bisect.bisect_left(self.col_positions, x1) + end_col = bisect.bisect_right(self.col_positions, x2) + if not x2 >= self.col_positions[-1]: + end_col += 1 + return start_col, end_col + + def redraw_highlight_get_text_fg( + self, + r, + c, + fc, + fr, + sc, + sr, + c_2_, + c_3_, + c_4_, + selections, + datarn, + datacn, + can_width, + ): + redrawn = False + kwargs = self.get_cell_kwargs(datarn, datacn, key="highlight") + if kwargs: + if kwargs[0] is not None: + c_1 = kwargs[0] if kwargs[0].startswith("#") else Color_Map_[kwargs[0]] + if "cells" in selections and (r, c) in selections["cells"]: + tf = ( + self.table_selected_cells_fg + if kwargs[1] is None or self.display_selected_fg_over_highlights + else kwargs[1] + ) + if kwargs[0] is not None: + fill = ( + f"#{int((int(c_1[1:3], 16) + c_2_[0]) / 2):02X}" + + f"{int((int(c_1[3:5], 16) + c_2_[1]) / 2):02X}" + + f"{int((int(c_1[5:], 16) + c_2_[2]) / 2):02X}" + ) + elif "rows" in selections and r in selections["rows"]: + tf = ( + self.table_selected_rows_fg + if kwargs[1] is None or self.display_selected_fg_over_highlights + else kwargs[1] + ) + if kwargs[0] is not None: + fill = ( + f"#{int((int(c_1[1:3], 16) + c_4_[0]) / 2):02X}" + + f"{int((int(c_1[3:5], 16) + c_4_[1]) / 2):02X}" + + f"{int((int(c_1[5:], 16) + c_4_[2]) / 2):02X}" + ) + elif "columns" in selections and c in selections["columns"]: + tf = ( + self.table_selected_columns_fg + if kwargs[1] is None or self.display_selected_fg_over_highlights + else kwargs[1] + ) + if kwargs[0] is not None: + fill = ( + f"#{int((int(c_1[1:3], 16) + c_3_[0]) / 2):02X}" + + f"{int((int(c_1[3:5], 16) + c_3_[1]) / 2):02X}" + + f"{int((int(c_1[5:], 16) + c_3_[2]) / 2):02X}" + ) + else: + tf = self.table_fg if kwargs[1] is None else kwargs[1] + if kwargs[0] is not None: + fill = kwargs[0] + if kwargs[0] is not None: + redrawn = self.redraw_highlight( + fc + 1, + fr + 1, + sc, + sr, + fill=fill, + outline=self.table_fg + if self.get_cell_kwargs(datarn, datacn, key="dropdown") and self.show_dropdown_borders + else "", + tag="hi", + can_width=can_width if (len(kwargs) > 2 and kwargs[2]) else None, + ) + elif not kwargs: + if "cells" in selections and (r, c) in selections["cells"]: + tf = self.table_selected_cells_fg + elif "rows" in selections and r in selections["rows"]: + tf = self.table_selected_rows_fg + elif "columns" in selections and c in selections["columns"]: + tf = self.table_selected_columns_fg + else: + tf = self.table_fg + return tf, redrawn + + def redraw_highlight(self, x1, y1, x2, y2, fill, outline, tag, can_width=None, pc=None): + if type(pc) != int or pc >= 100 or pc <= 0: # noqa: E721 + coords = ( + x1 - 1 if outline else x1, + y1 - 1 if outline else y1, + x2 if can_width is None else x2 + can_width, + y2, + ) + else: + coords = (x1, y1, (x2 - x1) * (pc / 100), y2) + if self.hidd_high: + iid, showing = self.hidd_high.popitem() + self.coords(iid, coords) + if showing: + self.itemconfig(iid, fill=fill, outline=outline) + else: + self.itemconfig(iid, fill=fill, outline=outline, tag=tag, state="normal") + else: + iid = self.create_rectangle(coords, fill=fill, outline=outline, tag=tag) + self.disp_high[iid] = True + return True + + def redraw_gridline( + self, + points, + fill, + width, + tag, + ): + if self.hidd_grid: + t, sh = self.hidd_grid.popitem() + self.coords(t, points) + if sh: + self.itemconfig( + t, + fill=fill, + width=width, + capstyle=tk.BUTT, + joinstyle=tk.ROUND, + ) + else: + self.itemconfig( + t, + fill=fill, + width=width, + capstyle=tk.BUTT, + joinstyle=tk.ROUND, + state="normal", + ) + else: + t = self.create_line( + points, + fill=fill, + width=width, + capstyle=tk.BUTT, + joinstyle=tk.ROUND, + tag=tag, + ) + self.disp_grid[t] = True + + def redraw_dropdown( + self, + x1, + y1, + x2, + y2, + fill, + outline, + tag, + draw_outline=True, + draw_arrow=True, + dd_is_open=False, + ): + if draw_outline and self.show_dropdown_borders: + self.redraw_highlight(x1 + 1, y1 + 1, x2, y2, fill="", outline=self.table_fg, tag=tag) + if draw_arrow: + topysub = floor(self.half_txt_h / 2) + mid_y = y1 + floor(self.min_row_height / 2) + if mid_y + topysub + 1 >= y1 + self.txt_h - 1: + mid_y -= 1 + if mid_y - topysub + 2 <= y1 + 4 + topysub: + mid_y -= 1 + ty1 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 3 + ty2 = mid_y - topysub + 3 if dd_is_open else mid_y + topysub + 1 + ty3 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 3 + else: + ty1 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 2 + ty2 = mid_y - topysub + 2 if dd_is_open else mid_y + topysub + 1 + ty3 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 2 + tx1 = x2 - self.txt_h + 1 + tx2 = x2 - self.half_txt_h - 1 + tx3 = x2 - 3 + if tx2 - tx1 > tx3 - tx2: + tx1 += (tx2 - tx1) - (tx3 - tx2) + elif tx2 - tx1 < tx3 - tx2: + tx1 -= (tx3 - tx2) - (tx2 - tx1) + points = (tx1, ty1, tx2, ty2, tx3, ty3) + if self.hidd_dropdown: + t, sh = self.hidd_dropdown.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=fill) + else: + self.itemconfig(t, fill=fill, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_line( + points, + fill=fill, + width=2, + capstyle=tk.ROUND, + joinstyle=tk.ROUND, + tag=tag, + ) + self.disp_dropdown[t] = True + + def get_checkbox_points(self, x1, y1, x2, y2, radius=8): + return [ + x1 + radius, + y1, + x1 + radius, + y1, + x2 - radius, + y1, + x2 - radius, + y1, + x2, + y1, + x2, + y1 + radius, + x2, + y1 + radius, + x2, + y2 - radius, + x2, + y2 - radius, + x2, + y2, + x2 - radius, + y2, + x2 - radius, + y2, + x1 + radius, + y2, + x1 + radius, + y2, + x1, + y2, + x1, + y2 - radius, + x1, + y2 - radius, + x1, + y1 + radius, + x1, + y1 + radius, + x1, + y1, + ] + + def redraw_checkbox(self, x1, y1, x2, y2, fill, outline, tag, draw_check=False): + points = self.get_checkbox_points(x1, y1, x2, y2) + if self.hidd_checkbox: + t, sh = self.hidd_checkbox.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=outline, outline=fill) + else: + self.itemconfig(t, fill=outline, outline=fill, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_polygon(points, fill=outline, outline=fill, tag=tag, smooth=True) + self.disp_checkbox[t] = True + if draw_check: + x1 = x1 + 4 + y1 = y1 + 4 + x2 = x2 - 3 + y2 = y2 - 3 + points = self.get_checkbox_points(x1, y1, x2, y2, radius=4) + if self.hidd_checkbox: + t, sh = self.hidd_checkbox.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=fill, outline=outline) + else: + self.itemconfig(t, fill=fill, outline=outline, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_polygon(points, fill=fill, outline=outline, tag=tag, smooth=True) + self.disp_checkbox[t] = True + + def main_table_redraw_grid_and_text( + self, + redraw_header=False, + redraw_row_index=False, + redraw_table=True, + ): + try: + can_width = self.winfo_width() + can_height = self.winfo_height() + except Exception: + return False + row_pos_exists = self.row_positions != [0] and self.row_positions + col_pos_exists = self.col_positions != [0] and self.col_positions + resized_cols = False + resized_rows = False + if self.auto_resize_columns and self.allow_auto_resize_columns and col_pos_exists: + max_w = int(can_width) + max_w -= self.empty_horizontal + if (len(self.col_positions) - 1) * self.auto_resize_columns < max_w: + resized_cols = True + change = int((max_w - self.col_positions[-1]) / (len(self.col_positions) - 1)) + widths = [ + int(b - a) + change - 1 + for a, b in zip(self.col_positions, islice(self.col_positions, 1, len(self.col_positions))) + ] + diffs = {} + for i, w in enumerate(widths): + if w < self.auto_resize_columns: + diffs[i] = self.auto_resize_columns - w + widths[i] = self.auto_resize_columns + if diffs and len(diffs) < len(widths): + change = sum(diffs.values()) / (len(widths) - len(diffs)) + for i, w in enumerate(widths): + if i not in diffs: + widths[i] -= change + self.col_positions = list(accumulate(chain([0], widths))) + if self.auto_resize_rows and self.allow_auto_resize_rows and row_pos_exists: + max_h = int(can_height) + max_h -= self.empty_vertical + if (len(self.row_positions) - 1) * self.auto_resize_rows < max_h: + resized_rows = True + change = int((max_h - self.row_positions[-1]) / (len(self.row_positions) - 1)) + heights = [ + int(b - a) + change - 1 + for a, b in zip(self.row_positions, islice(self.row_positions, 1, len(self.row_positions))) + ] + diffs = {} + for i, h in enumerate(heights): + if h < self.auto_resize_rows: + diffs[i] = self.auto_resize_rows - h + heights[i] = self.auto_resize_rows + if diffs and len(diffs) < len(heights): + change = sum(diffs.values()) / (len(heights) - len(diffs)) + for i, h in enumerate(heights): + if i not in diffs: + heights[i] -= change + self.row_positions = list(accumulate(chain([0], heights))) + if resized_cols or resized_rows: + self.recreate_all_selection_boxes() + last_col_line_pos = self.col_positions[-1] + 1 + last_row_line_pos = self.row_positions[-1] + 1 + if can_width >= last_col_line_pos + self.empty_horizontal and self.parentframe.xscroll_showing: + self.parentframe.xscroll.grid_forget() + self.parentframe.xscroll_showing = False + elif ( + can_width < last_col_line_pos + self.empty_horizontal + and not self.parentframe.xscroll_showing + and not self.parentframe.xscroll_disabled + and can_height > 40 + ): + self.parentframe.xscroll.grid(row=2, column=0, columnspan=2, sticky="nswe") + self.parentframe.xscroll_showing = True + if can_height >= last_row_line_pos + self.empty_vertical and self.parentframe.yscroll_showing: + self.parentframe.yscroll.grid_forget() + self.parentframe.yscroll_showing = False + elif ( + can_height < last_row_line_pos + self.empty_vertical + and not self.parentframe.yscroll_showing + and not self.parentframe.yscroll_disabled + and can_width > 40 + ): + self.parentframe.yscroll.grid(row=0, column=2, rowspan=3, sticky="nswe") + self.parentframe.yscroll_showing = True + self.configure( + scrollregion=( + 0, + 0, + last_col_line_pos + self.empty_horizontal + 2, + last_row_line_pos + self.empty_vertical + 2, + ) + ) + scrollpos_bot = self.canvasy(can_height) + end_row = bisect.bisect_right(self.row_positions, scrollpos_bot) + if not scrollpos_bot >= self.row_positions[-1]: + end_row += 1 + if redraw_row_index and self.show_index: + self.RI.auto_set_index_width(end_row - 1) + # return + scrollpos_left = self.canvasx(0) + scrollpos_top = self.canvasy(0) + scrollpos_right = self.canvasx(can_width) + start_row = bisect.bisect_left(self.row_positions, scrollpos_top) + start_col = bisect.bisect_left(self.col_positions, scrollpos_left) + end_col = bisect.bisect_right(self.col_positions, scrollpos_right) + self.row_width_resize_bbox = ( + scrollpos_left, + scrollpos_top, + scrollpos_left + 2, + scrollpos_bot, + ) + self.header_height_resize_bbox = ( + scrollpos_left + 6, + scrollpos_top, + scrollpos_right, + scrollpos_top + 2, + ) + self.hidd_text.update(self.disp_text) + self.disp_text = {} + self.hidd_high.update(self.disp_high) + self.disp_high = {} + self.hidd_grid.update(self.disp_grid) + self.disp_grid = {} + self.hidd_dropdown.update(self.disp_dropdown) + self.disp_dropdown = {} + self.hidd_checkbox.update(self.disp_checkbox) + self.disp_checkbox = {} + if not scrollpos_right >= self.col_positions[-1]: + end_col += 1 + if last_col_line_pos > scrollpos_right: + x_stop = scrollpos_right + else: + x_stop = last_col_line_pos + if last_row_line_pos > scrollpos_bot: + y_stop = scrollpos_bot + else: + y_stop = last_row_line_pos + if self.show_horizontal_grid and row_pos_exists: + if self.horizontal_grid_to_end_of_window: + x_grid_stop = scrollpos_right + can_width + else: + if last_col_line_pos > scrollpos_right: + x_grid_stop = x_stop + 1 + else: + x_grid_stop = x_stop - 1 + points = list( + chain.from_iterable( + [ + ( + self.canvasx(0) - 1, + self.row_positions[r], + x_grid_stop, + self.row_positions[r], + self.canvasx(0) - 1, + self.row_positions[r], + self.canvasx(0) - 1, + self.row_positions[r + 1] if len(self.row_positions) - 1 > r else self.row_positions[r], + ) + for r in range(start_row - 1, end_row) + ] + ) + ) + if points: + self.redraw_gridline( + points=points, + fill=self.table_grid_fg, + width=1, + tag="g", + ) + if self.show_vertical_grid and col_pos_exists: + if self.vertical_grid_to_end_of_window: + y_grid_stop = scrollpos_bot + can_height + else: + if last_row_line_pos > scrollpos_bot: + y_grid_stop = y_stop + 1 + else: + y_grid_stop = y_stop - 1 + points = list( + chain.from_iterable( + [ + ( + self.col_positions[c], + scrollpos_top - 1, + self.col_positions[c], + y_grid_stop, + self.col_positions[c], + scrollpos_top - 1, + self.col_positions[c + 1] if len(self.col_positions) - 1 > c else self.col_positions[c], + scrollpos_top - 1, + ) + for c in range(start_col - 1, end_col) + ] + ) + ) + if points: + self.redraw_gridline( + points=points, + fill=self.table_grid_fg, + width=1, + tag="g", + ) + if start_row > 0: + start_row -= 1 + if start_col > 0: + start_col -= 1 + end_row -= 1 + selections = self.get_redraw_selections(start_row, end_row, start_col, end_col) + c_2 = ( + self.table_selected_cells_bg + if self.table_selected_cells_bg.startswith("#") + else Color_Map_[self.table_selected_cells_bg] + ) + c_2_ = (int(c_2[1:3], 16), int(c_2[3:5], 16), int(c_2[5:], 16)) + c_3 = ( + self.table_selected_columns_bg + if self.table_selected_columns_bg.startswith("#") + else Color_Map_[self.table_selected_columns_bg] + ) + c_3_ = (int(c_3[1:3], 16), int(c_3[3:5], 16), int(c_3[5:], 16)) + c_4 = ( + self.table_selected_rows_bg + if self.table_selected_rows_bg.startswith("#") + else Color_Map_[self.table_selected_rows_bg] + ) + c_4_ = (int(c_4[1:3], 16), int(c_4[3:5], 16), int(c_4[5:], 16)) + rows_ = tuple(range(start_row, end_row)) + font = self.table_font + if redraw_table: + for c in range(start_col, end_col - 1): + for r in rows_: + rtopgridln = self.row_positions[r] + rbotgridln = self.row_positions[r + 1] + if rbotgridln - rtopgridln < self.txt_h: + continue + cleftgridln = self.col_positions[c] + crightgridln = self.col_positions[c + 1] + + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + + fill, dd_drawn = self.redraw_highlight_get_text_fg( + r, + c, + cleftgridln, + rtopgridln, + crightgridln, + rbotgridln, + c_2_, + c_3_, + c_4_, + selections, + datarn, + datacn, + can_width, + ) + align = self.get_cell_kwargs(datarn, datacn, key="align") + if align: + align = align + else: + align = self.align + kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") + if align == "w": + draw_x = cleftgridln + 3 + if kwargs: + mw = crightgridln - cleftgridln - self.txt_h - 2 + self.redraw_dropdown( + cleftgridln, + rtopgridln, + crightgridln, + self.row_positions[r + 1], + fill=fill, + outline=fill, + tag=f"dd_{r}_{c}", + draw_outline=not dd_drawn, + draw_arrow=mw >= 5, + dd_is_open=kwargs["window"] != "no dropdown open", + ) + else: + mw = crightgridln - cleftgridln - 1 + elif align == "e": + if kwargs: + mw = crightgridln - cleftgridln - self.txt_h - 2 + draw_x = crightgridln - 5 - self.txt_h + self.redraw_dropdown( + cleftgridln, + rtopgridln, + crightgridln, + self.row_positions[r + 1], + fill=fill, + outline=fill, + tag=f"dd_{r}_{c}", + draw_outline=not dd_drawn, + draw_arrow=mw >= 5, + dd_is_open=kwargs["window"] != "no dropdown open", + ) + else: + mw = crightgridln - cleftgridln - 1 + draw_x = crightgridln - 3 + elif align == "center": + stop = cleftgridln + 5 + if kwargs: + mw = crightgridln - cleftgridln - self.txt_h - 2 + draw_x = cleftgridln + ceil((crightgridln - cleftgridln - self.txt_h) / 2) + self.redraw_dropdown( + cleftgridln, + rtopgridln, + crightgridln, + self.row_positions[r + 1], + fill=fill, + outline=fill, + tag=f"dd_{r}_{c}", + draw_outline=not dd_drawn, + draw_arrow=mw >= 5, + dd_is_open=kwargs["window"] != "no dropdown open", + ) + else: + mw = crightgridln - cleftgridln - 1 + draw_x = cleftgridln + floor((crightgridln - cleftgridln) / 2) + if not kwargs: + kwargs = self.get_cell_kwargs(datarn, datacn, key="checkbox") + if kwargs and mw > self.txt_h + 2: + box_w = self.txt_h + 1 + mw -= box_w + if align == "w": + draw_x += box_w + 1 + elif align == "center": + draw_x += ceil(box_w / 2) + 1 + mw -= 1 + else: + mw -= 3 + try: + draw_check = self.data[datarn][datacn] + except Exception: + draw_check = False + self.redraw_checkbox( + cleftgridln + 2, + rtopgridln + 2, + cleftgridln + self.txt_h + 3, + rtopgridln + self.txt_h + 3, + fill=fill if kwargs["state"] == "normal" else self.table_grid_fg, + outline="", + tag="cb", + draw_check=draw_check, + ) + lns = self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True).split("\n") + if ( + lns != [""] + and mw > self.txt_w + and not ( + (align == "w" and draw_x > scrollpos_right) + or (align == "e" and cleftgridln + 5 > scrollpos_right) + or (align == "center" and stop > scrollpos_right) + ) + ): + draw_y = rtopgridln + self.fl_ins + start_ln = int((scrollpos_top - rtopgridln) / self.xtra_lines_increment) + if start_ln < 0: + start_ln = 0 + draw_y += start_ln * self.xtra_lines_increment + if draw_y + self.half_txt_h - 1 <= rbotgridln and len(lns) > start_ln: + for txt in islice(lns, start_ln, None): + if self.hidd_text: + iid, showing = self.hidd_text.popitem() + self.coords(iid, draw_x, draw_y) + if showing: + self.itemconfig( + iid, + text=txt, + fill=fill, + font=font, + anchor=align, + ) + else: + self.itemconfig( + iid, + text=txt, + fill=fill, + font=font, + anchor=align, + state="normal", + ) + self.tag_raise(iid) + else: + iid = self.create_text( + draw_x, + draw_y, + text=txt, + fill=fill, + font=font, + anchor=align, + tag="t", + ) + self.disp_text[iid] = True + wd = self.bbox(iid) + wd = wd[2] - wd[0] + if wd > mw: + if align == "w": + txt = txt[: int(len(txt) * (mw / wd))] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + while wd[2] - wd[0] > mw: + txt = txt[:-1] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + elif align == "e": + txt = txt[len(txt) - int(len(txt) * (mw / wd)) :] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + while wd[2] - wd[0] > mw: + txt = txt[1:] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + elif align == "center": + self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) + tmod = ceil((len(txt) - int(len(txt) * (mw / wd))) / 2) + txt = txt[tmod - 1 : -tmod] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + while wd[2] - wd[0] > mw: + txt = txt[next(self.c_align_cyc)] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + self.coords(iid, draw_x, draw_y) + draw_y += self.xtra_lines_increment + if draw_y + self.half_txt_h - 1 > rbotgridln: + break + if redraw_table: + for dct in (self.hidd_text, self.hidd_high, self.hidd_grid, self.hidd_dropdown, self.hidd_checkbox): + for iid, showing in dct.items(): + if showing: + self.itemconfig(iid, state="hidden") + dct[iid] = False + if self.show_selected_cells_border: + self.tag_raise("cellsbd") + self.tag_raise("selected") + self.tag_raise("rowsbd") + self.tag_raise("columnsbd") + if redraw_header and self.show_header: + self.CH.redraw_grid_and_text( + last_col_line_pos, + scrollpos_left, + x_stop, + start_col, + end_col, + scrollpos_right, + col_pos_exists, + ) + if redraw_row_index and self.show_index: + self.RI.redraw_grid_and_text( + last_row_line_pos, + scrollpos_top, + y_stop, + start_row, + end_row + 1, + scrollpos_bot, + row_pos_exists, + ) + self.parentframe.emit_event("<>") + return True + + def get_all_selection_items(self): + return sorted( + self.find_withtag("cells") + + self.find_withtag("rows") + + self.find_withtag("columns") + + self.find_withtag("selected") + ) + + def get_boxes(self, include_current=True): + boxes = {} + for item in self.get_all_selection_items(): + alltags = self.gettags(item) + if alltags[0] == "cells": + boxes[tuple(int(e) for e in alltags[1].split("_") if e)] = "cells" + elif alltags[0] == "rows": + boxes[tuple(int(e) for e in alltags[1].split("_") if e)] = "rows" + elif alltags[0] == "columns": + boxes[tuple(int(e) for e in alltags[1].split("_") if e)] = "columns" + elif include_current and alltags[0] == "selected": + boxes[tuple(int(e) for e in alltags[1].split("_") if e)] = f"{alltags[2]}" + return boxes + + def reselect_from_get_boxes(self, boxes): + for (r1, c1, r2, c2), v in boxes.items(): + if r2 < len(self.row_positions) and c2 < len(self.col_positions): + if v == "cells": + self.create_selected(r1, c1, r2, c2, "cells") + elif v == "rows": + self.create_selected(r1, c1, r2, c2, "rows") + elif v == "columns": + self.create_selected(r1, c1, r2, c2, "columns") + elif v in ("cell", "row", "column"): # currently selected + self.set_currently_selected(r1, c1, type_=v) + + def delete_selected(self, r1=None, c1=None, r2=None, c2=None, type_=None): + deleted_boxes = {} + tags_to_del = set() + box1 = (r1, c1, r2, c2) + for s in self.get_selection_tags_from_type(type_): + for item in self.find_withtag(s): + alltags = self.gettags(item) + if alltags: + box2 = tuple(int(e) for e in alltags[1].split("_") if e) + if box1 == box2: + tags_to_del.add(alltags) + deleted_boxes[box2] = ( + "cells" + if alltags[0].startswith("cell") + else "rows" + if alltags[0].startswith("row") + else "columns" + ) + self.delete(item) + for canvas in (self.RI, self.CH): + for item in canvas.find_withtag(s): + if canvas.gettags(item) in tags_to_del: + canvas.delete(item) + + def get_selection_tags_from_type(self, type_): + if type_ == "cells": + return {"cells", "cellsbd"} + if type_ == "rows": + return {"rows", "rowsbd"} + elif type_ == "columns": + return {"columns", "columnsbd"} + else: + return {"cells", "cellsbd", "rows", "rowsbd", "columns", "columnsbd"} + + def delete_selection_rects(self, cells=True, rows=True, cols=True, delete_current=True): + deleted_boxes = {} + if cells: + for item in self.find_withtag("cells"): + alltags = self.gettags(item) + if alltags: + deleted_boxes[tuple(int(e) for e in alltags[1].split("_") if e)] = "cells" + self.delete("cells", "cellsbd") + self.RI.delete("cells", "cellsbd") + self.CH.delete("cells", "cellsbd") + if rows: + for item in self.find_withtag("rows"): + alltags = self.gettags(item) + if alltags: + deleted_boxes[tuple(int(e) for e in alltags[1].split("_") if e)] = "rows" + self.delete("rows", "rowsbd") + self.RI.delete("rows", "rowsbd") + self.CH.delete("rows", "rowsbd") + if cols: + for item in self.find_withtag("columns"): + alltags = self.gettags(item) + if alltags: + deleted_boxes[tuple(int(e) for e in alltags[1].split("_") if e)] = "columns" + self.delete("columns", "columnsbd") + self.RI.delete("columns", "columnsbd") + self.CH.delete("columns", "columnsbd") + if delete_current: + self.delete("selected") + self.RI.delete("selected") + self.CH.delete("selected") + return deleted_boxes + + def currently_selected(self): + items = self.find_withtag("selected") + if not items: + return tuple() + alltags = self.gettags(items[0]) + box = tuple(int(e) for e in alltags[1].split("_") if e) + return CurrentlySelectedClass(box[0], box[1], alltags[2]) + + def get_tags_of_current(self): + items = self.find_withtag("selected") + if items: + return self.gettags(items[0]) + else: + return tuple() + + def set_currently_selected(self, r, c, type_="cell"): # cell, column or row + r1, c1, r2, c2 = r, c, r + 1, c + 1 + self.delete("selected") + self.RI.delete("selected") + self.CH.delete("selected") + if self.col_positions == [0]: + c1 = 0 + c2 = 0 + if self.row_positions == [0]: + r1 = 0 + r2 = 0 + tagr = ("selected", f"{r1}_{c1}_{r2}_{c2}", type_) + tag_index_header = ("cells", f"{r1}_{c1}_{r2}_{c2}", "selected") + if type_ == "cell": + outline = self.table_selected_cells_border_fg + elif type_ == "row": + outline = self.table_selected_rows_border_fg + elif type_ == "column": + outline = self.table_selected_columns_border_fg + if self.show_selected_cells_border: + b = self.create_rectangle( + self.col_positions[c1] + 1, + self.row_positions[r1] + 1, + self.col_positions[c2], + self.row_positions[r2], + fill="", + outline=outline, + width=2, + tags=tagr, + ) + self.tag_raise(b) + else: + b = self.create_rectangle( + self.col_positions[c1], + self.row_positions[r1], + self.col_positions[c2], + self.row_positions[r2], + fill=self.table_selected_cells_bg, + outline="", + tags=tagr, + ) + self.tag_lower(b) + ri = self.RI.create_rectangle( + 0, + self.row_positions[r1], + self.RI.current_width - 1, + self.row_positions[r2], + fill=self.RI.index_selected_cells_bg, + outline="", + tags=tag_index_header, + ) + ch = self.CH.create_rectangle( + self.col_positions[c1], + 0, + self.col_positions[c2], + self.CH.current_height - 1, + fill=self.CH.header_selected_cells_bg, + outline="", + tags=tag_index_header, + ) + self.RI.tag_lower(ri) + self.CH.tag_lower(ch) + return b + + def set_current_to_last(self): + if not self.currently_selected(): + items = sorted(self.find_withtag("cells") + self.find_withtag("rows") + self.find_withtag("columns")) + if items: + last = self.gettags(items[-1]) + r1, c1, r2, c2 = tuple(int(e) for e in last[1].split("_") if e) + if last[0] == "cells": + return self.gettags(self.set_currently_selected(r1, c1, "cell")) + elif last[0] == "rows": + return self.gettags(self.set_currently_selected(r1, c1, "row")) + elif last[0] == "columns": + return self.gettags(self.set_currently_selected(r1, c1, "column")) + return tuple() + + def delete_current(self): + self.delete("selected") + self.RI.delete("selected") + self.CH.delete("selected") + + def create_selected( + self, + r1=None, + c1=None, + r2=None, + c2=None, + type_="cells", + taglower=True, + state="normal", + ): + self.itemconfig("cells", state="normal") + coords = f"{r1}_{c1}_{r2}_{c2}" + if type_ == "cells": + tagr = ("cells", coords) + tagb = ("cellsbd", coords) + mt_bg = self.table_selected_cells_bg + mt_border_col = self.table_selected_cells_border_fg + elif type_ == "rows": + tagr = ("rows", coords) + tagb = ("rowsbd", coords) + tag_index_header = ("cells", coords) + mt_bg = self.table_selected_rows_bg + mt_border_col = self.table_selected_rows_border_fg + elif type_ == "columns": + tagr = ("columns", coords) + tagb = ("columnsbd", coords) + tag_index_header = ("cells", coords) + mt_bg = self.table_selected_columns_bg + mt_border_col = self.table_selected_columns_border_fg + self.last_selected = (r1, c1, r2, c2, type_) + ch_tags = tag_index_header if type_ == "rows" else tagr + ri_tags = tag_index_header if type_ == "columns" else tagr + r = self.create_rectangle( + self.col_positions[c1], + self.row_positions[r1], + self.canvasx(self.winfo_width()) if self.selected_rows_to_end_of_window else self.col_positions[c2], + self.row_positions[r2], + fill=mt_bg, + outline="", + state=state, + tags=tagr, + ) + self.RI.create_rectangle( + 0, + self.row_positions[r1], + self.RI.current_width - 1, + self.row_positions[r2], + fill=self.RI.index_selected_rows_bg if type_ == "rows" else self.RI.index_selected_cells_bg, + outline="", + tags=ri_tags, + ) + self.CH.create_rectangle( + self.col_positions[c1], + 0, + self.col_positions[c2], + self.CH.current_height - 1, + fill=self.CH.header_selected_columns_bg if type_ == "columns" else self.CH.header_selected_cells_bg, + outline="", + tags=ch_tags, + ) + if self.show_selected_cells_border and ( + (self.being_drawn_rect is None and self.RI.being_drawn_rect is None and self.CH.being_drawn_rect is None) + or len(self.anything_selected()) > 1 + ): + b = self.create_rectangle( + self.col_positions[c1], + self.row_positions[r1], + self.col_positions[c2], + self.row_positions[r2], + fill="", + outline=mt_border_col, + tags=tagb, + ) + else: + b = None + if taglower: + self.tag_lower("rows") + self.RI.tag_lower("rows") + self.tag_lower("columns") + self.CH.tag_lower("columns") + self.tag_lower("cells") + self.RI.tag_lower("cells") + self.CH.tag_lower("cells") + return r, b + + def recreate_all_selection_boxes(self): + curr = self.currently_selected() + for item in chain( + self.find_withtag("cells"), + self.find_withtag("rows"), + self.find_withtag("columns"), + ): + tags = self.gettags(item) + if tags: + r1, c1, r2, c2 = tuple(int(e) for e in tags[1].split("_") if e) + state = self.itemcget(item, "state") + self.delete(f"{r1}_{c1}_{r2}_{c2}") + self.RI.delete(f"{r1}_{c1}_{r2}_{c2}") + self.CH.delete(f"{r1}_{c1}_{r2}_{c2}") + if r1 >= len(self.row_positions) - 1 or c1 >= len(self.col_positions) - 1: + continue + if r2 > len(self.row_positions) - 1: + r2 = len(self.row_positions) - 1 + if c2 > len(self.col_positions) - 1: + c2 = len(self.col_positions) - 1 + self.create_selected(r1, c1, r2, c2, tags[0], state=state) + if curr: + self.set_currently_selected(curr.row, curr.column, curr.type_) + self.tag_lower("rows") + self.RI.tag_lower("rows") + self.tag_lower("columns") + self.CH.tag_lower("columns") + self.tag_lower("cells") + self.RI.tag_lower("cells") + self.CH.tag_lower("cells") + if not self.show_selected_cells_border: + self.tag_lower("selected") + + def get_redraw_selections(self, startr, endr, startc, endc): + d = defaultdict(list) + for item in chain( + self.find_withtag("cells"), + self.find_withtag("rows"), + self.find_withtag("columns"), + ): + tags = self.gettags(item) + d[tags[0]].append(tuple(int(e) for e in tags[1].split("_") if e)) + d2 = {} + if "cells" in d: + d2["cells"] = { + (r, c) + for r in range(startr, endr) + for c in range(startc, endc) + for r1, c1, r2, c2 in d["cells"] + if r1 <= r and c1 <= c and r2 > r and c2 > c + } + if "rows" in d: + d2["rows"] = {r for r in range(startr, endr) for r1, c1, r2, c2 in d["rows"] if r1 <= r and r2 > r} + if "columns" in d: + d2["columns"] = {c for c in range(startc, endc) for r1, c1, r2, c2 in d["columns"] if c1 <= c and c2 > c} + return d2 + + def get_selected_min_max(self): + min_x = float("inf") + min_y = float("inf") + max_x = 0 + max_y = 0 + for item in chain( + self.find_withtag("cells"), + self.find_withtag("rows"), + self.find_withtag("columns"), + self.find_withtag("selected"), + ): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + if r1 < min_y: + min_y = r1 + if c1 < min_x: + min_x = c1 + if r2 > max_y: + max_y = r2 + if c2 > max_x: + max_x = c2 + if min_x != float("inf") and min_y != float("inf") and max_x > 0 and max_y > 0: + return min_y, min_x, max_y, max_x + else: + return None, None, None, None + + def get_selected_rows(self, get_cells=False, within_range=None, get_cells_as_rows=False): + s = set() + if within_range is not None: + within_r1 = within_range[0] + within_r2 = within_range[1] + if get_cells: + if within_range is None: + for item in self.find_withtag("rows"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + s.update(set(product(range(r1, r2), range(0, len(self.col_positions) - 1)))) + if get_cells_as_rows: + s.update(self.get_selected_cells()) + else: + for item in self.find_withtag("rows"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + if r1 >= within_r1 or r2 <= within_r2: + if r1 > within_r1: + start_row = r1 + else: + start_row = within_r1 + if r2 < within_r2: + end_row = r2 + else: + end_row = within_r2 + s.update( + set( + product( + range(start_row, end_row), + range(0, len(self.col_positions) - 1), + ) + ) + ) + if get_cells_as_rows: + s.update( + self.get_selected_cells( + within_range=( + within_r1, + 0, + within_r2, + len(self.col_positions) - 1, + ) + ) + ) + else: + if within_range is None: + for item in self.find_withtag("rows"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + s.update(set(range(r1, r2))) + if get_cells_as_rows: + s.update(set(tup[0] for tup in self.get_selected_cells())) + else: + for item in self.find_withtag("rows"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + if r1 >= within_r1 or r2 <= within_r2: + if r1 > within_r1: + start_row = r1 + else: + start_row = within_r1 + if r2 < within_r2: + end_row = r2 + else: + end_row = within_r2 + s.update(set(range(start_row, end_row))) + if get_cells_as_rows: + s.update( + set( + tup[0] + for tup in self.get_selected_cells( + within_range=( + within_r1, + 0, + within_r2, + len(self.col_positions) - 1, + ) + ) + ) + ) + return s + + def get_selected_cols(self, get_cells=False, within_range=None, get_cells_as_cols=False): + s = set() + if within_range is not None: + within_c1 = within_range[0] + within_c2 = within_range[1] + if get_cells: + if within_range is None: + for item in self.find_withtag("columns"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + s.update(set(product(range(c1, c2), range(0, len(self.row_positions) - 1)))) + if get_cells_as_cols: + s.update(self.get_selected_cells()) + else: + for item in self.find_withtag("columns"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + if c1 >= within_c1 or c2 <= within_c2: + if c1 > within_c1: + start_col = c1 + else: + start_col = within_c1 + if c2 < within_c2: + end_col = c2 + else: + end_col = within_c2 + s.update( + set( + product( + range(start_col, end_col), + range(0, len(self.row_positions) - 1), + ) + ) + ) + if get_cells_as_cols: + s.update( + self.get_selected_cells( + within_range=( + 0, + within_c1, + len(self.row_positions) - 1, + within_c2, + ) + ) + ) + else: + if within_range is None: + for item in self.find_withtag("columns"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + s.update(set(range(c1, c2))) + if get_cells_as_cols: + s.update(set(tup[1] for tup in self.get_selected_cells())) + else: + for item in self.find_withtag("columns"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + if c1 >= within_c1 or c2 <= within_c2: + if c1 > within_c1: + start_col = c1 + else: + start_col = within_c1 + if c2 < within_c2: + end_col = c2 + else: + end_col = within_c2 + s.update(set(range(start_col, end_col))) + if get_cells_as_cols: + s.update( + set( + tup[0] + for tup in self.get_selected_cells( + within_range=( + 0, + within_c1, + len(self.row_positions) - 1, + within_c2, + ) + ) + ) + ) + return s + + def get_selected_cells(self, get_rows=False, get_cols=False, within_range=None): + s = set() + if within_range is not None: + within_r1 = within_range[0] + within_c1 = within_range[1] + within_r2 = within_range[2] + within_c2 = within_range[3] + if get_cols and get_rows: + iterable = chain( + self.find_withtag("cells"), + self.find_withtag("rows"), + self.find_withtag("columns"), + ) + elif get_rows and not get_cols: + iterable = chain(self.find_withtag("cells"), self.find_withtag("rows")) + elif get_cols and not get_rows: + iterable = chain(self.find_withtag("cells"), self.find_withtag("columns")) + else: + iterable = chain(self.find_withtag("cells")) + if within_range is None: + for item in iterable: + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + s.update(set(product(range(r1, r2), range(c1, c2)))) + else: + for item in iterable: + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + if r1 >= within_r1 or c1 >= within_c1 or r2 <= within_r2 or c2 <= within_c2: + if r1 > within_r1: + start_row = r1 + else: + start_row = within_r1 + if c1 > within_c1: + start_col = c1 + else: + start_col = within_c1 + if r2 < within_r2: + end_row = r2 + else: + end_row = within_r2 + if c2 < within_c2: + end_col = c2 + else: + end_col = within_c2 + s.update(set(product(range(start_row, end_row), range(start_col, end_col)))) + return s + + def get_all_selection_boxes(self): + return tuple( + tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + for item in chain( + self.find_withtag("cells"), + self.find_withtag("rows"), + self.find_withtag("columns"), + ) + ) + + def get_all_selection_boxes_with_types(self): + boxes = [] + for item in sorted(self.find_withtag("cells") + self.find_withtag("rows") + self.find_withtag("columns")): + tags = self.gettags(item) + boxes.append((tuple(int(e) for e in tags[1].split("_") if e), tags[0])) + return boxes + + def all_selected(self): + for r1, c1, r2, c2 in self.get_all_selection_boxes(): + if not r1 and not c1 and r2 == len(self.row_positions) - 1 and c2 == len(self.col_positions) - 1: + return True + return False + + # don't have to use "selected" because you can't have a current without a selection box + def cell_selected(self, r, c, inc_cols=False, inc_rows=False): + if not isinstance(r, int) or not isinstance(c, int): + return False + if not inc_cols and not inc_rows: + iterable = self.find_withtag("cells") + elif inc_cols and not inc_rows: + iterable = chain(self.find_withtag("columns"), self.find_withtag("cells")) + elif not inc_cols and inc_rows: + iterable = chain(self.find_withtag("rows"), self.find_withtag("cells")) + elif inc_cols and inc_rows: + iterable = chain( + self.find_withtag("rows"), + self.find_withtag("columns"), + self.find_withtag("cells"), + ) + for item in iterable: + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + if r1 <= r and c1 <= c and r2 > r and c2 > c: + return True + return False + + def col_selected(self, c): + if not isinstance(c, int): + return False + for item in self.find_withtag("columns"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + if c1 <= c and c2 > c: + return True + return False + + def row_selected(self, r): + if not isinstance(r, int): + return False + for item in self.find_withtag("rows"): + r1, c1, r2, c2 = tuple(int(e) for e in self.gettags(item)[1].split("_") if e) + if r1 <= r and r2 > r: + return True + return False + + def anything_selected(self, exclude_columns=False, exclude_rows=False, exclude_cells=False): + if exclude_columns and exclude_rows and not exclude_cells: + return self.find_withtag("cells") + elif exclude_columns and exclude_cells and not exclude_rows: + return self.find_withtag("rows") + elif exclude_rows and exclude_cells and not exclude_columns: + return self.find_withtag("columns") + + elif exclude_columns and not exclude_rows and not exclude_cells: + return self.find_withtag("cells") + self.find_withtag("rows") + elif exclude_rows and not exclude_columns and not exclude_cells: + return self.find_withtag("cells") + self.find_withtag("columns") + + elif exclude_cells and not exclude_columns and not exclude_rows: + return self.find_withtag("rows") + self.find_withtag("columns") + + elif not exclude_columns and not exclude_rows and not exclude_cells: + return self.find_withtag("cells") + self.find_withtag("rows") + self.find_withtag("columns") + return tuple() + + def hide_current(self): + for item in self.find_withtag("selected"): + self.itemconfig(item, state="hidden") + + def show_current(self): + for item in self.find_withtag("selected"): + self.itemconfig(item, state="normal") + + def open_cell(self, event=None, ignore_existing_editor=False): + if not self.anything_selected() or (not ignore_existing_editor and self.text_editor_id is not None): + return + currently_selected = self.currently_selected() + if not currently_selected: + return + r, c = int(currently_selected[0]), int(currently_selected[1]) + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + if self.get_cell_kwargs(datarn, datacn, key="readonly"): + return + elif self.get_cell_kwargs(datarn, datacn, key="dropdown") or self.get_cell_kwargs( + datarn, datacn, key="checkbox" + ): + if self.event_opens_dropdown_or_checkbox(event): + if self.get_cell_kwargs(datarn, datacn, key="dropdown"): + self.open_dropdown_window(r, c, event=event) + elif self.get_cell_kwargs(datarn, datacn, key="checkbox"): + self.click_checkbox(r=r, c=c, datarn=datarn, datacn=datacn) + else: + self.open_text_editor(event=event, r=r, c=c, dropdown=False) + + def event_opens_dropdown_or_checkbox(self, event=None): + if event is None: + return False + elif event == "rc": + return True + elif ( + (hasattr(event, "keysym") and event.keysym == "Return") + or (hasattr(event, "keysym") and event.keysym == "F2") # enter or f2 + or ( + event is not None + and hasattr(event, "keycode") + and event.keycode == "??" + and hasattr(event, "num") + and event.num == 1 + ) + or (hasattr(event, "keysym") and event.keysym == "BackSpace") + ): + return True + else: + return False + + # displayed indexes + def get_cell_align(self, r, c): + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + cell_alignment = self.get_cell_kwargs(datarn, datacn, key="align") + if cell_alignment: + return cell_alignment + return self.align + + # displayed indexes + def open_text_editor( + self, + event=None, + r=0, + c=0, + text=None, + state="normal", + see=True, + set_data_on_close=True, + binding=None, + dropdown=False, + ): + text = None + extra_func_key = "??" + if event is None or self.event_opens_dropdown_or_checkbox(event): + if event is not None: + if hasattr(event, "keysym") and event.keysym == "Return": + extra_func_key = "Return" + elif hasattr(event, "keysym") and event.keysym == "F2": + extra_func_key = "F2" + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + if event is not None and (hasattr(event, "keysym") and event.keysym == "BackSpace"): + extra_func_key = "BackSpace" + text = "" + else: + text = f"{self.get_cell_data(datarn, datacn, none_to_empty_str = True)}" + elif event is not None and ( + (hasattr(event, "char") and event.char.isalpha()) + or (hasattr(event, "char") and event.char.isdigit()) + or (hasattr(event, "char") and event.char in symbols_set) + ): + extra_func_key = event.char + text = event.char + else: + return False + self.text_editor_loc = (r, c) + if self.extra_begin_edit_cell_func is not None: + try: + text = self.extra_begin_edit_cell_func(EditCellEvent(r, c, extra_func_key, text, "begin_edit_cell")) + except Exception: + return False + if text is None: + return False + else: + text = text if isinstance(text, str) else f"{text}" + text = "" if text is None else text + if self.cell_auto_resize_enabled: + self.set_cell_size_to_text(r, c, only_set_if_too_small=True, redraw=True, run_binding=True) + + if (r, c) == self.text_editor_loc and self.text_editor is not None: + self.text_editor.set_text(self.text_editor.get() + "" if not isinstance(text, str) else text) + return + if self.text_editor is not None: + self.destroy_text_editor() + if see: + has_redrawn = self.see(r=r, c=c, check_cell_visibility=True) + if not has_redrawn: + self.refresh() + self.text_editor_loc = (r, c) + x = self.col_positions[c] + y = self.row_positions[r] + w = self.col_positions[c + 1] - x + 1 + h = self.row_positions[r + 1] - y + 1 + if text is None: + text = f"""{self.get_cell_data(r if self.all_rows_displayed else self.displayed_rows[r], + c if self.all_columns_displayed else self.displayed_columns[c], + none_to_empty_str = True)}""" + self.hide_current() + bg, fg = self.table_bg, self.table_fg + self.text_editor = TextEditor( + self, + text=text, + font=self.table_font, + state=state, + width=w, + height=h, + border_color=self.table_selected_cells_border_fg, + show_border=self.show_selected_cells_border, + bg=bg, + fg=fg, + popup_menu_font=self.popup_menu_font, + popup_menu_fg=self.popup_menu_fg, + popup_menu_bg=self.popup_menu_bg, + popup_menu_highlight_bg=self.popup_menu_highlight_bg, + popup_menu_highlight_fg=self.popup_menu_highlight_fg, + binding=binding, + align=self.get_cell_align(r, c), + r=r, + c=c, + newline_binding=self.text_editor_newline_binding, + ) + self.text_editor.update_idletasks() + self.text_editor_id = self.create_window((x, y), window=self.text_editor, anchor="nw") + if not dropdown: + self.text_editor.textedit.focus_set() + self.text_editor.scroll_to_bottom() + self.text_editor.textedit.bind("", lambda x: self.text_editor_newline_binding(r, c)) + if USER_OS == "darwin": + self.text_editor.textedit.bind("", lambda x: self.text_editor_newline_binding(r, c)) + for key, func in self.text_editor_user_bound_keys.items(): + self.text_editor.textedit.bind(key, func) + if binding is not None: + self.text_editor.textedit.bind("", lambda x: binding((r, c, "Tab"))) + self.text_editor.textedit.bind("", lambda x: binding((r, c, "Return"))) + self.text_editor.textedit.bind("", lambda x: binding((r, c, "FocusOut"))) + self.text_editor.textedit.bind("", lambda x: binding((r, c, "Escape"))) + elif binding is None and set_data_on_close: + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((r, c, "Tab"))) + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((r, c, "Return"))) + if not dropdown: + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((r, c, "FocusOut"))) + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((r, c, "Escape"))) + else: + self.text_editor.textedit.bind("", lambda x: self.destroy_text_editor("Escape")) + return True + + # displayed indexes + def text_editor_newline_binding(self, r=0, c=0, event=None, check_lines=True): + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + curr_height = self.text_editor.winfo_height() + if not check_lines or self.get_lines_cell_height(self.text_editor.get_num_lines() + 1) > curr_height: + new_height = curr_height + self.xtra_lines_increment + space_bot = self.get_space_bot(r) + if new_height > space_bot: + new_height = space_bot + if new_height != curr_height: + self.text_editor.config(height=new_height) + kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") + if kwargs: + text_editor_h = self.text_editor.winfo_height() + win_h, anchor = self.get_dropdown_height_anchor(r, c, text_editor_h) + if anchor == "nw": + self.coords( + kwargs["canvas_id"], + self.col_positions[c], + self.row_positions[r] + text_editor_h - 1, + ) + self.itemconfig(kwargs["canvas_id"], anchor=anchor, height=win_h) + elif anchor == "sw": + self.coords( + kwargs["canvas_id"], + self.col_positions[c], + self.row_positions[r], + ) + self.itemconfig(kwargs["canvas_id"], anchor=anchor, height=win_h) + + def refresh_open_window_positions(self): + if self.text_editor is not None: + r, c = self.text_editor_loc + self.text_editor.config(height=self.row_positions[r + 1] - self.row_positions[r]) + self.coords( + self.text_editor_id, + self.col_positions[c], + self.row_positions[r], + ) + if self.existing_dropdown_window is not None: + r, c = self.get_existing_dropdown_coords() + if self.text_editor is None: + text_editor_h = self.row_positions[r + 1] - self.row_positions[r] + anchor = self.itemcget(self.existing_dropdown_canvas_id, "anchor") + win_h = 0 + else: + text_editor_h = self.text_editor.winfo_height() + win_h, anchor = self.get_dropdown_height_anchor(r, c, text_editor_h) + if anchor == "nw": + self.coords( + self.existing_dropdown_canvas_id, + self.col_positions[c], + self.row_positions[r] + text_editor_h - 1, + ) + # self.itemconfig(self.existing_dropdown_canvas_id, anchor=anchor, height=win_h) + elif anchor == "sw": + self.coords( + self.existing_dropdown_canvas_id, + self.col_positions[c], + self.row_positions[r], + ) + # self.itemconfig(self.existing_dropdown_canvas_id, anchor=anchor, height=win_h) + + def destroy_text_editor(self, event=None): + if event is not None and self.extra_end_edit_cell_func is not None and self.text_editor_loc is not None: + self.extra_end_edit_cell_func( + EditCellEvent( + int(self.text_editor_loc[0]), + int(self.text_editor_loc[1]), + "Escape", + None, + "escape_edit_cell", + ) + ) + self.text_editor_loc = None + try: + self.delete(self.text_editor_id) + except Exception: + pass + try: + self.text_editor.destroy() + except Exception: + pass + self.text_editor = None + self.text_editor_id = None + self.show_current() + if event is not None and len(event) >= 3 and "Escape" in event: + self.focus_set() + + # c is displayed col + def close_text_editor( + self, + editor_info=None, + r=None, + c=None, + set_data_on_close=True, + event=None, + destroy=True, + move_down=True, + redraw=True, + recreate=True, + ): + if self.focus_get() is None and editor_info: + return "break" + if editor_info is not None and len(editor_info) >= 3 and editor_info[2] == "Escape": + self.destroy_text_editor("Escape") + self.close_dropdown_window(r, c) + return "break" + if self.text_editor is not None: + self.text_editor_value = self.text_editor.get() + if destroy: + self.destroy_text_editor() + if set_data_on_close: + if r is None and c is None and editor_info: + r, c = editor_info[0], editor_info[1] + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + if self.extra_end_edit_cell_func is None and self.input_valid_for_cell( + datarn, datacn, self.text_editor_value + ): + self.set_cell_data_undo( + r, + c, + datarn=datarn, + datacn=datacn, + value=self.text_editor_value, + redraw=False, + check_input_valid=False, + ) + elif ( + self.extra_end_edit_cell_func is not None + and not self.edit_cell_validation + and self.input_valid_for_cell(datarn, datacn, self.text_editor_value) + ): + self.set_cell_data_undo( + r, + c, + datarn=datarn, + datacn=datacn, + value=self.text_editor_value, + redraw=False, + check_input_valid=False, + ) + self.extra_end_edit_cell_func( + EditCellEvent( + r, + c, + editor_info[2] if len(editor_info) >= 3 else "FocusOut", + f"{self.text_editor_value}", + "end_edit_cell", + ) + ) + elif self.extra_end_edit_cell_func is not None and self.edit_cell_validation: + validation = self.extra_end_edit_cell_func( + EditCellEvent( + r, + c, + editor_info[2] if len(editor_info) >= 3 else "FocusOut", + f"{self.text_editor_value}", + "end_edit_cell", + ) + ) + self.text_editor_value = validation + if validation is not None and self.input_valid_for_cell(datarn, datacn, self.text_editor_value): + self.set_cell_data_undo( + r, + c, + datarn=datarn, + datacn=datacn, + value=self.text_editor_value, + redraw=False, + check_input_valid=False, + ) + if move_down: + if r is None and c is None and editor_info: + r, c = editor_info[0], editor_info[1] + currently_selected = self.currently_selected() + if ( + r is not None + and c is not None + and currently_selected + and r == currently_selected[0] + and c == currently_selected[1] + and (self.single_selection_enabled or self.toggle_selection_enabled) + ): + r1, c1, r2, c2 = self.find_last_selected_box_with_current(currently_selected) + numcols = c2 - c1 + numrows = r2 - r1 + if numcols == 1 and numrows == 1: + if editor_info is not None and len(editor_info) >= 3 and editor_info[2] == "Return": + self.select_cell(r + 1 if r < len(self.row_positions) - 2 else r, c) + self.see( + r + 1 if r < len(self.row_positions) - 2 else r, + c, + keep_xscroll=True, + bottom_right_corner=True, + check_cell_visibility=True, + ) + elif editor_info is not None and len(editor_info) >= 3 and editor_info[2] == "Tab": + self.select_cell(r, c + 1 if c < len(self.col_positions) - 2 else c) + self.see( + r, + c + 1 if c < len(self.col_positions) - 2 else c, + keep_xscroll=True, + bottom_right_corner=True, + check_cell_visibility=True, + ) + else: + moved = False + new_r = r + new_c = c + if editor_info is not None and len(editor_info) >= 3 and editor_info[2] == "Return": + if r + 1 == r2: + new_r = r1 + elif numrows > 1: + new_r = r + 1 + moved = True + if not moved: + if c + 1 == c2: + new_c = c1 + elif numcols > 1: + new_c = c + 1 + elif editor_info is not None and len(editor_info) >= 3 and editor_info[2] == "Tab": + if c + 1 == c2: + new_c = c1 + elif numcols > 1: + new_c = c + 1 + moved = True + if not moved: + if r + 1 == r2: + new_r = r1 + elif numrows > 1: + new_r = r + 1 + self.set_currently_selected(new_r, new_c, type_=currently_selected.type_) + self.see( + new_r, + new_c, + keep_xscroll=False, + bottom_right_corner=True, + check_cell_visibility=True, + ) + self.close_dropdown_window(r, c) + if recreate: + self.recreate_all_selection_boxes() + if redraw: + self.refresh() + if editor_info is not None and len(editor_info) >= 3 and editor_info[2] != "FocusOut": + self.focus_set() + return "break" + + def tab_key(self, event=None): + currently_selected = self.currently_selected() + if not currently_selected: + return + r = currently_selected.row + c = currently_selected.column + r1, c1, r2, c2 = self.find_last_selected_box_with_current(currently_selected) + numcols = c2 - c1 + numrows = r2 - r1 + if numcols == 1 and numrows == 1: + new_r = r + new_c = c + 1 if c < len(self.col_positions) - 2 else c + else: + moved = False + new_r = r + new_c = c + if c + 1 == c2: + new_c = c1 + elif numcols > 1: + new_c = c + 1 + moved = True + if not moved: + if r + 1 == r2: + new_r = r1 + elif numrows > 1: + new_r = r + 1 + self.set_currently_selected(new_r, new_c, type_=currently_selected.type_) + self.see( + new_r, + new_c, + keep_xscroll=False, + bottom_right_corner=True, + check_cell_visibility=True, + ) + return "break" + + # internal event use + def set_cell_data_undo( + self, + r=0, + c=0, + datarn=None, + datacn=None, + value="", + undo=True, + cell_resize=True, + redraw=True, + check_input_valid=True, + ): + if datacn is None: + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + if datarn is None: + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + if not check_input_valid or self.input_valid_for_cell(datarn, datacn, value): + if self.undo_enabled and undo: + self.undo_storage.append( + zlib.compress( + pickle.dumps( + ( + "edit_cells", + {(datarn, datacn): self.get_cell_data(datarn, datacn)}, + self.get_boxes(include_current=False), + self.currently_selected(), + ) + ) + ) + ) + self.set_cell_data(datarn, datacn, value) + if cell_resize and self.cell_auto_resize_enabled: + self.set_cell_size_to_text(r, c, only_set_if_too_small=True, redraw=redraw, run_binding=True) + self.parentframe.emit_event("<>") + return True + + def set_cell_data(self, datarn, datacn, value, kwargs={}, expand_sheet=True): + if expand_sheet: + if datarn >= len(self.data): + self.fix_data_len(datarn, datacn) + elif datacn >= len(self.data[datarn]): + self.fix_row_len(datarn, datacn) + if expand_sheet or (len(self.data) > datarn and len(self.data[datarn]) > datacn): + if ( + datarn, + datacn, + ) in self.cell_options and "checkbox" in self.cell_options[(datarn, datacn)]: + self.data[datarn][datacn] = try_to_bool(value) + else: + if not kwargs: + kwargs = self.get_cell_kwargs(datarn, datacn, key="format") + if kwargs: + if kwargs["formatter"] is None: + self.data[datarn][datacn] = format_data(value=value, **kwargs) + else: + self.data[datarn][datacn] = kwargs["formatter"](value, **kwargs) + else: + self.data[datarn][datacn] = value + + def get_value_for_empty_cell(self, datarn, datacn, r_ops=True, c_ops=True): + if self.get_cell_kwargs( + datarn, + datacn, + key="checkbox", + cell=r_ops and c_ops, + row=r_ops, + column=c_ops, + ): + return False + kwargs = self.get_cell_kwargs( + datarn, + datacn, + key="dropdown", + cell=r_ops and c_ops, + row=r_ops, + column=c_ops, + ) + if kwargs and kwargs["validate_input"] and kwargs["values"]: + return kwargs["values"][0] + return "" + + def get_empty_row_seq(self, datarn, end, start=0, r_ops=True, c_ops=True): + return [self.get_value_for_empty_cell(datarn, datacn, r_ops=r_ops, c_ops=c_ops) for datacn in range(start, end)] + + def fix_row_len(self, datarn, datacn): + self.data[datarn].extend(self.get_empty_row_seq(datarn, end=datacn + 1, start=len(self.data[datarn]))) + + def fix_row_values(self, datarn, start=None, end=None): + if datarn < len(self.data): + for datacn, v in enumerate(islice(self.data[datarn], start, end)): + if not self.input_valid_for_cell(datarn, datacn, v): + self.data[datarn][datacn] = self.get_value_for_empty_cell(datarn, datacn) + + def fix_data_len(self, datarn, datacn): + ncols = self.total_data_cols() if datacn is None else datacn + 1 + self.data.extend([self.get_empty_row_seq(rn, end=ncols, start=0) for rn in range(len(self.data), datarn + 1)]) + + # internal event use + def click_checkbox(self, r, c, datarn=None, datacn=None, undo=True, redraw=True): + if datarn is None: + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + if datacn is None: + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + kwargs = self.get_cell_kwargs(datarn, datacn, key="checkbox") + if kwargs["state"] == "normal": + self.set_cell_data_undo( + r, + c, + value=not self.data[datarn][datacn] if isinstance(self.data[datarn][datacn], bool) else False, + undo=undo, + cell_resize=False, + check_input_valid=False, + ) + if kwargs["check_function"] is not None: + kwargs["check_function"]((r, c, "CheckboxClicked", self.data[datarn][datacn])) + if self.extra_end_edit_cell_func is not None: + self.extra_end_edit_cell_func(EditCellEvent(r, c, "Return", self.data[datarn][datacn], "end_edit_cell")) + if redraw: + self.refresh() + + def create_checkbox(self, datarn=0, datacn=0, **kwargs): + self.delete_cell_format(datarn, datacn, clear_values=False) + if (datarn, datacn) in self.cell_options and ( + "dropdown" in self.cell_options[(datarn, datacn)] or "checkbox" in self.cell_options[(datarn, datacn)] + ): + self.delete_cell_options_dropdown_and_checkbox(datarn, datacn) + if (datarn, datacn) not in self.cell_options: + self.cell_options[(datarn, datacn)] = {} + self.cell_options[(datarn, datacn)]["checkbox"] = get_checkbox_dict(**kwargs) + self.set_cell_data(datarn, datacn, kwargs["checked"]) + + def checkbox_row(self, datarn=0, **kwargs): + self.delete_row_format(datarn, clear_values=False) + if datarn in self.row_options and ( + "dropdown" in self.row_options[datarn] or "checkbox" in self.row_options[datarn] + ): + self.delete_row_options_dropdown_and_checkbox(datarn) + if datarn not in self.row_options: + self.row_options[datarn] = {} + self.row_options[datarn]["checkbox"] = get_checkbox_dict(**kwargs) + for datacn in range(self.total_data_cols()): + self.set_cell_data(datarn, datacn, kwargs["checked"]) + + def checkbox_column(self, datacn=0, **kwargs): + self.delete_column_format(datacn, clear_values=False) + if datacn in self.col_options and ( + "dropdown" in self.col_options[datacn] or "checkbox" in self.col_options[datacn] + ): + self.delete_column_options_dropdown_and_checkbox(datacn) + if datacn not in self.col_options: + self.col_options[datacn] = {} + self.col_options[datacn]["checkbox"] = get_checkbox_dict(**kwargs) + for datarn in range(self.total_data_rows()): + self.set_cell_data(datarn, datacn, kwargs["checked"]) + + def checkbox_sheet(self, **kwargs): + self.delete_sheet_format(clear_values=False) + if "dropdown" in self.options or "checkbox" in self.options: + self.delete_options_dropdown_and_checkbox() + self.options["checkbox"] = get_checkbox_dict(**kwargs) + total_cols = self.total_data_cols() + for datarn in range(self.total_data_rows()): + for datacn in range(total_cols): + self.set_cell_data(datarn, datacn, kwargs["checked"]) + + def create_dropdown(self, datarn=0, datacn=0, **kwargs): + if (datarn, datacn) in self.cell_options and ( + "dropdown" in self.cell_options[(datarn, datacn)] or "checkbox" in self.cell_options[(datarn, datacn)] + ): + self.delete_cell_options_dropdown_and_checkbox(datarn, datacn) + if (datarn, datacn) not in self.cell_options: + self.cell_options[(datarn, datacn)] = {} + self.cell_options[(datarn, datacn)]["dropdown"] = get_dropdown_dict(**kwargs) + self.set_cell_data( + datarn, + datacn, + kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "", + ) + + def dropdown_row(self, datarn=0, **kwargs): + if datarn in self.row_options and ( + "dropdown" in self.row_options[datarn] or "checkbox" in self.row_options[datarn] + ): + self.delete_row_options_dropdown_and_checkbox(datarn) + if datarn not in self.row_options: + self.row_options[datarn] = {} + self.row_options[datarn]["dropdown"] = get_dropdown_dict(**kwargs) + value = ( + kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" + ) + for datacn in range(self.total_data_cols()): + self.set_cell_data(datarn, datacn, value) + + def dropdown_column(self, datacn=0, **kwargs): + if datacn in self.col_options and ( + "dropdown" in self.col_options[datacn] or "checkbox" in self.col_options[datacn] + ): + self.delete_column_options_dropdown_and_checkbox(datacn) + if datacn not in self.col_options: + self.col_options[datacn] = {} + self.col_options[datacn]["dropdown"] = get_dropdown_dict(**kwargs) + value = ( + kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" + ) + for datarn in range(self.total_data_rows()): + self.set_cell_data(datarn, datacn, value) + + def dropdown_sheet(self, **kwargs): + if "dropdown" in self.options or "checkbox" in self.options: + self.delete_options_dropdown_and_checkbox() + self.options["dropdown"] = get_dropdown_dict(**kwargs) + value = ( + kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" + ) + total_cols = self.total_data_cols() + for datarn in range(self.total_data_rows()): + for datacn in range(total_cols): + self.set_cell_data(datarn, datacn, value) + + def format_cell(self, datarn, datacn, **kwargs): + if (datarn, datacn) in self.cell_options and "checkbox" in self.cell_options[(datarn, datacn)]: + return + kwargs = self.format_fix_kwargs(kwargs) + if (datarn, datacn) not in self.cell_options: + self.cell_options[(datarn, datacn)] = {} + self.cell_options[(datarn, datacn)]["format"] = kwargs + self.set_cell_data( + datarn, + datacn, + value=kwargs["value"] if "value" in kwargs else self.get_cell_data(datarn, datacn), + kwargs=kwargs, + ) + + def format_row(self, datarn, **kwargs): + if datarn in self.row_options and "checkbox" in self.row_options[datarn]: + return + kwargs = self.format_fix_kwargs(kwargs) + if datarn not in self.row_options: + self.row_options[datarn] = {} + self.row_options[datarn]["format"] = kwargs + for datacn in range(self.total_data_cols()): + self.set_cell_data( + datarn, + datacn, + value=kwargs["value"] if "value" in kwargs else self.get_cell_data(datarn, datacn), + kwargs=kwargs, + ) + + def format_column(self, datacn, **kwargs): + if datacn in self.col_options and "checkbox" in self.col_options[datacn]: + return + kwargs = self.format_fix_kwargs(kwargs) + if datacn not in self.col_options: + self.col_options[datacn] = {} + self.col_options[datacn]["format"] = kwargs + for datarn in range(self.total_data_rows()): + self.set_cell_data( + datarn, + datacn, + value=kwargs["value"] if "value" in kwargs else self.get_cell_data(datarn, datacn), + kwargs=kwargs, + ) + + def format_sheet(self, **kwargs): + kwargs = self.format_fix_kwargs(kwargs) + self.options["format"] = kwargs + for datarn in range(self.total_data_rows()): + for datacn in range(self.total_data_cols()): + self.set_cell_data( + datarn, + datacn, + value=kwargs["value"] if "value" in kwargs else self.get_cell_data(datarn, datacn), + kwargs=kwargs, + ) + + def format_fix_kwargs(self, kwargs): + if kwargs["formatter"] is None: + if kwargs["nullable"]: + if isinstance(kwargs["datatypes"], (list, tuple)): + kwargs["datatypes"] = tuple(kwargs["datatypes"]) + (type(None),) + else: + kwargs["datatypes"] = (kwargs["datatypes"], type(None)) + elif (isinstance(kwargs["datatypes"], (list, tuple)) and type(None) in kwargs["datatypes"]) or kwargs[ + "datatypes" + ] is type(None): + raise TypeError("Non-nullable cells cannot have NoneType as a datatype.") + if not isinstance(kwargs["invalid_value"], str): + kwargs["invalid_value"] = f"{kwargs['invalid_value']}" + return kwargs + + def reapply_formatting(self): + if "format" in self.options: + for r in range(len(self.data)): + if r not in self.row_options: + for c in range(len(self.data[r])): + if not ( + (r, c) in self.cell_options + and "format" in self.cell_options[(r, c)] + or c in self.col_options + and "format" in self.col_options[c] + ): + self.set_cell_data(r, c, value=self.data[r][c]) + for c in self.yield_formatted_columns(): + for r in range(len(self.data)): + if not ( + (r, c) in self.cell_options + and "format" in self.cell_options[(r, c)] + or r in self.row_options + and "format" in self.row_options[r] + ): + self.set_cell_data(r, c, value=self.data[r][c]) + for r in self.yield_formatted_rows(): + for c in range(len(self.data[r])): + if not ((r, c) in self.cell_options and "format" in self.cell_options[(r, c)]): + self.set_cell_data(r, c, value=self.data[r][c]) + for r, c in self.yield_formatted_cells(): + if len(self.data) > r and len(self.data[r]) > c: + self.set_cell_data(r, c, value=self.data[r][c]) + + def delete_all_formatting(self, clear_values=False): + self.delete_cell_format("all", clear_values=clear_values) + self.delete_row_format("all", clear_values=clear_values) + self.delete_column_format("all", clear_values=clear_values) + self.delete_sheet_format(clear_values=clear_values) + + def delete_cell_format(self, datarn="all", datacn=0, clear_values=False): + if isinstance(datarn, str) and datarn.lower() == "all": + for datarn, datacn in self.yield_formatted_cells(): + del self.cell_options[(datarn, datacn)]["format"] + if clear_values: + self.set_cell_data(datarn, datacn, "", expand_sheet=False) + else: + if (datarn, datacn) in self.cell_options and "format" in self.cell_options[(datarn, datacn)]: + del self.cell_options[(datarn, datacn)]["format"] + if clear_values: + self.set_cell_data(datarn, datacn, "", expand_sheet=False) + + def delete_row_format(self, datarn="all", clear_values=False): + if isinstance(datarn, str) and datarn.lower() == "all": + for datarn in self.yield_formatted_rows(): + del self.row_options[datarn]["format"] + if clear_values: + for datacn in range(len(self.data[datarn])): + self.set_cell_data(datarn, datacn, "", expand_sheet=False) + else: + if datarn in self.row_options and "format" in self.row_options[datarn]: + del self.row_options[datarn]["format"] + if clear_values: + for datacn in range(len(self.data[datarn])): + self.set_cell_data(datarn, datacn, "", expand_sheet=False) + + def delete_column_format(self, datacn="all", clear_values=False): + if isinstance(datacn, str) and datacn.lower() == "all": + for datacn in self.yield_formatted_columns(): + del self.col_options[datacn]["format"] + if clear_values: + for datarn in range(len(self.data)): + self.set_cell_data(datarn, datacn, "", expand_sheet=False) + else: + if datacn in self.col_options and "format" in self.col_options[datacn]: + del self.col_options[datacn]["format"] + if clear_values: + for datarn in range(len(self.data)): + self.set_cell_data(datarn, datacn, "", expand_sheet=False) + + def delete_sheet_format(self, clear_values=False): + if "format" in self.options: + del self.options["format"] + if clear_values: + total_cols = self.total_data_cols() + self.data = [ + [self.get_value_for_empty_cell(r, c) for c in range(total_cols)] + for r in range(self.total_data_rows()) + ] + + # deals with possibility of formatter class being in self.data cell + # if cell is formatted - possibly returns invalid_value kwarg if cell value is not in datatypes kwarg + # if get displayed is true then Nones are replaced by "" + def get_valid_cell_data_as_str(self, datarn, datacn, get_displayed=False, **kwargs) -> str: + if get_displayed: + kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") + if kwargs: + if kwargs["text"] is not None: + return f"{kwargs['text']}" + else: + kwargs = self.get_cell_kwargs(datarn, datacn, key="checkbox") + if kwargs: + return f"{kwargs['text']}" + value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else "" + kwargs = self.get_cell_kwargs(datarn, datacn, key="format") + if kwargs: + if kwargs["formatter"] is None: + if get_displayed: + return data_to_str(value, **kwargs) + else: + return f"{get_data_with_valid_check(value, **kwargs)}" + else: + if get_displayed: + # assumed given formatter class has __str__() function + return f"{value}" + else: + # assumed given formatter class has get_data_with_valid_check() function + return f"{value.get_data_with_valid_check()}" + return "" if value is None else f"{value}" + + def get_cell_data(self, datarn, datacn, get_displayed=False, none_to_empty_str=False, **kwargs) -> Any: + if get_displayed: + return self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True) + value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else "" + kwargs = self.get_cell_kwargs(datarn, datacn, key="format") + if kwargs and kwargs["formatter"] is not None: + value = value.value # assumed given formatter class has value attribute + return "" if (value is None and none_to_empty_str) else value + + def input_valid_for_cell(self, datarn, datacn, value): + if self.get_cell_kwargs(datarn, datacn, key="readonly"): + return False + if self.cell_equal_to(datarn, datacn, value): + return False + if self.get_cell_kwargs(datarn, datacn, key="format"): + return True + if self.get_cell_kwargs(datarn, datacn, key="checkbox"): + return is_bool_like(value) + kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") + if kwargs and kwargs["validate_input"] and value not in kwargs["values"]: + return False + return True + + def cell_equal_to(self, datarn, datacn, value, **kwargs): + v = self.get_cell_data(datarn, datacn) + kwargs = self.get_cell_kwargs(datarn, datacn, key="format") + if kwargs and kwargs["formatter"] is None: + return v == format_data(value=value, **kwargs) + # assumed if there is a formatter class in cell then it has a __eq__() function anyway + # else if there is not a formatter class in cell and cell is not formatted + # then compare value as is + return v == value + + def get_cell_clipboard(self, datarn, datacn) -> Union[str, int, float, bool]: + value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else "" + kwargs = self.get_cell_kwargs(datarn, datacn, key="format") + if kwargs: + if kwargs["formatter"] is None: + return get_clipboard_data(value, **kwargs) + else: + # assumed given formatter class has get_clipboard_data() + # function and it returns one of above type hints + return value.get_clipboard_data() + return f"{value}" + + def yield_formatted_cells(self, formatter=None): + if formatter is None: + yield from ( + cell + for cell, options in self.cell_options.items() + if "format" in options and options["format"]["formatter"] == formatter + ) + else: + yield from (cell for cell, options in self.cell_options.items() if "format" in options) + + def yield_formatted_rows(self, formatter=None): + if formatter is None: + yield from (r for r, options in self.row_options.items() if "format" in options) + else: + yield from ( + r + for r, options in self.row_options.items() + if "format" in options and options["format"]["formatter"] == formatter + ) + + def yield_formatted_columns(self, formatter=None): + if formatter is None: + yield from (c for c, options in self.col_options.items() if "format" in options) + else: + yield from ( + c + for c, options in self.col_options.items() + if "format" in options and options["format"]["formatter"] == formatter + ) + + def get_cell_kwargs( + self, + datarn, + datacn, + key="format", + cell=True, + row=True, + column=True, + entire=True, + ): + if cell and (datarn, datacn) in self.cell_options and key in self.cell_options[(datarn, datacn)]: + return self.cell_options[(datarn, datacn)][key] + if row and datarn in self.row_options and key in self.row_options[datarn]: + return self.row_options[datarn][key] + if column and datacn in self.col_options and key in self.col_options[datacn]: + return self.col_options[datacn][key] + if entire and key in self.options: + return self.options[key] + return {} + + def get_space_bot(self, r, text_editor_h=None): + if len(self.row_positions) <= 1: + if text_editor_h is None: + win_h = int(self.winfo_height()) + sheet_h = int(1 + self.empty_vertical) + else: + win_h = int(self.winfo_height() - text_editor_h) + sheet_h = int(1 + self.empty_vertical - text_editor_h) + else: + if text_editor_h is None: + win_h = int(self.canvasy(0) + self.winfo_height() - self.row_positions[r + 1]) + sheet_h = int(self.row_positions[-1] + 1 + self.empty_vertical - self.row_positions[r + 1]) + else: + win_h = int(self.canvasy(0) + self.winfo_height() - (self.row_positions[r] + text_editor_h)) + sheet_h = int( + self.row_positions[-1] + 1 + self.empty_vertical - (self.row_positions[r] + text_editor_h) + ) + if win_h > 0: + win_h -= 1 + if sheet_h > 0: + sheet_h -= 1 + return win_h if win_h >= sheet_h else sheet_h + + def get_dropdown_height_anchor(self, r, c, text_editor_h=None): + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + win_h = 5 + for i, v in enumerate(self.get_cell_kwargs(datarn, datacn, key="dropdown")["values"]): + v_numlines = len(v.split("\n") if isinstance(v, str) else f"{v}".split("\n")) + if v_numlines > 1: + win_h += self.fl_ins + (v_numlines * self.xtra_lines_increment) + 5 # end of cell + else: + win_h += self.min_row_height + if i == 5: + break + if win_h > 500: + win_h = 500 + space_bot = self.get_space_bot(r, text_editor_h) + space_top = int(self.row_positions[r]) + anchor = "nw" + win_h2 = int(win_h) + if win_h > space_bot: + if space_bot >= space_top: + anchor = "nw" + win_h = space_bot - 1 + elif space_top > space_bot: + anchor = "sw" + win_h = space_top - 1 + if win_h < self.txt_h + 5: + win_h = self.txt_h + 5 + elif win_h > win_h2: + win_h = win_h2 + return win_h, anchor + + # c is displayed col + def open_dropdown_window(self, r, c, event=None): + self.destroy_text_editor("Escape") + self.destroy_opened_dropdown_window() + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") + if kwargs["state"] == "normal": + if not self.open_text_editor(event=event, r=r, c=c, dropdown=True): + return + win_h, anchor = self.get_dropdown_height_anchor(r, c) + window = self.parentframe.dropdown_class( + self.winfo_toplevel(), + r, + c, + width=self.col_positions[c + 1] - self.col_positions[c] + 1, + height=win_h, + font=self.table_font, + colors={ + "bg": self.popup_menu_bg, + "fg": self.popup_menu_fg, + "highlight_bg": self.popup_menu_highlight_bg, + "highlight_fg": self.popup_menu_highlight_fg, + }, + outline_color=self.table_selected_cells_border_fg, + outline_thickness=2, + values=kwargs["values"], + close_dropdown_window=self.close_dropdown_window, + search_function=kwargs["search_function"], + arrowkey_RIGHT=self.arrowkey_RIGHT, + arrowkey_LEFT=self.arrowkey_LEFT, + align="w", + ) # self.get_cell_align(r, c) + if kwargs["state"] == "normal": + if anchor == "nw": + ypos = self.row_positions[r] + self.text_editor.h_ - 1 + else: + ypos = self.row_positions[r] + kwargs["canvas_id"] = self.create_window((self.col_positions[c], ypos), window=window, anchor=anchor) + self.text_editor.textedit.bind( + "<>", + lambda x: window.search_and_see( + DropDownModifiedEvent("ComboboxModified", r, c, self.text_editor.get()) + ), + ) + if kwargs["modified_function"] is not None: + window.modified_function = kwargs["modified_function"] + self.update_idletasks() + try: + self.after(1, lambda: self.text_editor.textedit.focus()) + self.after(2, self.text_editor.scroll_to_bottom()) + except Exception: + return + redraw = False + else: + if anchor == "nw": + ypos = self.row_positions[r + 1] + else: + ypos = self.row_positions[r] + kwargs["canvas_id"] = self.create_window((self.col_positions[c], ypos), window=window, anchor=anchor) + self.update_idletasks() + window.bind("", lambda x: self.close_dropdown_window(r, c)) + window.focus() + redraw = True + self.existing_dropdown_window = window + kwargs["window"] = window + self.existing_dropdown_canvas_id = kwargs["canvas_id"] + if redraw: + self.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=False) + + # displayed indexes, not data + def close_dropdown_window(self, r=None, c=None, selection=None, redraw=True): + if r is not None and c is not None and selection is not None: + datacn = c if self.all_columns_displayed else self.displayed_columns[c] + datarn = r if self.all_rows_displayed else self.displayed_rows[r] + kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") + if kwargs["select_function"] is not None: # user has specified a selection function + kwargs["select_function"](EditCellEvent(r, c, "ComboboxSelected", f"{selection}", "end_edit_cell")) + if self.extra_end_edit_cell_func is None: + self.set_cell_data_undo(r, c, value=selection, redraw=not redraw) + elif self.extra_end_edit_cell_func is not None and self.edit_cell_validation: + validation = self.extra_end_edit_cell_func( + EditCellEvent(r, c, "ComboboxSelected", f"{selection}", "end_edit_cell") + ) + if validation is not None: + selection = validation + self.set_cell_data_undo(r, c, value=selection, redraw=not redraw) + elif self.extra_end_edit_cell_func is not None and not self.edit_cell_validation: + self.set_cell_data_undo(r, c, value=selection, redraw=not redraw) + self.extra_end_edit_cell_func(EditCellEvent(r, c, "ComboboxSelected", f"{selection}", "end_edit_cell")) + self.focus_set() + self.recreate_all_selection_boxes() + self.destroy_text_editor("Escape") + self.destroy_opened_dropdown_window(r, c) + if redraw: + self.refresh() + + def get_existing_dropdown_coords(self): + if self.existing_dropdown_window is not None: + return int(self.existing_dropdown_window.r), int(self.existing_dropdown_window.c) + return None + + def mouseclick_outside_editor_or_dropdown(self): + closed_dd_coords = self.get_existing_dropdown_coords() + if self.text_editor_loc is not None and self.text_editor is not None: + self.close_text_editor(editor_info=self.text_editor_loc + ("ButtonPress-1",)) + else: + self.destroy_text_editor("Escape") + if closed_dd_coords is not None: + self.destroy_opened_dropdown_window( + closed_dd_coords[0], closed_dd_coords[1] + ) # displayed coords not data, necessary for b1 function + return closed_dd_coords + + def mouseclick_outside_editor_or_dropdown_all_canvases(self): + self.CH.mouseclick_outside_editor_or_dropdown() + self.RI.mouseclick_outside_editor_or_dropdown() + return self.mouseclick_outside_editor_or_dropdown() + + # function can receive 4 None args + def destroy_opened_dropdown_window(self, r=None, c=None, datarn=None, datacn=None): + if r is None and datarn is None and c is None and datacn is None and self.existing_dropdown_window is not None: + r, c = self.get_existing_dropdown_coords() + if c is not None or datacn is not None: + if datacn is None: + datacn_ = c if self.all_columns_displayed else self.displayed_columns[c] + else: + datacn_ = datacn + else: + datacn_ = None + if r is not None or datarn is not None: + if datarn is None: + datarn_ = r if self.all_rows_displayed else self.displayed_rows[r] + else: + datarn_ = datarn + else: + datarn_ = None + try: + self.delete(self.existing_dropdown_canvas_id) + except Exception: + pass + self.existing_dropdown_canvas_id = None + try: + self.existing_dropdown_window.destroy() + except Exception: + pass + kwargs = self.get_cell_kwargs(datarn_, datacn_, key="dropdown") + if kwargs: + kwargs["canvas_id"] = "no dropdown open" + kwargs["window"] = "no dropdown open" + try: + self.delete(kwargs["canvas_id"]) + except Exception: + pass + self.existing_dropdown_window = None + + def get_displayed_col_from_datacn(self, datacn): + try: + return self.displayed_columns.index(datacn) + except Exception: + return None + + def delete_cell_options_dropdown(self, datarn, datacn): + self.destroy_opened_dropdown_window() + if (datarn, datacn) in self.cell_options and "dropdown" in self.cell_options[(datarn, datacn)]: + del self.cell_options[(datarn, datacn)]["dropdown"] + + def delete_cell_options_checkbox(self, datarn, datacn): + if (datarn, datacn) in self.cell_options and "checkbox" in self.cell_options[(datarn, datacn)]: + del self.cell_options[(datarn, datacn)]["checkbox"] + + def delete_cell_options_dropdown_and_checkbox(self, datarn, datacn): + self.delete_cell_options_dropdown(datarn, datacn) + self.delete_cell_options_checkbox(datarn, datacn) + + def delete_row_options_dropdown(self, datarn): + self.destroy_opened_dropdown_window() + if datarn in self.row_options and "dropdown" in self.row_options[datarn]: + del self.row_options[datarn]["dropdown"] + + def delete_row_options_checkbox(self, datarn): + if datarn in self.row_options and "checkbox" in self.row_options[datarn]: + del self.row_options[datarn]["checkbox"] + + def delete_row_options_dropdown_and_checkbox(self, datarn): + self.delete_row_options_dropdown(datarn) + self.delete_row_options_checkbox(datarn) + + def delete_column_options_dropdown(self, datacn): + self.destroy_opened_dropdown_window() + if datacn in self.col_options and "dropdown" in self.col_options[datacn]: + del self.col_options[datacn]["dropdown"] + + def delete_column_options_checkbox(self, datacn): + if datacn in self.col_options and "checkbox" in self.col_options[datacn]: + del self.col_options[datacn]["checkbox"] + + def delete_column_options_dropdown_and_checkbox(self, datacn): + self.delete_column_options_dropdown(datacn) + self.delete_column_options_checkbox(datacn) + + def delete_options_dropdown(self): + self.destroy_opened_dropdown_window() + if "dropdown" in self.options: + del self.options["dropdown"] + + def delete_options_checkbox(self): + if "checkbox" in self.options: + del self.options["checkbox"] + + def delete_options_dropdown_and_checkbox(self): + self.delete_options_dropdown() + self.delete_options_checkbox() diff --git a/thirdparty/tksheet/_tksheet_other_classes.py b/thirdparty/tksheet/_tksheet_other_classes.py new file mode 100644 index 0000000..eaaf3d0 --- /dev/null +++ b/thirdparty/tksheet/_tksheet_other_classes.py @@ -0,0 +1,400 @@ +from __future__ import annotations + +import bisect +import tkinter as tk +from collections import namedtuple +from itertools import islice +from ._tksheet_vars import ( + ctrl_key, + get_font, + rc_binding, +) +import warnings + + +def show_kwargs_warning(kwargs, name): + for kw in kwargs: + warnings.warn(f"Argument '{kw}' for function '{name}' is not valid or has been deprecated", stacklevel=2) + + +CurrentlySelectedClass = namedtuple("CurrentlySelectedClass", "row column type_") +CtrlKeyEvent = namedtuple("CtrlKeyEvent", "eventname selectionboxes currentlyselected rows") +PasteEvent = namedtuple("PasteEvent", "eventname currentlyselected rows") +UndoEvent = namedtuple("UndoEvent", "eventname type storeddata") +SelectCellEvent = namedtuple("SelectCellEvent", "eventname row column") +SelectColumnEvent = namedtuple("SelectColumnEvent", "eventname column") +SelectRowEvent = namedtuple("SelectRowEvent", "eventname row") +DeselectionEvent = namedtuple("DeselectionEvent", "eventname selectionboxes") +SelectionBoxEvent = namedtuple("SelectionBoxEvent", "eventname selectionboxes") +InsertEvent = namedtuple("InsertEvent", "eventname dataindex displayindex quantity") +DeleteRowColumnEvent = namedtuple("DeleteRowColumnEvent", "eventname deleteindexes") +EditCellEvent = namedtuple("EditCellEvent", "row column key text eventname") +EditHeaderEvent = namedtuple("EditHeaderEvent", "column key text eventname") +EditIndexEvent = namedtuple("EditIndexEvent", "row key text eventname") +BeginDragDropEvent = namedtuple("BeginDragDropEvent", "eventname columnstomove movedto") +EndDragDropEvent = namedtuple("EndDragDropEvent", "eventname oldindexes newindexes movedto") +ResizeEvent = namedtuple("ResizeEvent", "eventname index oldsize newsize") +DropDownModifiedEvent = namedtuple("DropDownModifiedEvent", "eventname row column value") +DraggedRowColumn = namedtuple("DraggedRowColumn", "dragged to_move") + + +class TextEditor_(tk.Text): + def __init__( + self, + parent, + font=get_font(), + text=None, + state="normal", + bg="white", + fg="black", + popup_menu_font=("Arial", 11, "normal"), + popup_menu_bg="white", + popup_menu_fg="black", + popup_menu_highlight_bg="blue", + popup_menu_highlight_fg="white", + align="w", + newline_binding=None, + ): + tk.Text.__init__( + self, + parent, + font=font, + state=state, + spacing1=0, + spacing2=0, + spacing3=0, + bd=0, + highlightthickness=0, + undo=True, + maxundo=30, + background=bg, + foreground=fg, + insertbackground=fg, + ) + self.parent = parent + self.newline_bindng = newline_binding + if align == "w": + self.align = "left" + elif align == "center": + self.align = "center" + elif align == "e": + self.align = "right" + self.tag_configure("align", justify=self.align) + if text: + self.insert(1.0, text) + self.yview_moveto(1) + self.tag_add("align", 1.0, "end") + self.rc_popup_menu = tk.Menu(self, tearoff=0) + self.rc_popup_menu.add_command( + label="Select all", + accelerator="Ctrl+A", + font=popup_menu_font, + foreground=popup_menu_fg, + background=popup_menu_bg, + activebackground=popup_menu_highlight_bg, + activeforeground=popup_menu_highlight_fg, + command=self.select_all, + ) + self.rc_popup_menu.add_command( + label="Cut", + accelerator="Ctrl+X", + font=popup_menu_font, + foreground=popup_menu_fg, + background=popup_menu_bg, + activebackground=popup_menu_highlight_bg, + activeforeground=popup_menu_highlight_fg, + command=self.cut, + ) + self.rc_popup_menu.add_command( + label="Copy", + accelerator="Ctrl+C", + font=popup_menu_font, + foreground=popup_menu_fg, + background=popup_menu_bg, + activebackground=popup_menu_highlight_bg, + activeforeground=popup_menu_highlight_fg, + command=self.copy, + ) + self.rc_popup_menu.add_command( + label="Paste", + accelerator="Ctrl+V", + font=popup_menu_font, + foreground=popup_menu_fg, + background=popup_menu_bg, + activebackground=popup_menu_highlight_bg, + activeforeground=popup_menu_highlight_fg, + command=self.paste, + ) + self.rc_popup_menu.add_command( + label="Undo", + accelerator="Ctrl+Z", + font=popup_menu_font, + foreground=popup_menu_fg, + background=popup_menu_bg, + activebackground=popup_menu_highlight_bg, + activeforeground=popup_menu_highlight_fg, + command=self.undo, + ) + self.bind("<1>", lambda event: self.focus_set()) + self.bind(rc_binding, self.rc) + self._orig = self._w + "_orig" + self.tk.call("rename", self._w, self._orig) + self.tk.createcommand(self._w, self._proxy) + + def _proxy(self, command, *args): + cmd = (self._orig, command) + args + try: + result = self.tk.call(cmd) + except Exception: + return + if command in ("insert", "delete", "replace"): + self.tag_add("align", 1.0, "end") + self.event_generate("<>") + if args and len(args) > 1 and args[1] != "\n": + out_of_bounds = self.yview() + if out_of_bounds != (0.0, 1.0) and self.newline_bindng is not None: + self.newline_bindng(r=self.parent.r, c=self.parent.c, check_lines=False) + return result + + def rc(self, event): + self.focus_set() + self.rc_popup_menu.tk_popup(event.x_root, event.y_root) + + def select_all(self, event=None): + self.event_generate(f"<{ctrl_key}-a>") + return "break" + + def cut(self, event=None): + self.event_generate(f"<{ctrl_key}-x>") + return "break" + + def copy(self, event=None): + self.event_generate(f"<{ctrl_key}-c>") + return "break" + + def paste(self, event=None): + self.event_generate(f"<{ctrl_key}-v>") + return "break" + + def undo(self, event=None): + self.event_generate(f"<{ctrl_key}-z>") + return "break" + + +class TextEditor(tk.Frame): + def __init__( + self, + parent, + font=get_font(), + text=None, + state="normal", + width=None, + height=None, + border_color="black", + show_border=True, + bg="white", + fg="black", + popup_menu_font=("Arial", 11, "normal"), + popup_menu_bg="white", + popup_menu_fg="black", + popup_menu_highlight_bg="blue", + popup_menu_highlight_fg="white", + binding=None, + align="w", + r=0, + c=0, + newline_binding=None, + ): + tk.Frame.__init__( + self, + parent, + height=height, + width=width, + highlightbackground=border_color, + highlightcolor=border_color, + highlightthickness=2 if show_border else 0, + bd=0, + ) + self.parent = parent + self.r = r + self.c = c + self.textedit = TextEditor_( + self, + font=font, + text=text, + state=state, + bg=bg, + fg=fg, + popup_menu_font=popup_menu_font, + popup_menu_bg=popup_menu_bg, + popup_menu_fg=popup_menu_fg, + popup_menu_highlight_bg=popup_menu_highlight_bg, + popup_menu_highlight_fg=popup_menu_highlight_fg, + align=align, + newline_binding=newline_binding, + ) + self.textedit.grid(row=0, column=0, sticky="nswe") + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(0, weight=1) + self.grid_propagate(False) + self.w_ = width + self.h_ = height + self.binding = binding + self.textedit.focus_set() + + def get(self): + return self.textedit.get("1.0", "end-1c") + + def get_num_lines(self): + return int(self.textedit.index("end-1c").split(".")[0]) + + def set_text(self, text): + self.textedit.delete(1.0, "end") + self.textedit.insert(1.0, text) + + def scroll_to_bottom(self): + self.textedit.yview_moveto(1) + + +class GeneratedMouseEvent: + def __init__(self): + self.keycode = "??" + self.num = 1 + + +def dropdown_search_function(search_for, data): + search_len = len(search_for) + best_match = {"rn": float("inf"), "st": float("inf"), "len_diff": float("inf")} + for rn, row in enumerate(data): + dd_val = rf"{row[0]}".lower() + st = dd_val.find(search_for) + if st > -1: + # priority is start index + # if there's already a matching start + # then compare the len difference + len_diff = len(dd_val) - search_len + if st < best_match["st"] or (st == best_match["st"] and len_diff < best_match["len_diff"]): + best_match["rn"] = rn + best_match["st"] = st + best_match["len_diff"] = len_diff + if best_match["rn"] != float("inf"): + return best_match["rn"] + return None + + +def get_dropdown_kwargs( + values=[], + set_value=None, + state="normal", + redraw=True, + selection_function=None, + modified_function=None, + search_function=dropdown_search_function, + validate_input=True, + text=None, + **kwargs, +): + if kwargs: + show_kwargs_warning(kwargs, "get_dropdown_kwargs") + return { + "values": values, + "set_value": set_value, + "state": state, + "redraw": redraw, + "selection_function": selection_function, + "modified_function": modified_function, + "search_function": search_function, + "validate_input": validate_input, + "text": text, + } + + +def get_dropdown_dict(**kwargs): + return { + "values": kwargs["values"], + "window": "no dropdown open", + "canvas_id": "no dropdown open", + "select_function": kwargs["selection_function"], + "modified_function": kwargs["modified_function"], + "search_function": kwargs["search_function"], + "validate_input": kwargs["validate_input"], + "text": kwargs["text"], + "state": kwargs["state"], + } + + +def get_checkbox_kwargs(checked=False, state="normal", redraw=True, check_function=None, text="", **kwargs): + if kwargs: + show_kwargs_warning(kwargs, "get_checkbox_kwargs") + return { + "checked": checked, + "state": state, + "redraw": redraw, + "check_function": check_function, + "text": text, + } + + +def get_checkbox_dict(**kwargs): + return { + "check_function": kwargs["check_function"], + "state": kwargs["state"], + "text": kwargs["text"], + } + + +def is_iterable(o): + if isinstance(o, str): + return False + try: + iter(o) + return True + except Exception: + return False + + +def num2alpha(n): + s = "" + n += 1 + while n > 0: + n, r = divmod(n - 1, 26) + s = chr(65 + r) + s + return s + + +def get_n2a(n=0, _type="numbers"): + if _type == "letters": + return num2alpha(n) + elif _type == "numbers": + return f"{n + 1}" + else: + return f"{num2alpha(n)} {n + 1}" + + +def get_index_of_gap_in_sorted_integer_seq_forward(seq, start=0): + prevn = seq[start] + for idx, n in enumerate(islice(seq, start + 1, None), start + 1): + if n != prevn + 1: + return idx + prevn = n + return None + + +def get_index_of_gap_in_sorted_integer_seq_reverse(seq, start=0): + prevn = seq[start] + for idx, n in zip(range(start, -1, -1), reversed(seq[:start])): + if n != prevn - 1: + return idx + prevn = n + return None + + +def get_seq_without_gaps_at_index(seq, position): + start_idx = bisect.bisect_left(seq, position) + forward_gap = get_index_of_gap_in_sorted_integer_seq_forward(seq, start_idx) + reverse_gap = get_index_of_gap_in_sorted_integer_seq_reverse(seq, start_idx) + if forward_gap is not None: + seq[:] = seq[:forward_gap] + if reverse_gap is not None: + seq[:] = seq[reverse_gap:] + return seq diff --git a/thirdparty/tksheet/_tksheet_row_index.py b/thirdparty/tksheet/_tksheet_row_index.py new file mode 100644 index 0000000..26e05fe --- /dev/null +++ b/thirdparty/tksheet/_tksheet_row_index.py @@ -0,0 +1,2363 @@ +from __future__ import annotations + +import pickle +import tkinter as tk +import zlib +from collections import defaultdict +from itertools import accumulate, chain, cycle, islice +from math import ceil, floor + +from ._tksheet_formatters import ( + is_bool_like, + try_to_bool, +) +from ._tksheet_other_classes import ( + BeginDragDropEvent, + DraggedRowColumn, + DropDownModifiedEvent, + EditHeaderEvent, + EditIndexEvent, + EndDragDropEvent, + ResizeEvent, + SelectionBoxEvent, + SelectRowEvent, + TextEditor, + get_checkbox_dict, + get_dropdown_dict, + get_n2a, + get_seq_without_gaps_at_index, + num2alpha, +) +from ._tksheet_vars import ( + USER_OS, + Color_Map_, + rc_binding, + symbols_set, +) + + +class RowIndex(tk.Canvas): + def __init__(self, *args, **kwargs): + tk.Canvas.__init__( + self, + kwargs["parentframe"], + background=kwargs["index_bg"], + highlightthickness=0, + ) + self.parentframe = kwargs["parentframe"] + self.MT = None # is set from within MainTable() __init__ + self.CH = None # is set from within MainTable() __init__ + self.TL = None # is set from within TopLeftRectangle() __init__ + self.popup_menu_loc = None + self.extra_begin_edit_cell_func = None + self.extra_end_edit_cell_func = None + self.text_editor = None + self.text_editor_id = None + self.text_editor_loc = None + self.b1_pressed_loc = None + self.existing_dropdown_canvas_id = None + self.existing_dropdown_window = None + self.closed_dropdown = None + self.centre_alignment_text_mod_indexes = (slice(1, None), slice(None, -1)) + self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) + self.being_drawn_rect = None + self.extra_motion_func = None + self.extra_b1_press_func = None + self.extra_b1_motion_func = None + self.extra_b1_release_func = None + self.extra_rc_func = None + self.selection_binding_func = None + self.shift_selection_binding_func = None + self.ctrl_selection_binding_func = None + self.drag_selection_binding_func = None + self.ri_extra_begin_drag_drop_func = None + self.ri_extra_end_drag_drop_func = None + self.extra_double_b1_func = None + self.row_height_resize_func = None + self.new_row_width = 0 + self.cell_options = {} + self.options = {} + self.drag_and_drop_enabled = False + self.dragged_row = None + self.width_resizing_enabled = False + self.height_resizing_enabled = False + self.double_click_resizing_enabled = False + self.row_selection_enabled = False + self.rc_insert_row_enabled = False + self.rc_delete_row_enabled = False + self.edit_cell_enabled = False + self.visible_row_dividers = {} + self.row_width_resize_bbox = tuple() + self.rsz_w = None + self.rsz_h = None + self.currently_resizing_width = False + self.currently_resizing_height = False + self.ri_rc_popup_menu = None + + self.disp_text = {} + self.disp_high = {} + self.disp_grid = {} + self.disp_fill_sels = {} + self.disp_bord_sels = {} + self.disp_resize_lines = {} + self.disp_dropdown = {} + self.disp_checkbox = {} + self.hidd_text = {} + self.hidd_high = {} + self.hidd_grid = {} + self.hidd_fill_sels = {} + self.hidd_bord_sels = {} + self.hidd_resize_lines = {} + self.hidd_dropdown = {} + self.hidd_checkbox = {} + + self.row_drag_and_drop_perform = kwargs["row_drag_and_drop_perform"] + self.index_fg = kwargs["index_fg"] + self.index_grid_fg = kwargs["index_grid_fg"] + self.index_border_fg = kwargs["index_border_fg"] + self.index_selected_cells_bg = kwargs["index_selected_cells_bg"] + self.index_selected_cells_fg = kwargs["index_selected_cells_fg"] + self.index_selected_rows_bg = kwargs["index_selected_rows_bg"] + self.index_selected_rows_fg = kwargs["index_selected_rows_fg"] + self.index_hidden_rows_expander_bg = kwargs["index_hidden_rows_expander_bg"] + self.index_bg = kwargs["index_bg"] + self.drag_and_drop_bg = kwargs["drag_and_drop_bg"] + self.resizing_line_fg = kwargs["resizing_line_fg"] + self.align = kwargs["row_index_align"] + self.show_default_index_for_empty = kwargs["show_default_index_for_empty"] + self.auto_resize_width = kwargs["auto_resize_width"] + self.default_index = kwargs["default_row_index"].lower() + self.basic_bindings() + + def basic_bindings(self, enable=True): + if enable: + self.bind("", self.mouse_motion) + self.bind("", self.b1_press) + self.bind("", self.b1_motion) + self.bind("", self.b1_release) + self.bind("", self.double_b1) + self.bind(rc_binding, self.rc) + else: + self.unbind("") + self.unbind("") + self.unbind("") + self.unbind("") + self.unbind("") + self.unbind(rc_binding) + + def set_width(self, new_width, set_TL=False): + self.current_width = new_width + try: + self.config(width=new_width) + except Exception: + return + if set_TL: + self.TL.set_dimensions(new_w=new_width) + + def enable_bindings(self, binding): + if binding == "row_width_resize": + self.width_resizing_enabled = True + elif binding == "row_height_resize": + self.height_resizing_enabled = True + elif binding == "double_click_row_resize": + self.double_click_resizing_enabled = True + elif binding == "row_select": + self.row_selection_enabled = True + elif binding == "drag_and_drop": + self.drag_and_drop_enabled = True + + def disable_bindings(self, binding): + if binding == "row_width_resize": + self.width_resizing_enabled = False + elif binding == "row_height_resize": + self.height_resizing_enabled = False + elif binding == "double_click_row_resize": + self.double_click_resizing_enabled = False + elif binding == "row_select": + self.row_selection_enabled = False + elif binding == "drag_and_drop": + self.drag_and_drop_enabled = False + + def check_mouse_position_height_resizers(self, x, y): + for r, (x1, y1, x2, y2) in self.visible_row_dividers.items(): + if x >= x1 and y >= y1 and x <= x2 and y <= y2: + return r + + def rc(self, event): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + popup_menu = None + if self.MT.identify_row(y=event.y, allow_end=False) is None: + self.MT.deselect("all") + if self.MT.rc_popup_menus_enabled: + popup_menu = self.MT.empty_rc_popup_menu + elif self.row_selection_enabled and not self.currently_resizing_width and not self.currently_resizing_height: + r = self.MT.identify_row(y=event.y) + if r < len(self.MT.row_positions) - 1: + if self.MT.row_selected(r): + if self.MT.rc_popup_menus_enabled: + popup_menu = self.ri_rc_popup_menu + else: + if self.MT.single_selection_enabled and self.MT.rc_select_enabled: + self.select_row(r, redraw=True) + elif self.MT.toggle_selection_enabled and self.MT.rc_select_enabled: + self.toggle_select_row(r, redraw=True) + if self.MT.rc_popup_menus_enabled: + popup_menu = self.ri_rc_popup_menu + if self.extra_rc_func is not None: + self.extra_rc_func(event) + if popup_menu is not None: + self.popup_menu_loc = r + popup_menu.tk_popup(event.x_root, event.y_root) + + def ctrl_b1_press(self, event=None): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + if ( + (self.drag_and_drop_enabled or self.row_selection_enabled) + and self.MT.ctrl_select_enabled + and self.rsz_h is None + and self.rsz_w is None + ): + r = self.MT.identify_row(y=event.y) + if r < len(self.MT.row_positions) - 1: + r_selected = self.MT.row_selected(r) + if not r_selected and self.row_selection_enabled: + self.add_selection(r, set_as_current=True) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.ctrl_selection_binding_func is not None: + self.ctrl_selection_binding_func(SelectionBoxEvent("ctrl_select_rows", (r, r + 1))) + elif r_selected: + self.dragged_row = DraggedRowColumn( + dragged=r, + to_move=get_seq_without_gaps_at_index(sorted(self.MT.get_selected_rows()), r), + ) + elif not self.MT.ctrl_select_enabled: + self.b1_press(event) + + def ctrl_shift_b1_press(self, event): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + y = event.y + r = self.MT.identify_row(y=y) + if ( + (self.drag_and_drop_enabled or self.row_selection_enabled) + and self.MT.ctrl_select_enabled + and self.rsz_h is None + and self.rsz_w is None + ): + if r < len(self.MT.row_positions) - 1: + r_selected = self.MT.row_selected(r) + if not r_selected and self.row_selection_enabled: + currently_selected = self.MT.currently_selected() + if currently_selected and currently_selected.type_ == "row": + min_r = int(currently_selected.row) + if r > min_r: + self.MT.create_selected(min_r, 0, r + 1, len(self.MT.col_positions) - 1, "rows") + func_event = tuple(range(min_r, r + 1)) + elif r < min_r: + self.MT.create_selected(r, 0, min_r + 1, len(self.MT.col_positions) - 1, "rows") + func_event = tuple(range(r, min_r + 1)) + else: + self.add_selection(r, set_as_current=True) + func_event = (r,) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.ctrl_selection_binding_func is not None: + self.ctrl_selection_binding_func(SelectionBoxEvent("ctrl_select_rows", func_event)) + elif r_selected: + self.dragged_row = DraggedRowColumn( + dragged=r, + to_move=get_seq_without_gaps_at_index(sorted(self.MT.get_selected_rows()), r), + ) + elif not self.MT.ctrl_select_enabled: + self.shift_b1_press(event) + + def shift_b1_press(self, event): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + y = event.y + r = self.MT.identify_row(y=y) + if (self.drag_and_drop_enabled or self.row_selection_enabled) and self.rsz_h is None and self.rsz_w is None: + if r < len(self.MT.row_positions) - 1: + r_selected = self.MT.row_selected(r) + if not r_selected and self.row_selection_enabled: + currently_selected = self.MT.currently_selected() + if currently_selected and currently_selected.type_ == "row": + min_r = int(currently_selected.row) + self.MT.delete_selection_rects(delete_current=False) + if r > min_r: + self.MT.create_selected(min_r, 0, r + 1, len(self.MT.col_positions) - 1, "rows") + func_event = tuple(range(min_r, r + 1)) + elif r < min_r: + self.MT.create_selected(r, 0, min_r + 1, len(self.MT.col_positions) - 1, "rows") + func_event = tuple(range(r, min_r + 1)) + else: + self.select_row(r) + func_event = (r,) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.shift_selection_binding_func is not None: + self.shift_selection_binding_func(SelectionBoxEvent("shift_select_rows", func_event)) + elif r_selected: + self.dragged_row = DraggedRowColumn( + dragged=r, + to_move=get_seq_without_gaps_at_index(sorted(self.MT.get_selected_rows()), r), + ) + + def create_resize_line(self, x1, y1, x2, y2, width, fill, tag): + if self.hidd_resize_lines: + t, sh = self.hidd_resize_lines.popitem() + self.coords(t, x1, y1, x2, y2) + if sh: + self.itemconfig(t, width=width, fill=fill, tag=tag) + else: + self.itemconfig(t, width=width, fill=fill, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_line(x1, y1, x2, y2, width=width, fill=fill, tag=tag) + self.disp_resize_lines[t] = True + + def delete_resize_lines(self): + self.hidd_resize_lines.update(self.disp_resize_lines) + self.disp_resize_lines = {} + for t, sh in self.hidd_resize_lines.items(): + if sh: + self.itemconfig(t, state="hidden") + self.hidd_resize_lines[t] = False + + def mouse_motion(self, event): + if not self.currently_resizing_height and not self.currently_resizing_width: + x = self.canvasx(event.x) + y = self.canvasy(event.y) + mouse_over_resize = False + mouse_over_selected = False + if self.height_resizing_enabled and not mouse_over_resize: + r = self.check_mouse_position_height_resizers(x, y) + if r is not None: + self.config(cursor="sb_v_double_arrow") + self.rsz_h = r + mouse_over_resize = True + else: + self.rsz_h = None + if self.width_resizing_enabled and not mouse_over_resize: + try: + x1, y1, x2, y2 = ( + self.row_width_resize_bbox[0], + self.row_width_resize_bbox[1], + self.row_width_resize_bbox[2], + self.row_width_resize_bbox[3], + ) + if x >= x1 and y >= y1 and x <= x2 and y <= y2: + self.config(cursor="sb_h_double_arrow") + self.rsz_w = True + mouse_over_resize = True + else: + self.rsz_w = None + except Exception: + self.rsz_w = None + if not mouse_over_resize: + if self.MT.row_selected(self.MT.identify_row(event, allow_end=False)): + self.config(cursor="hand2") + mouse_over_selected = True + if not mouse_over_resize and not mouse_over_selected: + self.MT.reset_mouse_motion_creations() + if self.extra_motion_func is not None: + self.extra_motion_func(event) + + def double_b1(self, event=None): + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.focus_set() + if ( + self.double_click_resizing_enabled + and self.height_resizing_enabled + and self.rsz_h is not None + and not self.currently_resizing_height + ): + row = self.rsz_h - 1 + old_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] + new_height = self.set_row_height(row) + self.MT.allow_auto_resize_rows = False + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.row_height_resize_func is not None and old_height != new_height: + self.row_height_resize_func(ResizeEvent("row_height_resize", row, old_height, new_height)) + elif self.width_resizing_enabled and self.rsz_h is None and self.rsz_w: + self.set_width_of_index_to_text() + elif self.row_selection_enabled and self.rsz_h is None and self.rsz_w is None: + r = self.MT.identify_row(y=event.y) + if r < len(self.MT.row_positions) - 1: + if self.MT.single_selection_enabled: + self.select_row(r, redraw=True) + elif self.MT.toggle_selection_enabled: + self.toggle_select_row(r, redraw=True) + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + if ( + self.get_cell_kwargs(datarn, key="dropdown") + or self.get_cell_kwargs(datarn, key="checkbox") + or self.edit_cell_enabled + ): + self.open_cell(event) + self.rsz_h = None + self.mouse_motion(event) + if self.extra_double_b1_func is not None: + self.extra_double_b1_func(event) + + def b1_press(self, event=None): + self.MT.unbind("") + self.focus_set() + self.closed_dropdown = self.mouseclick_outside_editor_or_dropdown_all_canvases() + x = self.canvasx(event.x) + y = self.canvasy(event.y) + r = self.MT.identify_row(y=event.y) + self.b1_pressed_loc = r + if self.check_mouse_position_height_resizers(x, y) is None: + self.rsz_h = None + if ( + not x >= self.row_width_resize_bbox[0] + and y >= self.row_width_resize_bbox[1] + and x <= self.row_width_resize_bbox[2] + and y <= self.row_width_resize_bbox[3] + ): + self.rsz_w = None + if self.height_resizing_enabled and self.rsz_h is not None: + self.currently_resizing_height = True + y = self.MT.row_positions[self.rsz_h] + line2y = self.MT.row_positions[self.rsz_h - 1] + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + self.create_resize_line( + 0, + y, + self.current_width, + y, + width=1, + fill=self.resizing_line_fg, + tag="rhl", + ) + self.MT.create_resize_line(x1, y, x2, y, width=1, fill=self.resizing_line_fg, tag="rhl") + self.create_resize_line( + 0, + line2y, + self.current_width, + line2y, + width=1, + fill=self.resizing_line_fg, + tag="rhl2", + ) + self.MT.create_resize_line(x1, line2y, x2, line2y, width=1, fill=self.resizing_line_fg, tag="rhl2") + elif self.width_resizing_enabled and self.rsz_h is None and self.rsz_w: + self.currently_resizing_width = True + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + x = int(event.x) + if x < self.MT.min_column_width: + x = int(self.MT.min_column_width) + self.new_row_width = x + self.create_resize_line(x, y1, x, y2, width=1, fill=self.resizing_line_fg, tag="rwl") + elif self.MT.identify_row(y=event.y, allow_end=False) is None: + self.MT.deselect("all") + elif self.row_selection_enabled and self.rsz_h is None and self.rsz_w is None: + r = self.MT.identify_row(y=event.y) + if r < len(self.MT.row_positions) - 1: + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + if ( + self.MT.row_selected(r) + and not self.event_over_dropdown(r, datarn, event, y) + and not self.event_over_checkbox(r, datarn, event, y) + ): + self.dragged_row = DraggedRowColumn( + dragged=r, + to_move=get_seq_without_gaps_at_index(sorted(self.MT.get_selected_rows()), r), + ) + else: + self.being_drawn_rect = ( + r, + 0, + r + 1, + len(self.MT.col_positions) - 1, + "rows", + ) + if self.MT.single_selection_enabled: + self.select_row(r, redraw=True) + elif self.MT.toggle_selection_enabled: + self.toggle_select_row(r, redraw=True) + if self.extra_b1_press_func is not None: + self.extra_b1_press_func(event) + + def b1_motion(self, event): + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + if self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: + y = self.canvasy(event.y) + size = y - self.MT.row_positions[self.rsz_h - 1] + if size >= self.MT.min_row_height and size < self.MT.max_row_height: + self.delete_all_resize_and_ctrl_lines(ctrl_lines=False) + line2y = self.MT.row_positions[self.rsz_h - 1] + self.create_resize_line( + 0, + y, + self.current_width, + y, + width=1, + fill=self.resizing_line_fg, + tag="rhl", + ) + self.MT.create_resize_line(x1, y, x2, y, width=1, fill=self.resizing_line_fg, tag="rhl") + self.create_resize_line( + 0, + line2y, + self.current_width, + line2y, + width=1, + fill=self.resizing_line_fg, + tag="rhl2", + ) + self.MT.create_resize_line( + x1, + line2y, + x2, + line2y, + width=1, + fill=self.resizing_line_fg, + tag="rhl2", + ) + elif self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: + evx = event.x + self.delete_all_resize_and_ctrl_lines(ctrl_lines=False) + if evx > self.current_width: + x = self.MT.canvasx(evx - self.current_width) + if evx > self.MT.max_index_width: + evx = int(self.MT.max_index_width) + x = self.MT.canvasx(evx - self.current_width) + self.new_row_width = evx + self.MT.create_resize_line(x, y1, x, y2, width=1, fill=self.resizing_line_fg, tag="rwl") + else: + x = evx + if x < self.MT.min_column_width: + x = int(self.MT.min_column_width) + self.new_row_width = x + self.create_resize_line(x, y1, x, y2, width=1, fill=self.resizing_line_fg, tag="rwl") + if ( + self.drag_and_drop_enabled + and self.row_selection_enabled + and self.rsz_h is None + and self.rsz_w is None + and self.dragged_row is not None + and self.MT.anything_selected(exclude_cells=True, exclude_columns=True) + ): + y = self.canvasy(event.y) + if y > 0 and y < self.MT.row_positions[-1]: + self.show_drag_and_drop_indicators( + self.drag_and_drop_motion(event), + x1, + x2, + self.dragged_row.to_move[0], + self.dragged_row.to_move[-1], + ) + elif ( + self.MT.drag_selection_enabled and self.row_selection_enabled and self.rsz_h is None and self.rsz_w is None + ): + need_redraw = False + end_row = self.MT.identify_row(y=event.y) + currently_selected = self.MT.currently_selected() + if end_row < len(self.MT.row_positions) - 1 and currently_selected: + if currently_selected.type_ == "row": + start_row = currently_selected.row + if end_row >= start_row: + rect = ( + start_row, + 0, + end_row + 1, + len(self.MT.col_positions) - 1, + "rows", + ) + func_event = tuple(range(start_row, end_row + 1)) + elif end_row < start_row: + rect = ( + end_row, + 0, + start_row + 1, + len(self.MT.col_positions) - 1, + "rows", + ) + func_event = tuple(range(end_row, start_row + 1)) + if self.being_drawn_rect != rect: + need_redraw = True + self.MT.delete_selection_rects(delete_current=False) + self.MT.create_selected(*rect) + self.being_drawn_rect = rect + if self.drag_selection_binding_func is not None: + self.drag_selection_binding_func(SelectionBoxEvent("drag_select_rows", func_event)) + if self.scroll_if_event_offscreen(event): + need_redraw = True + if need_redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True) + if self.extra_b1_motion_func is not None: + self.extra_b1_motion_func(event) + + def ctrl_b1_motion(self, event): + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + if ( + self.drag_and_drop_enabled + and self.row_selection_enabled + and self.rsz_h is None + and self.rsz_w is None + and self.dragged_row is not None + and self.MT.anything_selected(exclude_cells=True, exclude_columns=True) + ): + y = self.canvasy(event.y) + if y > 0 and y < self.MT.row_positions[-1]: + self.show_drag_and_drop_indicators( + self.drag_and_drop_motion(event), + x1, + x2, + self.dragged_row.to_move[0], + self.dragged_row.to_move[-1], + ) + elif ( + self.MT.ctrl_select_enabled + and self.row_selection_enabled + and self.MT.drag_selection_enabled + and self.rsz_h is None + and self.rsz_w is None + ): + need_redraw = False + end_row = self.MT.identify_row(y=event.y) + currently_selected = self.MT.currently_selected() + if end_row < len(self.MT.row_positions) - 1 and currently_selected: + if currently_selected.type_ == "row": + start_row = currently_selected.row + if end_row >= start_row: + rect = ( + start_row, + 0, + end_row + 1, + len(self.MT.col_positions) - 1, + "rows", + ) + func_event = tuple(range(start_row, end_row + 1)) + elif end_row < start_row: + rect = ( + end_row, + 0, + start_row + 1, + len(self.MT.col_positions) - 1, + "rows", + ) + func_event = tuple(range(end_row, start_row + 1)) + if self.being_drawn_rect != rect: + need_redraw = True + if self.being_drawn_rect is not None: + self.MT.delete_selected(*self.being_drawn_rect) + self.MT.create_selected(*rect) + self.being_drawn_rect = rect + if self.drag_selection_binding_func is not None: + self.drag_selection_binding_func(SelectionBoxEvent("drag_select_rows", func_event)) + if self.scroll_if_event_offscreen(event): + need_redraw = True + if need_redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True) + elif not self.MT.ctrl_select_enabled: + self.b1_motion(event) + + def drag_and_drop_motion(self, event): + y = event.y + hend = self.winfo_height() + ycheck = self.yview() + if y >= hend - 0 and len(ycheck) > 1 and ycheck[1] < 1: + if y >= hend + 15: + self.MT.yview_scroll(2, "units") + self.yview_scroll(2, "units") + else: + self.MT.yview_scroll(1, "units") + self.yview_scroll(1, "units") + self.fix_yview() + self.MT.y_move_synced_scrolls("moveto", self.MT.yview()[0]) + self.MT.main_table_redraw_grid_and_text(redraw_row_index=True) + elif y <= 0 and len(ycheck) > 1 and ycheck[0] > 0: + if y >= -15: + self.MT.yview_scroll(-1, "units") + self.yview_scroll(-1, "units") + else: + self.MT.yview_scroll(-2, "units") + self.yview_scroll(-2, "units") + self.fix_yview() + self.MT.y_move_synced_scrolls("moveto", self.MT.yview()[0]) + self.MT.main_table_redraw_grid_and_text(redraw_row_index=True) + row = self.MT.identify_row(y=event.y) + if row >= self.dragged_row.to_move[0] and row <= self.dragged_row.to_move[-1]: + ypos = self.MT.row_positions[self.dragged_row.to_move[0]] + else: + if row < self.dragged_row.to_move[0]: + ypos = self.MT.row_positions[row] + else: + ypos = ( + self.MT.row_positions[row + 1] + if len(self.MT.row_positions) - 1 > row + else self.MT.row_positions[row] + ) + return ypos + + def show_drag_and_drop_indicators(self, ypos, x1, x2, start_row, end_row): + self.delete_all_resize_and_ctrl_lines() + self.create_resize_line( + 0, + ypos, + self.current_width, + ypos, + width=3, + fill=self.drag_and_drop_bg, + tag="dd", + ) + self.MT.create_resize_line(x1, ypos, x2, ypos, width=3, fill=self.drag_and_drop_bg, tag="dd") + self.MT.show_ctrl_outline( + start_cell=(0, start_row), + end_cell=(len(self.MT.col_positions) - 1, end_row + 1), + dash=(), + outline=self.drag_and_drop_bg, + delete_on_timer=False, + ) + + def delete_all_resize_and_ctrl_lines(self, ctrl_lines=True): + self.delete_resize_lines() + self.MT.delete_resize_lines() + if ctrl_lines: + self.MT.delete_ctrl_outlines() + + def scroll_if_event_offscreen(self, event): + ycheck = self.yview() + need_redraw = False + if event.y > self.winfo_height() and len(ycheck) > 1 and ycheck[1] < 1: + try: + self.MT.yview_scroll(1, "units") + self.yview_scroll(1, "units") + except Exception: + pass + self.fix_yview() + self.MT.y_move_synced_scrolls("moveto", self.MT.yview()[0]) + need_redraw = True + elif event.y < 0 and self.canvasy(self.winfo_height()) > 0 and ycheck and ycheck[0] > 0: + try: + self.yview_scroll(-1, "units") + self.MT.yview_scroll(-1, "units") + except Exception: + pass + self.fix_yview() + self.MT.y_move_synced_scrolls("moveto", self.MT.yview()[0]) + need_redraw = True + return need_redraw + + def fix_yview(self): + ycheck = self.yview() + if ycheck and ycheck[0] < 0: + self.MT.set_yviews("moveto", 0) + if len(ycheck) > 1 and ycheck[1] > 1: + self.MT.set_yviews("moveto", 1) + + def event_over_dropdown(self, r, datarn, event, canvasy): + if ( + canvasy < self.MT.row_positions[r] + self.MT.txt_h + and self.get_cell_kwargs(datarn, key="dropdown") + and event.x > self.current_width - self.MT.txt_h - 4 + ): + return True + return False + + def event_over_checkbox(self, r, datarn, event, canvasy): + if ( + canvasy < self.MT.row_positions[r] + self.MT.txt_h + and self.get_cell_kwargs(datarn, key="checkbox") + and event.x < self.MT.txt_h + 4 + ): + return True + return False + + def b1_release(self, event=None): + if self.being_drawn_rect is not None: + self.MT.delete_selected(*self.being_drawn_rect) + to_sel = tuple(self.being_drawn_rect) + self.being_drawn_rect = None + self.MT.create_selected(*to_sel) + self.MT.bind("", self.MT.mousewheel) + if self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: + self.currently_resizing_height = False + new_row_pos = int(self.coords("rhl")[1]) + self.delete_all_resize_and_ctrl_lines(ctrl_lines=False) + old_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] + size = new_row_pos - self.MT.row_positions[self.rsz_h - 1] + if size < self.MT.min_row_height: + new_row_pos = ceil(self.MT.row_positions[self.rsz_h - 1] + self.MT.min_row_height) + elif size > self.MT.max_row_height: + new_row_pos = floor(self.MT.row_positions[self.rsz_h - 1] + self.MT.max_row_height) + increment = new_row_pos - self.MT.row_positions[self.rsz_h] + self.MT.row_positions[self.rsz_h + 1 :] = [ + e + increment for e in islice(self.MT.row_positions, self.rsz_h + 1, len(self.MT.row_positions)) + ] + self.MT.row_positions[self.rsz_h] = new_row_pos + self.MT.allow_auto_resize_rows = False + new_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] + self.MT.recreate_all_selection_boxes() + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.row_height_resize_func is not None and old_height != new_height: + self.row_height_resize_func(ResizeEvent("row_height_resize", self.rsz_h - 1, old_height, new_height)) + elif self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: + self.currently_resizing_width = False + self.delete_all_resize_and_ctrl_lines(ctrl_lines=False) + self.set_width(self.new_row_width, set_TL=True) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if ( + self.drag_and_drop_enabled + and self.MT.anything_selected(exclude_cells=True, exclude_columns=True) + and self.row_selection_enabled + and self.rsz_h is None + and self.rsz_w is None + and self.dragged_row is not None + ): + self.delete_all_resize_and_ctrl_lines() + y = event.y + r = self.MT.identify_row(y=y) + orig_selected = self.dragged_row.to_move + if ( + r != self.dragged_row + and r is not None + and (r < self.dragged_row.to_move[0] or r > self.dragged_row.to_move[-1]) + and len(orig_selected) != (len(self.MT.row_positions) - 1) + ): + rm1start = orig_selected[0] + totalrows = len(orig_selected) + extra_func_success = True + if r >= len(self.MT.row_positions) - 1: + r -= 1 + if self.ri_extra_begin_drag_drop_func is not None: + try: + self.ri_extra_begin_drag_drop_func( + BeginDragDropEvent( + "begin_row_index_drag_drop", + tuple(orig_selected), + int(r), + ) + ) + except Exception: + extra_func_success = False + if extra_func_success: + new_selected, dispset = self.MT.move_rows_adjust_options_dict( + r, rm1start, totalrows, move_data=self.row_drag_and_drop_perform + ) + if self.MT.undo_enabled: + self.MT.undo_storage.append( + zlib.compress(pickle.dumps(("move_rows", orig_selected, new_selected))) + ) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.ri_extra_end_drag_drop_func is not None: + self.ri_extra_end_drag_drop_func( + EndDragDropEvent( + "end_row_index_drag_drop", + orig_selected, + new_selected, + int(r), + ) + ) + self.parentframe.emit_event("<>") + elif self.b1_pressed_loc is not None and self.rsz_w is None and self.rsz_h is None: + r = self.MT.identify_row(y=event.y) + if ( + r is not None + and r < len(self.MT.row_positions) - 1 + and r == self.b1_pressed_loc + and self.b1_pressed_loc != self.closed_dropdown + ): + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + canvasy = self.canvasy(event.y) + if self.event_over_dropdown(r, datarn, event, canvasy) or self.event_over_checkbox( + r, datarn, event, canvasy + ): + self.open_cell(event) + else: + self.mouseclick_outside_editor_or_dropdown_all_canvases() + self.b1_pressed_loc = None + self.closed_dropdown = None + self.dragged_row = None + self.currently_resizing_width = False + self.currently_resizing_height = False + self.rsz_w = None + self.rsz_h = None + self.mouse_motion(event) + if self.extra_b1_release_func is not None: + self.extra_b1_release_func(event) + + def readonly_index(self, rows=[], readonly=True): + if isinstance(rows, int): + rows_ = [rows] + else: + rows_ = rows + if not readonly: + for r in rows_: + if r in self.cell_options and "readonly" in self.cell_options[r]: + del self.cell_options[r]["readonly"] + else: + for r in rows_: + if r not in self.cell_options: + self.cell_options[r] = {} + self.cell_options[r]["readonly"] = True + + def toggle_select_row( + self, + row, + add_selection=True, + redraw=True, + run_binding_func=True, + set_as_current=True, + ): + if add_selection: + if self.MT.row_selected(row): + self.MT.deselect(r=row, redraw=redraw) + else: + self.add_selection( + r=row, + redraw=redraw, + run_binding_func=run_binding_func, + set_as_current=set_as_current, + ) + else: + if self.MT.row_selected(row): + self.MT.deselect(r=row, redraw=redraw) + else: + self.select_row(row, redraw=redraw) + + def select_row(self, r, redraw=False): + self.MT.delete_selection_rects() + self.MT.create_selected(r, 0, r + 1, len(self.MT.col_positions) - 1, "rows") + self.MT.set_currently_selected(r, 0, type_="row") + if redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.selection_binding_func is not None: + self.selection_binding_func(SelectRowEvent("select_row", int(r))) + + def add_selection(self, r, redraw=False, run_binding_func=True, set_as_current=True): + if set_as_current: + self.MT.set_currently_selected(r, 0, type_="row") + self.MT.create_selected(r, 0, r + 1, len(self.MT.col_positions) - 1, "rows") + if redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True) + if self.selection_binding_func is not None and run_binding_func: + self.selection_binding_func(("select_row", r)) + + def get_cell_dimensions(self, datarn): + txt = self.get_valid_cell_data_as_str(datarn, fix=False) + if txt: + self.MT.txt_measure_canvas.itemconfig(self.MT.txt_measure_canvas_text, text=txt, font=self.MT.index_font) + b = self.MT.txt_measure_canvas.bbox(self.MT.txt_measure_canvas_text) + w = b[2] - b[0] + 7 + h = b[3] - b[1] + 5 + else: + w = self.MT.default_index_width + h = self.MT.min_row_height + if self.get_cell_kwargs(datarn, key="dropdown") or self.get_cell_kwargs(datarn, key="checkbox"): + return w + self.MT.txt_h, h + return w, h + + def set_row_height( + self, + row, + height=None, + only_set_if_too_small=False, + recreate=True, + return_new_height=False, + displayed_only=False, + ): + r_norm = row + 1 + r_extra = row + 2 + min_rh = self.MT.min_row_height + datarn = row if self.MT.all_rows_displayed else self.MT.displayed_rows[row] + if height is None: + if self.MT.all_columns_displayed: + if displayed_only: + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + start_col, end_col = self.MT.get_visible_columns(x1, x2) + else: + start_col, end_col = ( + 0, + len(self.MT.data[row]) if self.MT.data else 0, + ) + iterable = range(start_col, end_col) + else: + if displayed_only: + x1, y1, x2, y2 = self.MT.get_canvas_visible_area() + start_col, end_col = self.MT.get_visible_columns(x1, x2) + else: + start_col, end_col = 0, len(self.MT.displayed_columns) + iterable = self.MT.displayed_columns[start_col:end_col] + new_height = int(min_rh) + w_, h = self.get_cell_dimensions(datarn) + if h < min_rh: + h = int(min_rh) + elif h > self.MT.max_row_height: + h = int(self.MT.max_row_height) + if h > new_height: + new_height = h + if self.MT.data: + for datacn in iterable: + txt = self.MT.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True) + if txt: + h = self.MT.get_txt_h(txt) + 5 + else: + h = min_rh + if h < min_rh: + h = int(min_rh) + elif h > self.MT.max_row_height: + h = int(self.MT.max_row_height) + if h > new_height: + new_height = h + else: + new_height = int(height) + if new_height < min_rh: + new_height = int(min_rh) + elif new_height > self.MT.max_row_height: + new_height = int(self.MT.max_row_height) + if only_set_if_too_small and new_height <= self.MT.row_positions[row + 1] - self.MT.row_positions[row]: + return self.MT.row_positions[row + 1] - self.MT.row_positions[row] + if not return_new_height: + new_row_pos = self.MT.row_positions[row] + new_height + increment = new_row_pos - self.MT.row_positions[r_norm] + self.MT.row_positions[r_extra:] = [ + e + increment for e in islice(self.MT.row_positions, r_extra, len(self.MT.row_positions)) + ] + self.MT.row_positions[r_norm] = new_row_pos + if recreate: + self.MT.recreate_all_selection_boxes() + return new_height + + def set_width_of_index_to_text(self, text=None): + if ( + text is None + and not self.MT._row_index + and isinstance(self.MT._row_index, list) + or isinstance(self.MT._row_index, int) + and self.MT._row_index >= len(self.MT.data) + ): + return + qconf = self.MT.txt_measure_canvas.itemconfig + qbbox = self.MT.txt_measure_canvas.bbox + qtxtm = self.MT.txt_measure_canvas_text + new_width = int(self.MT.min_column_width) + self.fix_index() + if text is not None: + if text: + qconf(qtxtm, text=text) + b = qbbox(qtxtm) + w = b[2] - b[0] + 10 + if w > new_width: + new_width = w + else: + w = self.MT.default_index_width + else: + if self.MT.all_rows_displayed: + if isinstance(self.MT._row_index, list): + iterable = range(len(self.MT._row_index)) + else: + iterable = range(len(self.MT.data)) + else: + iterable = self.MT.displayed_rows + if isinstance(self.MT._row_index, list): + for datarn in iterable: + w, h_ = self.get_cell_dimensions(datarn) + if w < self.MT.min_column_width: + w = int(self.MT.min_column_width) + elif w > self.MT.max_index_width: + w = int(self.MT.max_index_width) + if self.get_cell_kwargs(datarn, key="checkbox"): + w += self.MT.txt_h + 6 + elif self.get_cell_kwargs(datarn, key="dropdown"): + w += self.MT.txt_h + 4 + if w > new_width: + new_width = w + elif isinstance(self.MT._row_index, int): + datacn = self.MT._row_index + for datarn in iterable: + txt = self.MT.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True) + if txt: + qconf(qtxtm, text=txt) + b = qbbox(qtxtm) + w = b[2] - b[0] + 10 + else: + w = self.MT.default_index_width + if w < self.MT.min_column_width: + w = int(self.MT.min_column_width) + elif w > self.MT.max_index_width: + w = int(self.MT.max_index_width) + if w > new_width: + new_width = w + if new_width == self.MT.min_column_width: + new_width = self.MT.min_column_width + 10 + self.set_width(new_width, set_TL=True) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + + def set_height_of_all_rows(self, height=None, only_set_if_too_small=False, recreate=True): + if height is None: + self.MT.row_positions = list( + accumulate( + chain( + [0], + ( + self.set_row_height( + rn, + only_set_if_too_small=only_set_if_too_small, + recreate=False, + return_new_height=True, + ) + for rn in range(len(self.MT.data)) + ), + ) + ) + ) + else: + self.MT.row_positions = list(accumulate(chain([0], (height for r in range(len(self.MT.data)))))) + if recreate: + self.MT.recreate_all_selection_boxes() + + def align_cells(self, rows=[], align="global"): + if isinstance(rows, int): + rows = [rows] + else: + rows = rows + if align == "global": + for r in rows: + if r in self.cell_options and "align" in self.cell_options[r]: + del self.cell_options[r]["align"] + else: + for r in rows: + if r not in self.cell_options: + self.cell_options[r] = {} + self.cell_options[r]["align"] = align + + def auto_set_index_width(self, end_row): + if not self.MT._row_index and not isinstance(self.MT._row_index, int) and self.auto_resize_width: + if self.default_index == "letters": + new_w = ( + self.MT.get_txt_w( + f"{num2alpha(end_row)}", + font=self.MT.index_font, + ) + + 20 + ) + if self.current_width - new_w > 15 or new_w - self.current_width > 5: + self.set_width(new_w, set_TL=True) + return True + elif self.default_index == "numbers": + new_w = ( + self.MT.get_txt_w( + f"{end_row}", + font=self.MT.index_font, + ) + + 20 + ) + if self.current_width - new_w > 15 or new_w - self.current_width > 5: + self.set_width(new_w, set_TL=True) + return True + elif self.default_index == "both": + new_w = ( + self.MT.get_txt_w( + f"{end_row + 1} {num2alpha(end_row)}", + font=self.MT.index_font, + ) + + 20 + ) + if self.current_width - new_w > 15 or new_w - self.current_width > 5: + self.set_width(new_w, set_TL=True) + return True + return False + + def redraw_highlight_get_text_fg(self, fr, sr, r, c_2, c_3, selections, datarn): + redrawn = False + kwargs = self.get_cell_kwargs(datarn, key="highlight") + if kwargs: + if kwargs[0] is not None: + c_1 = kwargs[0] if kwargs[0].startswith("#") else Color_Map_[kwargs[0]] + if "rows" in selections and r in selections["rows"]: + tf = ( + self.index_selected_rows_fg + if kwargs[1] is None or self.MT.display_selected_fg_over_highlights + else kwargs[1] + ) + if kwargs[0] is not None: + fill = ( + f"#{int((int(c_1[1:3], 16) + int(c_3[1:3], 16)) / 2):02X}" + + f"{int((int(c_1[3:5], 16) + int(c_3[3:5], 16)) / 2):02X}" + + f"{int((int(c_1[5:], 16) + int(c_3[5:], 16)) / 2):02X}" + ) + elif "cells" in selections and r in selections["cells"]: + tf = ( + self.index_selected_cells_fg + if kwargs[1] is None or self.MT.display_selected_fg_over_highlights + else kwargs[1] + ) + if kwargs[0] is not None: + fill = ( + f"#{int((int(c_1[1:3], 16) + int(c_2[1:3], 16)) / 2):02X}" + + f"{int((int(c_1[3:5], 16) + int(c_2[3:5], 16)) / 2):02X}" + + f"{int((int(c_1[5:], 16) + int(c_2[5:], 16)) / 2):02X}" + ) + else: + tf = self.index_fg if kwargs[1] is None else kwargs[1] + if kwargs[0] is not None: + fill = kwargs[0] + if kwargs[0] is not None: + redrawn = self.redraw_highlight( + 0, + fr + 1, + self.current_width - 1, + sr, + fill=fill, + outline=self.index_fg + if self.get_cell_kwargs(datarn, key="dropdown") and self.MT.show_dropdown_borders + else "", + tag="s", + ) + elif not kwargs: + if "rows" in selections and r in selections["rows"]: + tf = self.index_selected_rows_fg + elif "cells" in selections and r in selections["cells"]: + tf = self.index_selected_cells_fg + else: + tf = self.index_fg + return tf, redrawn + + def redraw_highlight(self, x1, y1, x2, y2, fill, outline, tag): + coords = (x1, y1, x2, y2) + if self.hidd_high: + iid, showing = self.hidd_high.popitem() + self.coords(iid, coords) + if showing: + self.itemconfig(iid, fill=fill, outline=outline) + else: + self.itemconfig(iid, fill=fill, outline=outline, tag=tag, state="normal") + else: + iid = self.create_rectangle(coords, fill=fill, outline=outline, tag=tag) + self.disp_high[iid] = True + return True + + def redraw_gridline(self, points, fill, width, tag): + if self.hidd_grid: + t, sh = self.hidd_grid.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=fill, width=width, tag=tag) + else: + self.itemconfig(t, fill=fill, width=width, tag=tag, state="normal") + self.disp_grid[t] = True + else: + self.disp_grid[self.create_line(points, fill=fill, width=width, tag=tag)] = True + + def redraw_dropdown( + self, + x1, + y1, + x2, + y2, + fill, + outline, + tag, + draw_outline=True, + draw_arrow=True, + dd_is_open=False, + ): + if draw_outline and self.MT.show_dropdown_borders: + self.redraw_highlight(x1 + 1, y1 + 1, x2, y2, fill="", outline=self.index_fg, tag=tag) + if draw_arrow: + topysub = floor(self.MT.half_txt_h / 2) + mid_y = y1 + floor(self.MT.min_row_height / 2) + if mid_y + topysub + 1 >= y1 + self.MT.txt_h - 1: + mid_y -= 1 + if mid_y - topysub + 2 <= y1 + 4 + topysub: + mid_y -= 1 + ty1 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 3 + ty2 = mid_y - topysub + 3 if dd_is_open else mid_y + topysub + 1 + ty3 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 3 + else: + ty1 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 2 + ty2 = mid_y - topysub + 2 if dd_is_open else mid_y + topysub + 1 + ty3 = mid_y + topysub + 1 if dd_is_open else mid_y - topysub + 2 + tx1 = x2 - self.MT.txt_h + 1 + tx2 = x2 - self.MT.half_txt_h - 1 + tx3 = x2 - 3 + if tx2 - tx1 > tx3 - tx2: + tx1 += (tx2 - tx1) - (tx3 - tx2) + elif tx2 - tx1 < tx3 - tx2: + tx1 -= (tx3 - tx2) - (tx2 - tx1) + points = (tx1, ty1, tx2, ty2, tx3, ty3) + if self.hidd_dropdown: + t, sh = self.hidd_dropdown.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=fill) + else: + self.itemconfig(t, fill=fill, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_line( + points, + fill=fill, + width=2, + capstyle=tk.ROUND, + joinstyle=tk.ROUND, + tag=tag, + ) + self.disp_dropdown[t] = True + + def redraw_checkbox(self, x1, y1, x2, y2, fill, outline, tag, draw_check=False): + points = self.MT.get_checkbox_points(x1, y1, x2, y2) + if self.hidd_checkbox: + t, sh = self.hidd_checkbox.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=outline, outline=fill) + else: + self.itemconfig(t, fill=outline, outline=fill, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_polygon(points, fill=outline, outline=fill, tag=tag, smooth=True) + self.disp_checkbox[t] = True + if draw_check: + # draw filled box + x1 = x1 + 4 + y1 = y1 + 4 + x2 = x2 - 3 + y2 = y2 - 3 + points = self.MT.get_checkbox_points(x1, y1, x2, y2, radius=4) + if self.hidd_checkbox: + t, sh = self.hidd_checkbox.popitem() + self.coords(t, points) + if sh: + self.itemconfig(t, fill=fill, outline=outline) + else: + self.itemconfig(t, fill=fill, outline=outline, tag=tag, state="normal") + self.lift(t) + else: + t = self.create_polygon(points, fill=fill, outline=outline, tag=tag, smooth=True) + self.disp_checkbox[t] = True + + def redraw_grid_and_text( + self, + last_row_line_pos, + scrollpos_top, + y_stop, + start_row, + end_row, + scrollpos_bot, + row_pos_exists, + ): + try: + self.configure( + scrollregion=( + 0, + 0, + self.current_width, + last_row_line_pos + self.MT.empty_vertical + 2, + ) + ) + except Exception: + return + self.hidd_text.update(self.disp_text) + self.disp_text = {} + self.hidd_high.update(self.disp_high) + self.disp_high = {} + self.hidd_grid.update(self.disp_grid) + self.disp_grid = {} + self.hidd_dropdown.update(self.disp_dropdown) + self.disp_dropdown = {} + self.hidd_checkbox.update(self.disp_checkbox) + self.disp_checkbox = {} + self.visible_row_dividers = {} + draw_y = self.MT.row_positions[start_row] + xend = self.current_width - 6 + self.row_width_resize_bbox = ( + self.current_width - 2, + scrollpos_top, + self.current_width, + scrollpos_bot, + ) + if (self.MT.show_horizontal_grid or self.height_resizing_enabled) and row_pos_exists: + points = [ + self.current_width - 1, + y_stop - 1, + self.current_width - 1, + scrollpos_top - 1, + -1, + scrollpos_top - 1, + ] + for r in range(start_row + 1, end_row): + draw_y = self.MT.row_positions[r] + if self.height_resizing_enabled: + self.visible_row_dividers[r] = (1, draw_y - 2, xend, draw_y + 2) + points.extend( + ( + -1, + draw_y, + self.current_width, + draw_y, + -1, + draw_y, + -1, + self.MT.row_positions[r + 1] if len(self.MT.row_positions) - 1 > r else draw_y, + ) + ) + self.redraw_gridline(points=points, fill=self.index_grid_fg, width=1, tag="h") + c_2 = ( + self.index_selected_cells_bg + if self.index_selected_cells_bg.startswith("#") + else Color_Map_[self.index_selected_cells_bg] + ) + c_3 = ( + self.index_selected_rows_bg + if self.index_selected_rows_bg.startswith("#") + else Color_Map_[self.index_selected_rows_bg] + ) + font = self.MT.index_font + selections = self.get_redraw_selections(start_row, end_row) + for r in range(start_row, end_row - 1): + rtopgridln = self.MT.row_positions[r] + rbotgridln = self.MT.row_positions[r + 1] + if rbotgridln - rtopgridln < self.MT.txt_h: + continue + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + fill, dd_drawn = self.redraw_highlight_get_text_fg(rtopgridln, rbotgridln, r, c_2, c_3, selections, datarn) + + if datarn in self.cell_options and "align" in self.cell_options[datarn]: + align = self.cell_options[datarn]["align"] + else: + align = self.align + dropdown_kwargs = self.get_cell_kwargs(datarn, key="dropdown") + if align == "w": + draw_x = 3 + if dropdown_kwargs: + mw = self.current_width - self.MT.txt_h - 2 + self.redraw_dropdown( + 0, + rtopgridln, + self.current_width - 1, + rbotgridln - 1, + fill=fill, + outline=fill, + tag="dd", + draw_outline=not dd_drawn, + draw_arrow=mw >= 5, + dd_is_open=dropdown_kwargs["window"] != "no dropdown open", + ) + else: + mw = self.current_width - 2 + + elif align == "e": + if dropdown_kwargs: + mw = self.current_width - self.MT.txt_h - 2 + draw_x = self.current_width - 5 - self.MT.txt_h + self.redraw_dropdown( + 0, + rtopgridln, + self.current_width - 1, + rbotgridln - 1, + fill=fill, + outline=fill, + tag="dd", + draw_outline=not dd_drawn, + draw_arrow=mw >= 5, + dd_is_open=dropdown_kwargs["window"] != "no dropdown open", + ) + else: + mw = self.current_width - 2 + draw_x = self.current_width - 3 + + elif align == "center": + if dropdown_kwargs: + mw = self.current_width - self.MT.txt_h - 2 + draw_x = ceil((self.current_width - self.MT.txt_h) / 2) + self.redraw_dropdown( + 0, + rtopgridln, + self.current_width - 1, + rbotgridln - 1, + fill=fill, + outline=fill, + tag="dd", + draw_outline=not dd_drawn, + draw_arrow=mw >= 5, + dd_is_open=dropdown_kwargs["window"] != "no dropdown open", + ) + else: + mw = self.current_width - 1 + draw_x = floor(self.current_width / 2) + checkbox_kwargs = self.get_cell_kwargs(datarn, key="checkbox") + if not dropdown_kwargs and checkbox_kwargs and mw > 2: + box_w = self.MT.txt_h + 1 + mw -= box_w + if align == "w": + draw_x += box_w + 1 + elif align == "center": + draw_x += ceil(box_w / 2) + 1 + mw -= 1 + else: + mw -= 3 + try: + draw_check = ( + self.MT._row_index[datarn] + if isinstance(self.MT._row_index, (list, tuple)) + else self.MT.data[datarn][self.MT._row_index] + ) + except Exception: + draw_check = False + self.redraw_checkbox( + 2, + rtopgridln + 2, + self.MT.txt_h + 3, + rtopgridln + self.MT.txt_h + 3, + fill=fill if checkbox_kwargs["state"] == "normal" else self.index_grid_fg, + outline="", + tag="cb", + draw_check=draw_check, + ) + lns = self.get_valid_cell_data_as_str(datarn, fix=False).split("\n") + if lns == [""]: + if self.show_default_index_for_empty: + lns = (get_n2a(datarn, self.default_index),) + else: + continue + draw_y = rtopgridln + self.MT.fl_ins + if mw > 5: + draw_y = rtopgridln + self.MT.fl_ins + start_ln = int((scrollpos_top - rtopgridln) / self.MT.xtra_lines_increment) + if start_ln < 0: + start_ln = 0 + draw_y += start_ln * self.MT.xtra_lines_increment + if draw_y + self.MT.half_txt_h - 1 <= rbotgridln and len(lns) > start_ln: + for txt in islice(lns, start_ln, None): + if self.hidd_text: + iid, showing = self.hidd_text.popitem() + self.coords(iid, draw_x, draw_y) + if showing: + self.itemconfig( + iid, + text=txt, + fill=fill, + font=font, + anchor=align, + ) + else: + self.itemconfig( + iid, + text=txt, + fill=fill, + font=font, + anchor=align, + state="normal", + ) + self.tag_raise(iid) + else: + iid = self.create_text( + draw_x, + draw_y, + text=txt, + fill=fill, + font=font, + anchor=align, + tag="t", + ) + self.disp_text[iid] = True + wd = self.bbox(iid) + wd = wd[2] - wd[0] + if wd > mw: + if align == "w" and dropdown_kwargs: + txt = txt[: int(len(txt) * (mw / wd))] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + while wd[2] - wd[0] > mw: + txt = txt[:-1] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + elif align == "e" and (dropdown_kwargs or checkbox_kwargs): + txt = txt[len(txt) - int(len(txt) * (mw / wd)) :] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + while wd[2] - wd[0] > mw: + txt = txt[1:] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + elif align == "center" and (dropdown_kwargs or checkbox_kwargs): + tmod = ceil((len(txt) - int(len(txt) * (mw / wd))) / 2) + txt = txt[tmod - 1 : -tmod] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + self.c_align_cyc = cycle(self.centre_alignment_text_mod_indexes) + while wd[2] - wd[0] > mw: + txt = txt[next(self.c_align_cyc)] + self.itemconfig(iid, text=txt) + wd = self.bbox(iid) + self.coords(iid, draw_x, draw_y) + draw_y += self.MT.xtra_lines_increment + if draw_y + self.MT.half_txt_h - 1 > rbotgridln: + break + for dct in (self.hidd_text, self.hidd_high, self.hidd_grid, self.hidd_dropdown, self.hidd_checkbox): + for iid, showing in dct.items(): + if showing: + self.itemconfig(iid, state="hidden") + dct[iid] = False + + def get_redraw_selections(self, startr, endr): + d = defaultdict(list) + for item in chain(self.find_withtag("cells"), self.find_withtag("rows")): + tags = self.gettags(item) + d[tags[0]].append(tuple(int(e) for e in tags[1].split("_") if e)) + d2 = {} + if "cells" in d: + d2["cells"] = {r for r in range(startr, endr) for r1, c1, r2, c2 in d["cells"] if r1 <= r and r2 > r} + if "rows" in d: + d2["rows"] = {r for r in range(startr, endr) for r1, c1, r2, c2 in d["rows"] if r1 <= r and r2 > r} + return d2 + + def open_cell(self, event=None, ignore_existing_editor=False): + if not self.MT.anything_selected() or (not ignore_existing_editor and self.text_editor_id is not None): + return + currently_selected = self.MT.currently_selected() + if not currently_selected: + return + r = int(currently_selected[0]) + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + if self.get_cell_kwargs(datarn, key="readonly"): + return + elif self.get_cell_kwargs(datarn, key="dropdown") or self.get_cell_kwargs(datarn, key="checkbox"): + if self.MT.event_opens_dropdown_or_checkbox(event): + if self.get_cell_kwargs(datarn, key="dropdown"): + self.open_dropdown_window(r, event=event) + elif self.get_cell_kwargs(datarn, key="checkbox"): + self.click_checkbox(r, datarn) + elif self.edit_cell_enabled: + self.open_text_editor(event=event, r=r, dropdown=False) + + # displayed indexes + def get_cell_align(self, r): + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + if datarn in self.cell_options and "align" in self.cell_options[datarn]: + align = self.cell_options[datarn]["align"] + else: + align = self.align + return align + + # r is displayed row + def open_text_editor( + self, + event=None, + r=0, + text=None, + state="normal", + see=True, + set_data_on_close=True, + binding=None, + dropdown=False, + ): + text = None + extra_func_key = "??" + if event is None or self.MT.event_opens_dropdown_or_checkbox(event): + if event is not None: + if hasattr(event, "keysym") and event.keysym == "Return": + extra_func_key = "Return" + elif hasattr(event, "keysym") and event.keysym == "F2": + extra_func_key = "F2" + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + text = self.get_cell_data(datarn, none_to_empty_str=True, redirect_int=True) + elif event is not None and ( + (hasattr(event, "keysym") and event.keysym == "BackSpace") or event.keycode in (8, 855638143) + ): + extra_func_key = "BackSpace" + text = "" + elif event is not None and ( + (hasattr(event, "char") and event.char.isalpha()) + or (hasattr(event, "char") and event.char.isdigit()) + or (hasattr(event, "char") and event.char in symbols_set) + ): + extra_func_key = event.char + text = event.char + else: + return False + self.text_editor_loc = r + if self.extra_begin_edit_cell_func is not None: + try: + text = self.extra_begin_edit_cell_func(EditIndexEvent(r, extra_func_key, text, "begin_edit_index")) + except Exception: + return False + if text is None: + return False + else: + text = text if isinstance(text, str) else f"{text}" + text = "" if text is None else text + if self.MT.cell_auto_resize_enabled: + self.set_row_height_run_binding(r) + + if r == self.text_editor_loc and self.text_editor is not None: + self.text_editor.set_text(self.text_editor.get() + "" if not isinstance(text, str) else text) + return + if self.text_editor is not None: + self.destroy_text_editor() + if see: + has_redrawn = self.MT.see(r=r, c=0, keep_yscroll=True, check_cell_visibility=True) + if not has_redrawn: + self.MT.refresh() + self.text_editor_loc = r + x = 0 + y = self.MT.row_positions[r] + 1 + w = self.current_width + 1 + h = self.MT.row_positions[r + 1] - y + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + if text is None: + text = self.get_cell_data(datarn, none_to_empty_str=True, redirect_int=True) + bg, fg = self.index_bg, self.index_fg + self.text_editor = TextEditor( + self, + text=text, + font=self.MT.index_font, + state=state, + width=w, + height=h, + border_color=self.MT.table_selected_cells_border_fg, + show_border=False, + bg=bg, + fg=fg, + popup_menu_font=self.MT.popup_menu_font, + popup_menu_fg=self.MT.popup_menu_fg, + popup_menu_bg=self.MT.popup_menu_bg, + popup_menu_highlight_bg=self.MT.popup_menu_highlight_bg, + popup_menu_highlight_fg=self.MT.popup_menu_highlight_fg, + binding=binding, + align=self.get_cell_align(r), + r=r, + newline_binding=self.text_editor_newline_binding, + ) + self.text_editor.update_idletasks() + self.text_editor_id = self.create_window((x, y), window=self.text_editor, anchor="nw") + if not dropdown: + self.text_editor.textedit.focus_set() + self.text_editor.scroll_to_bottom() + self.text_editor.textedit.bind("", lambda x: self.text_editor_newline_binding(r=r)) + if USER_OS == "darwin": + self.text_editor.textedit.bind("", lambda x: self.text_editor_newline_binding(r=r)) + for key, func in self.MT.text_editor_user_bound_keys.items(): + self.text_editor.textedit.bind(key, func) + if binding is not None: + self.text_editor.textedit.bind("", lambda x: binding((r, "Tab"))) + self.text_editor.textedit.bind("", lambda x: binding((r, "Return"))) + self.text_editor.textedit.bind("", lambda x: binding((r, "FocusOut"))) + self.text_editor.textedit.bind("", lambda x: binding((r, "Escape"))) + elif binding is None and set_data_on_close: + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((r, "Tab"))) + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((r, "Return"))) + if not dropdown: + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((r, "FocusOut"))) + self.text_editor.textedit.bind("", lambda x: self.close_text_editor((r, "Escape"))) + else: + self.text_editor.textedit.bind("", lambda x: self.destroy_text_editor("Escape")) + return True + + def text_editor_newline_binding(self, r=0, c=0, event=None, check_lines=True): + if self.height_resizing_enabled: + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + curr_height = self.text_editor.winfo_height() + if ( + not check_lines + or self.MT.get_lines_cell_height(self.text_editor.get_num_lines() + 1, font=self.MT.index_font) + > curr_height + ): + new_height = curr_height + self.MT.xtra_lines_increment + space_bot = self.MT.get_space_bot(r) + if new_height > space_bot: + new_height = space_bot + if new_height != curr_height: + self.set_row_height(r, new_height) + self.MT.refresh() + self.text_editor.config(height=new_height) + self.coords(self.text_editor_id, 0, self.MT.row_positions[r] + 1) + kwargs = self.get_cell_kwargs(datarn, key="dropdown") + if kwargs: + text_editor_h = self.text_editor.winfo_height() + win_h, anchor = self.get_dropdown_height_anchor(r, text_editor_h) + if anchor == "nw": + self.coords( + kwargs["canvas_id"], + 0, + self.MT.row_positions[r] + text_editor_h - 1, + ) + self.itemconfig(kwargs["canvas_id"], anchor=anchor, height=win_h) + elif anchor == "sw": + self.coords( + kwargs["canvas_id"], + 0, + self.MT.row_positions[r], + ) + self.itemconfig(kwargs["canvas_id"], anchor=anchor, height=win_h) + + def refresh_open_window_positions(self): + if self.text_editor is not None: + r = self.text_editor_loc + self.text_editor.config(height=self.MT.row_positions[r + 1] - self.MT.row_positions[r]) + self.coords( + self.text_editor_id, + 0, + self.MT.row_positions[r], + ) + if self.existing_dropdown_window is not None: + r = self.get_existing_dropdown_coords() + if self.text_editor is None: + text_editor_h = self.MT.row_positions[r + 1] - self.MT.row_positions[r] + anchor = self.itemcget(self.existing_dropdown_canvas_id, "anchor") + win_h = 0 + else: + text_editor_h = self.text_editor.winfo_height() + win_h, anchor = self.get_dropdown_height_anchor(r, text_editor_h) + if anchor == "nw": + self.coords( + self.existing_dropdown_canvas_id, + 0, + self.MT.row_positions[r] + text_editor_h - 1, + ) + # self.itemconfig(self.existing_dropdown_canvas_id, anchor=anchor, height=win_h) + elif anchor == "sw": + self.coords( + self.existing_dropdown_canvas_id, + 0, + self.MT.row_positions[r], + ) + # self.itemconfig(self.existing_dropdown_canvas_id, anchor=anchor, height=win_h) + + def bind_cell_edit(self, enable=True): + if enable: + self.edit_cell_enabled = True + else: + self.edit_cell_enabled = False + + def bind_text_editor_destroy(self, binding, r): + self.text_editor.textedit.bind("", lambda x: binding((r, "Return"))) + self.text_editor.textedit.bind("", lambda x: binding((r, "FocusOut"))) + self.text_editor.textedit.bind("", lambda x: binding((r, "Escape"))) + self.text_editor.textedit.focus_set() + + def destroy_text_editor(self, event=None): + if event is not None and self.extra_end_edit_cell_func is not None and self.text_editor_loc is not None: + self.extra_end_edit_cell_func( + EditHeaderEvent(int(self.text_editor_loc), "Escape", None, "escape_edit_index") + ) + self.text_editor_loc = None + try: + self.delete(self.text_editor_id) + except Exception: + pass + try: + self.text_editor.destroy() + except Exception: + pass + self.text_editor = None + self.text_editor_id = None + if event is not None and len(event) >= 3 and "Escape" in event: + self.focus_set() + + # r is displayed row + def close_text_editor( + self, + editor_info=None, + r=None, + set_data_on_close=True, + event=None, + destroy=True, + move_down=True, + redraw=True, + recreate=True, + ): + if self.focus_get() is None and editor_info: + return "break" + if editor_info is not None and len(editor_info) >= 2 and editor_info[1] == "Escape": + self.destroy_text_editor("Escape") + self.close_dropdown_window(r) + return "break" + if self.text_editor is not None: + self.text_editor_value = self.text_editor.get() + if destroy: + self.destroy_text_editor() + if set_data_on_close: + if r is None and editor_info is not None and len(editor_info) >= 2: + r = editor_info[0] + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + if self.extra_end_edit_cell_func is None and self.input_valid_for_cell(datarn, self.text_editor_value): + self.set_cell_data_undo( + r, + datarn=datarn, + value=self.text_editor_value, + check_input_valid=False, + ) + elif ( + self.extra_end_edit_cell_func is not None + and not self.MT.edit_cell_validation + and self.input_valid_for_cell(datarn, self.text_editor_value) + ): + self.set_cell_data_undo( + r, + datarn=datarn, + value=self.text_editor_value, + check_input_valid=False, + ) + self.extra_end_edit_cell_func( + EditIndexEvent( + r, + editor_info[1] if len(editor_info) >= 2 else "FocusOut", + f"{self.text_editor_value}", + "end_edit_index", + ) + ) + elif self.extra_end_edit_cell_func is not None and self.MT.edit_cell_validation: + validation = self.extra_end_edit_cell_func( + EditIndexEvent( + r, + editor_info[1] if len(editor_info) >= 2 else "FocusOut", + f"{self.text_editor_value}", + "end_edit_index", + ) + ) + if validation is not None: + self.text_editor_value = validation + if self.input_valid_for_cell(datarn, self.text_editor_value): + self.set_cell_data_undo( + r, + datarn=datarn, + value=self.text_editor_value, + check_input_valid=False, + ) + if move_down: + pass + self.close_dropdown_window(r) + if recreate: + self.MT.recreate_all_selection_boxes() + if redraw: + self.MT.refresh() + if editor_info is not None and len(editor_info) >= 2 and editor_info[1] != "FocusOut": + self.focus_set() + return "break" + + # internal event use + def set_cell_data_undo( + self, + r=0, + datarn=None, + value="", + cell_resize=True, + undo=True, + redraw=True, + check_input_valid=True, + ): + if datarn is None: + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + if isinstance(self.MT._row_index, int): + self.MT.set_cell_data_undo(r=r, c=self.MT._row_index, datarn=datarn, value=value, undo=True) + else: + self.fix_index(datarn) + if not check_input_valid or self.input_valid_for_cell(datarn, value): + if self.MT.undo_enabled and undo: + self.MT.undo_storage.append( + zlib.compress( + pickle.dumps( + ( + "edit_index", + {datarn: self.MT._row_index[datarn]}, + self.MT.get_boxes(include_current=False), + self.MT.currently_selected(), + ) + ) + ) + ) + self.set_cell_data(datarn=datarn, value=value) + if cell_resize and self.MT.cell_auto_resize_enabled: + self.set_row_height_run_binding(r, only_set_if_too_small=False) + if redraw: + self.MT.refresh() + self.parentframe.emit_event("<>") + + def set_cell_data(self, datarn=None, value=""): + if isinstance(self.MT._row_index, int): + self.MT.set_cell_data(datarn=datarn, datacn=self.MT._row_index, value=value) + else: + self.fix_index(datarn) + if self.get_cell_kwargs(datarn, key="checkbox"): + self.MT._row_index[datarn] = try_to_bool(value) + else: + self.MT._row_index[datarn] = value + + def input_valid_for_cell(self, datarn, value): + if self.get_cell_kwargs(datarn, key="readonly"): + return False + if self.get_cell_kwargs(datarn, key="checkbox"): + return is_bool_like(value) + if self.cell_equal_to(datarn, value): + return False + kwargs = self.get_cell_kwargs(datarn, key="dropdown") + if kwargs and kwargs["validate_input"] and value not in kwargs["values"]: + return False + return True + + def cell_equal_to(self, datarn, value): + self.fix_index(datarn) + if isinstance(self.MT._row_index, list): + return self.MT._row_index[datarn] == value + elif isinstance(self.MT._row_index, int): + return self.MT.cell_equal_to(datarn, self.MT._row_index, value) + + def get_cell_data(self, datarn, get_displayed=False, none_to_empty_str=False, redirect_int=False): + if get_displayed: + return self.get_valid_cell_data_as_str(datarn, fix=False) + if redirect_int and isinstance(self.MT._row_index, int): # internal use + return self.MT.get_cell_data(datarn, self.MT._row_index, none_to_empty_str=True) + if ( + isinstance(self.MT._row_index, int) + or not self.MT._row_index + or datarn >= len(self.MT._row_index) + or (self.MT._row_index[datarn] is None and none_to_empty_str) + ): + return "" + return self.MT._row_index[datarn] + + def get_valid_cell_data_as_str(self, datarn, fix=True) -> str: + kwargs = self.get_cell_kwargs(datarn, key="dropdown") + if kwargs: + if kwargs["text"] is not None: + return f"{kwargs['text']}" + else: + kwargs = self.get_cell_kwargs(datarn, key="checkbox") + if kwargs: + return f"{kwargs['text']}" + if isinstance(self.MT._row_index, int): + return self.MT.get_valid_cell_data_as_str(datarn, self.MT._row_index, get_displayed=True) + if fix: + self.fix_index(datarn) + try: + return "" if self.MT._row_index[datarn] is None else f"{self.MT._row_index[datarn]}" + except Exception: + return "" + + def get_value_for_empty_cell(self, datarn, r_ops=True): + if self.get_cell_kwargs(datarn, key="checkbox", cell=r_ops): + return False + kwargs = self.get_cell_kwargs(datarn, key="dropdown", cell=r_ops) + if kwargs and kwargs["validate_input"] and kwargs["values"]: + return kwargs["values"][0] + return "" + + def get_empty_index_seq(self, end, start=0, r_ops=True): + return [self.get_value_for_empty_cell(datarn, r_ops=r_ops) for datarn in range(start, end)] + + def fix_index(self, datarn=None, fix_values=tuple()): + if isinstance(self.MT._row_index, int): + return + if isinstance(self.MT._row_index, float): + self.MT._row_index = int(self.MT._row_index) + return + if not isinstance(self.MT._row_index, list): + try: + self.MT._row_index = list(self.MT._row_index) + except Exception: + self.MT._row_index = [] + if isinstance(datarn, int) and datarn >= len(self.MT._row_index): + self.MT._row_index.extend(self.get_empty_index_seq(end=datarn + 1, start=len(self.MT._row_index))) + if fix_values: + for rn, v in enumerate(islice(self.MT._row_index, fix_values[0], fix_values[1])): + if not self.input_valid_for_cell(rn, v): + self.MT._row_index[rn] = self.get_value_for_empty_cell(rn) + + def set_row_height_run_binding(self, r, only_set_if_too_small=True): + old_height = self.MT.row_positions[r + 1] - self.MT.row_positions[r] + new_height = self.set_row_height(r, only_set_if_too_small=only_set_if_too_small) + if self.row_height_resize_func is not None and old_height != new_height: + self.row_height_resize_func(ResizeEvent("row_height_resize", r, old_height, new_height)) + + # internal event use + def click_checkbox(self, r, datarn=None, undo=True, redraw=True): + if datarn is None: + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + kwargs = self.get_cell_kwargs(datarn, key="checkbox") + if kwargs["state"] == "normal": + if isinstance(self.MT._row_index, list): + value = not self.MT._row_index[datarn] if isinstance(self.MT._row_index[datarn], bool) else False + elif isinstance(self.MT._row_index, int): + value = ( + not self.MT.data[datarn][self.MT._row_index] + if isinstance(self.MT.data[datarn][self.MT._row_index], bool) + else False + ) + else: + value = False + self.set_cell_data_undo(r, datarn=datarn, value=value, cell_resize=False) + if kwargs["check_function"] is not None: + kwargs["check_function"]( + ( + r, + 0, + "IndexCheckboxClicked", + self.MT._row_index[datarn] + if isinstance(self.MT._row_index, list) + else self.MT.get_cell_data(datarn, self.MT._row_index), + ) + ) + if self.extra_end_edit_cell_func is not None: + self.extra_end_edit_cell_func( + EditIndexEvent( + r, + "Return", + self.MT._row_index[datarn] + if isinstance(self.MT._row_index, list) + else self.MT.get_cell_data(datarn, self.MT._row_index), + "end_edit_index", + ) + ) + if redraw: + self.MT.refresh() + + def checkbox_index(self, **kwargs): + self.destroy_opened_dropdown_window() + if "dropdown" in self.options or "checkbox" in self.options: + self.delete_options_dropdown_and_checkbox() + if "checkbox" not in self.options: + self.options["checkbox"] = {} + self.options["checkbox"] = get_checkbox_dict(**kwargs) + total_rows = self.MT.total_data_rows() + if isinstance(self.MT._row_index, int): + for datarn in range(total_rows): + self.MT.set_cell_data(datarn=datarn, datacn=self.MT._row_index, value=kwargs["checked"]) + else: + for datarn in range(total_rows): + self.set_cell_data(datarn=datarn, value=kwargs["checked"]) + + def dropdown_index(self, **kwargs): + self.destroy_opened_dropdown_window() + if "dropdown" in self.options or "checkbox" in self.options: + self.delete_options_dropdown_and_checkbox() + if "dropdown" not in self.options: + self.options["dropdown"] = {} + self.options["dropdown"] = get_dropdown_dict(**kwargs) + total_rows = self.MT.total_data_rows() + value = ( + kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" + ) + if isinstance(self.MT._row_index, int): + for datarn in range(total_rows): + self.MT.set_cell_data(datarn=datarn, datacn=self.MT._row_index, value=value) + else: + for datarn in range(total_rows): + self.set_cell_data(datarn=datarn, value=value) + + def create_checkbox(self, datarn=0, **kwargs): + if datarn in self.cell_options and ( + "dropdown" in self.cell_options[datarn] or "checkbox" in self.cell_options[datarn] + ): + self.delete_cell_options_dropdown_and_checkbox(datarn) + if datarn not in self.cell_options: + self.cell_options[datarn] = {} + self.cell_options[datarn]["checkbox"] = get_checkbox_dict(**kwargs) + self.set_cell_data(datarn=datarn, value=kwargs["checked"]) + + def create_dropdown(self, datarn, **kwargs): + if datarn in self.cell_options and ( + "dropdown" in self.cell_options[datarn] or "checkbox" in self.cell_options[datarn] + ): + self.delete_cell_options_dropdown_and_checkbox(datarn) + if datarn not in self.cell_options: + self.cell_options[datarn] = {} + self.cell_options[datarn]["dropdown"] = get_dropdown_dict(**kwargs) + self.set_cell_data( + datarn=datarn, + value=kwargs["set_value"] + if kwargs["set_value"] is not None + else kwargs["values"][0] + if kwargs["values"] + else "", + ) + + def get_dropdown_height_anchor(self, r, text_editor_h=None): + win_h = 5 + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + for i, v in enumerate(self.get_cell_kwargs(datarn, key="dropdown")["values"]): + v_numlines = len(v.split("\n") if isinstance(v, str) else f"{v}".split("\n")) + if v_numlines > 1: + win_h += self.MT.fl_ins + (v_numlines * self.MT.xtra_lines_increment) + 5 # end of cell + else: + win_h += self.MT.min_row_height + if i == 5: + break + if win_h > 500: + win_h = 500 + space_bot = self.MT.get_space_bot(0, text_editor_h) + win_h2 = int(win_h) + if win_h > space_bot: + win_h = space_bot - 1 + if win_h < self.MT.txt_h + 5: + win_h = self.MT.txt_h + 5 + elif win_h > win_h2: + win_h = win_h2 + return win_h, "nw" + + # r is displayed row + def open_dropdown_window(self, r, datarn=None, event=None): + self.destroy_text_editor("Escape") + self.destroy_opened_dropdown_window() + if datarn is None: + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + kwargs = self.get_cell_kwargs(datarn, key="dropdown") + if kwargs["state"] == "normal": + if not self.open_text_editor(event=event, r=r, dropdown=True): + return + win_h, anchor = self.get_dropdown_height_anchor(r) + window = self.MT.parentframe.dropdown_class( + self.MT.winfo_toplevel(), + r, + 0, + width=self.current_width, + height=win_h, + font=self.MT.index_font, + colors={ + "bg": self.MT.popup_menu_bg, + "fg": self.MT.popup_menu_fg, + "highlight_bg": self.MT.popup_menu_highlight_bg, + "highlight_fg": self.MT.popup_menu_highlight_fg, + }, + outline_color=self.MT.popup_menu_fg, + values=kwargs["values"], + close_dropdown_window=self.close_dropdown_window, + search_function=kwargs["search_function"], + arrowkey_RIGHT=self.MT.arrowkey_RIGHT, + arrowkey_LEFT=self.MT.arrowkey_LEFT, + align="w", + single_index="r", + ) + ypos = self.MT.row_positions[r + 1] + kwargs["canvas_id"] = self.create_window((0, ypos), window=window, anchor=anchor) + if kwargs["state"] == "normal": + self.text_editor.textedit.bind( + "<>", + lambda x: window.search_and_see( + DropDownModifiedEvent("IndexComboboxModified", r, 0, self.text_editor.get()) + ), + ) + if kwargs["modified_function"] is not None: + window.modified_function = kwargs["modified_function"] + self.update_idletasks() + try: + self.after(1, lambda: self.text_editor.textedit.focus()) + self.after(2, self.text_editor.scroll_to_bottom()) + except Exception: + return + redraw = False + else: + window.bind("", lambda x: self.close_dropdown_window(r)) + self.update_idletasks() + window.focus_set() + redraw = True + self.existing_dropdown_window = window + kwargs["window"] = window + self.existing_dropdown_canvas_id = kwargs["canvas_id"] + if redraw: + self.MT.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=True, redraw_table=False) + + # r is displayed row + def close_dropdown_window(self, r=None, selection=None, redraw=True): + if r is not None and selection is not None: + datarn = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + kwargs = self.get_cell_kwargs(datarn, key="dropdown") + if kwargs["select_function"] is not None: # user has specified a selection function + kwargs["select_function"](EditIndexEvent(r, "IndexComboboxSelected", f"{selection}", "end_edit_index")) + if self.extra_end_edit_cell_func is None: + self.set_cell_data_undo(r, datarn=datarn, value=selection, redraw=not redraw) + elif self.extra_end_edit_cell_func is not None and self.MT.edit_cell_validation: + validation = self.extra_end_edit_cell_func( + EditIndexEvent(r, "IndexComboboxSelected", f"{selection}", "end_edit_index") + ) + if validation is not None: + selection = validation + self.set_cell_data_undo(r, datarn=datarn, value=selection, redraw=not redraw) + elif self.extra_end_edit_cell_func is not None and not self.MT.edit_cell_validation: + self.set_cell_data_undo(r, datarn=datarn, value=selection, redraw=not redraw) + self.extra_end_edit_cell_func( + EditIndexEvent(r, "IndexComboboxSelected", f"{selection}", "end_edit_index") + ) + self.focus_set() + self.MT.recreate_all_selection_boxes() + self.destroy_text_editor("Escape") + self.destroy_opened_dropdown_window(r) + if redraw: + self.MT.refresh() + + def get_existing_dropdown_coords(self): + if self.existing_dropdown_window is not None: + return int(self.existing_dropdown_window.r) + return None + + def mouseclick_outside_editor_or_dropdown(self): + closed_dd_coords = self.get_existing_dropdown_coords() + if self.text_editor_loc is not None and self.text_editor is not None: + self.close_text_editor(editor_info=(self.text_editor_loc, "ButtonPress-1")) + else: + self.destroy_text_editor("Escape") + if closed_dd_coords is not None: + self.destroy_opened_dropdown_window( + closed_dd_coords + ) # displayed coords not data, necessary for b1 function + return closed_dd_coords + + def mouseclick_outside_editor_or_dropdown_all_canvases(self): + self.CH.mouseclick_outside_editor_or_dropdown() + self.MT.mouseclick_outside_editor_or_dropdown() + return self.mouseclick_outside_editor_or_dropdown() + + # r is displayed row, function can have two None args + def destroy_opened_dropdown_window(self, r=None, datarn=None): + if r is None and datarn is None and self.existing_dropdown_window is not None: + r = self.get_existing_dropdown_coords() + if r is not None or datarn is not None: + if datarn is None: + datarn_ = r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] + else: + datarn_ = r + else: + datarn_ = None + try: + self.delete(self.existing_dropdown_canvas_id) + except Exception: + pass + self.existing_dropdown_canvas_id = None + try: + self.existing_dropdown_window.destroy() + except Exception: + pass + kwargs = self.get_cell_kwargs(datarn_, key="dropdown") + if kwargs: + kwargs["canvas_id"] = "no dropdown open" + kwargs["window"] = "no dropdown open" + try: + self.delete(kwargs["canvas_id"]) + except Exception: + pass + self.existing_dropdown_window = None + + def get_cell_kwargs(self, datarn, key="dropdown", cell=True, entire=True): + if cell and datarn in self.cell_options and key in self.cell_options[datarn]: + return self.cell_options[datarn][key] + if entire and key in self.options: + return self.options[key] + return {} + + def delete_options_dropdown(self): + self.destroy_opened_dropdown_window() + if "dropdown" in self.options: + del self.options["dropdown"] + + def delete_options_checkbox(self): + if "checkbox" in self.options: + del self.options["checkbox"] + + def delete_options_dropdown_and_checkbox(self): + self.delete_options_dropdown() + self.delete_options_checkbox() + + def delete_cell_options_dropdown(self, datarn): + self.destroy_opened_dropdown_window(datarn=datarn) + if datarn in self.cell_options and "dropdown" in self.cell_options[datarn]: + del self.cell_options[datarn]["dropdown"] + + def delete_cell_options_checkbox(self, datarn): + if datarn in self.cell_options and "checkbox" in self.cell_options[datarn]: + del self.cell_options[datarn]["checkbox"] + + def delete_cell_options_dropdown_and_checkbox(self, datarn): + self.delete_cell_options_dropdown(datarn) + self.delete_cell_options_checkbox(datarn) diff --git a/thirdparty/tksheet/_tksheet_top_left_rectangle.py b/thirdparty/tksheet/_tksheet_top_left_rectangle.py new file mode 100644 index 0000000..ba38c5b --- /dev/null +++ b/thirdparty/tksheet/_tksheet_top_left_rectangle.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import tkinter as tk + +from ._tksheet_vars import ( + rc_binding, +) + + +class TopLeftRectangle(tk.Canvas): + def __init__(self, *args, **kwargs): + tk.Canvas.__init__( + self, + kwargs["parentframe"], + background=kwargs["top_left_bg"], + highlightthickness=0, + ) + self.parentframe = kwargs["parentframe"] + self.top_left_fg = kwargs["top_left_fg"] + self.top_left_fg_highlight = kwargs["top_left_fg_highlight"] + self.MT = kwargs["main_canvas"] + self.RI = kwargs["row_index_canvas"] + self.CH = kwargs["header_canvas"] + try: + self.config(width=self.RI.current_width, height=self.CH.current_height) + except Exception: + return + self.extra_motion_func = None + self.extra_b1_press_func = None + self.extra_b1_motion_func = None + self.extra_b1_release_func = None + self.extra_double_b1_func = None + self.extra_rc_func = None + self.MT.TL = self + self.RI.TL = self + self.CH.TL = self + w = self.RI.current_width + h = self.CH.current_height + self.create_rectangle( + 0, + h - 5, + w, + h, + fill=self.top_left_fg, + outline="", + tag="rw", + state="normal" if self.RI.width_resizing_enabled else "hidden", + ) + self.create_rectangle( + w - 5, + 0, + w, + h, + fill=self.top_left_fg, + outline="", + tag="rh", + state="normal" if self.CH.height_resizing_enabled else "hidden", + ) + self.tag_bind("rw", "", self.rw_enter) + self.tag_bind("rh", "", self.rh_enter) + self.tag_bind("rw", "", self.rw_leave) + self.tag_bind("rh", "", self.rh_leave) + self.bind("", self.mouse_motion) + self.bind("", self.b1_press) + self.bind("", self.b1_motion) + self.bind("", self.b1_release) + self.bind("", self.double_b1) + self.bind(rc_binding, self.rc) + + def rw_state(self, state="normal"): + self.itemconfig("rw", state=state) + + def rh_state(self, state="normal"): + self.itemconfig("rh", state=state) + + def rw_enter(self, event=None): + if self.RI.width_resizing_enabled: + self.itemconfig("rw", fill=self.top_left_fg_highlight) + + def rh_enter(self, event=None): + if self.CH.height_resizing_enabled: + self.itemconfig("rh", fill=self.top_left_fg_highlight) + + def rw_leave(self, event=None): + self.itemconfig("rw", fill=self.top_left_fg) + + def rh_leave(self, event=None): + self.itemconfig("rh", fill=self.top_left_fg) + + def basic_bindings(self, enable=True): + if enable: + self.bind("", self.mouse_motion) + self.bind("", self.b1_press) + self.bind("", self.b1_motion) + self.bind("", self.b1_release) + self.bind("", self.double_b1) + self.bind(rc_binding, self.rc) + else: + self.unbind("") + self.unbind("") + self.unbind("") + self.unbind("") + self.unbind("") + self.unbind(rc_binding) + + def set_dimensions(self, new_w=None, new_h=None): + try: + if new_w: + self.config(width=new_w) + w = new_w + h = self.winfo_height() + if new_h: + self.config(height=new_h) + w = self.winfo_width() + h = new_h + except Exception: + return + self.coords("rw", 0, h - 5, w, h) + self.coords("rh", w - 5, 0, w, h) + self.MT.recreate_all_selection_boxes() + + def mouse_motion(self, event=None): + self.MT.reset_mouse_motion_creations() + if self.extra_motion_func is not None: + self.extra_motion_func(event) + + def b1_press(self, event=None): + self.focus_set() + rect = self.find_overlapping(event.x, event.y, event.x, event.y) + if not rect: + if self.MT.select_all_enabled: + self.MT.deselect("all") + self.MT.select_all() + else: + self.MT.deselect("all") + elif rect[0] == 1: + if self.RI.width_resizing_enabled: + self.RI.set_width(self.MT.default_index_width, set_TL=True) + elif rect[0] == 2: + if self.CH.height_resizing_enabled: + self.CH.set_height(self.MT.default_header_height[1], set_TL=True) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.extra_b1_press_func is not None: + self.extra_b1_press_func(event) + + def b1_motion(self, event=None): + self.focus_set() + if self.extra_b1_motion_func is not None: + self.extra_b1_motion_func(event) + + def b1_release(self, event=None): + self.focus_set() + if self.extra_b1_release_func is not None: + self.extra_b1_release_func(event) + + def double_b1(self, event=None): + self.focus_set() + if self.extra_double_b1_func is not None: + self.extra_double_b1_func(event) + + def rc(self, event=None): + self.focus_set() + if self.extra_rc_func is not None: + self.extra_rc_func(event) diff --git a/thirdparty/tksheet/_tksheet_vars.py b/thirdparty/tksheet/_tksheet_vars.py new file mode 100644 index 0000000..7ae4259 --- /dev/null +++ b/thirdparty/tksheet/_tksheet_vars.py @@ -0,0 +1,2007 @@ +from __future__ import annotations + +# for mac bindings +from platform import system as get_os + +USER_OS = f"{get_os()}".lower() +ctrl_key = "Command" if USER_OS == "darwin" else "Control" +rc_binding = "<2>" if USER_OS == "darwin" else "<3>" +symbols_set = set("""!#\$%&'()*+,-./:;"@[]^_`{|}~>?= """) +nonelike = {None, "none", ""} +truthy = {True, "true", "t", "yes", "y", "on", "1", 1, 1.0} +falsy = {False, "false", "f", "no", "n", "off", "0", 0, 0.0} + +arrowkey_bindings_helper = { + "tab": "Tab", + "up": "Up", + "right": "Right", + "left": "Left", + "down": "Down", + "prior": "Prior", + "next": "Next", +} +emitted_events = { + "<>", + "<>", +} + + +def get_font(): + return ("Calibri", 13 if USER_OS == "darwin" else 11, "normal") + + +def get_index_font(): + return ("Calibri", 13 if USER_OS == "darwin" else 11, "normal") + + +def get_heading_font(): + return ("Calibri", 13 if USER_OS == "darwin" else 11, "normal") + + +theme_light_blue = { + "popup_menu_fg": "#000000", + "popup_menu_bg": "#FFFFFF", + "popup_menu_highlight_bg": "#DCDEE0", + "popup_menu_highlight_fg": "#000000", + "index_hidden_rows_expander_bg": "#747775", + "header_hidden_columns_expander_bg": "#747775", + "header_bg": "#FFFFFF", + "header_border_fg": "#C4C7C5", + "header_grid_fg": "#C4C7C5", + "header_fg": "#444746", + "header_selected_cells_bg": "#D3E3FD", + "header_selected_cells_fg": "black", + "index_bg": "#FFFFFF", + "index_border_fg": "#C4C7C5", + "index_grid_fg": "#C4C7C5", + "index_fg": "black", + "index_selected_cells_bg": "#D3E3FD", + "index_selected_cells_fg": "black", + "top_left_bg": "#FFFFFF", + "top_left_fg": "#C7C7C7", + "top_left_fg_highlight": "#747775", + "table_bg": "#FFFFFF", + "table_grid_fg": "#E1E1E1", + "table_fg": "black", + "table_selected_cells_border_fg": "#0B57D0", + "table_selected_cells_bg": "#E6EFFD", + "table_selected_cells_fg": "black", + "resizing_line_fg": "black", + "drag_and_drop_bg": "black", + "outline_color": "gray2", + "header_selected_columns_bg": "#0B57D0", + "header_selected_columns_fg": "#FFFFFF", + "index_selected_rows_bg": "#0B57D0", + "index_selected_rows_fg": "#FFFFFF", + "table_selected_rows_border_fg": "#0B57D0", + "table_selected_rows_bg": "#E6EFFD", + "table_selected_rows_fg": "black", + "table_selected_columns_border_fg": "#0B57D0", + "table_selected_columns_bg": "#E6EFFD", + "table_selected_columns_fg": "black", +} + +theme_light_green = { + "popup_menu_fg": "#000000", + "popup_menu_bg": "#FFFFFF", + "popup_menu_highlight_bg": "#DCDEE0", + "popup_menu_highlight_fg": "#000000", + "index_hidden_rows_expander_bg": "gray30", + "header_hidden_columns_expander_bg": "gray30", + "header_bg": "#ECECEC", + "header_border_fg": "#ababab", + "header_grid_fg": "#ababab", + "header_fg": "black", + "header_selected_cells_bg": "#d6d4d2", + "header_selected_cells_fg": "#217346", + "index_bg": "#ECECEC", + "index_border_fg": "#ababab", + "index_grid_fg": "#ababab", + "index_fg": "black", + "index_selected_cells_bg": "#d6d4d2", + "index_selected_cells_fg": "#217346", + "top_left_bg": "#ECECEC", + "top_left_fg": "#b7b7b7", + "top_left_fg_highlight": "#5f6368", + "table_bg": "#FFFFFF", + "table_grid_fg": "#bfbfbf", + "table_fg": "black", + "table_selected_cells_border_fg": "#217346", + "table_selected_cells_bg": "#E3E3E3", + "table_selected_cells_fg": "black", + "resizing_line_fg": "black", + "drag_and_drop_bg": "black", + "outline_color": "gray2", + "header_selected_columns_bg": "#d3f0e0", + "header_selected_columns_fg": "#217346", + "index_selected_rows_bg": "#d3f0e0", + "index_selected_rows_fg": "#217346", + "table_selected_rows_border_fg": "#217346", + "table_selected_rows_bg": "#E3E3E3", + "table_selected_rows_fg": "black", + "table_selected_columns_border_fg": "#217346", + "table_selected_columns_bg": "#E3E3E3", + "table_selected_columns_fg": "black", +} + +theme_dark = { + "popup_menu_fg": "white", + "popup_menu_bg": "gray15", + "popup_menu_highlight_bg": "gray40", + "popup_menu_highlight_fg": "white", + "index_hidden_rows_expander_bg": "gray30", + "header_hidden_columns_expander_bg": "gray30", + "header_bg": "#141414", + "header_border_fg": "#505054", + "header_grid_fg": "#8C8C8C", + "header_fg": "gray70", + "header_selected_cells_bg": "#545454", + "header_selected_cells_fg": "#6aa2fc", + "index_bg": "#141414", + "index_border_fg": "#505054", + "index_grid_fg": "#8C8C8C", + "index_fg": "gray70", + "index_selected_cells_bg": "#545454", + "index_selected_cells_fg": "#6aa2fc", + "top_left_bg": "#141414", + "top_left_fg": "#505054", + "top_left_fg_highlight": "white", + "table_bg": "#000000", + "table_grid_fg": "#595959", + "table_fg": "#E3E3E3", + "table_selected_cells_border_fg": "#6aa2fc", + "table_selected_cells_bg": "#404040", + "table_selected_cells_fg": "#F7F7F7", + "resizing_line_fg": "white", + "drag_and_drop_bg": "#ecf0f2", + "outline_color": "gray95", + "header_selected_columns_bg": "#4489F7", + "header_selected_columns_fg": "white", + "index_selected_rows_bg": "#4489F7", + "index_selected_rows_fg": "white", + "table_selected_rows_border_fg": "#4489F7", + "table_selected_rows_bg": "#404040", + "table_selected_rows_fg": "#F7F7F7", + "table_selected_columns_border_fg": "#4489F7", + "table_selected_columns_bg": "#404040", + "table_selected_columns_fg": "#F7F7F7", +} + +theme_black = { + "popup_menu_fg": "white", + "popup_menu_bg": "gray15", + "popup_menu_highlight_bg": "gray40", + "popup_menu_highlight_fg": "white", + "index_hidden_rows_expander_bg": "gray30", + "header_hidden_columns_expander_bg": "gray30", + "header_bg": "#000000", + "header_border_fg": "#505054", + "header_grid_fg": "#8C8C8C", + "header_fg": "#FBB86C", + "header_selected_cells_bg": "#545454", + "header_selected_cells_fg": "#FBB86C", + "index_bg": "#000000", + "index_border_fg": "#505054", + "index_grid_fg": "#8C8C8C", + "index_fg": "#FBB86C", + "index_selected_cells_bg": "#545454", + "index_selected_cells_fg": "#FBB86C", + "top_left_bg": "#000000", + "top_left_fg": "#505054", + "top_left_fg_highlight": "#FBB86C", + "table_bg": "#000000", + "table_grid_fg": "#595959", + "table_fg": "#E3E3E3", + "table_selected_cells_border_fg": "#FBB86C", + "table_selected_cells_bg": "#404040", + "table_selected_cells_fg": "#F7F7F7", + "resizing_line_fg": "white", + "drag_and_drop_bg": "#ecf0f2", + "outline_color": "gray95", + "header_selected_columns_bg": "#FBB86C", + "header_selected_columns_fg": "#000000", + "index_selected_rows_bg": "#FBB86C", + "index_selected_rows_fg": "#000000", + "table_selected_rows_border_fg": "#FBB86C", + "table_selected_rows_bg": "#404040", + "table_selected_rows_fg": "#F7F7F7", + "table_selected_columns_border_fg": "#FBB86C", + "table_selected_columns_bg": "#404040", + "table_selected_columns_fg": "#F7F7F7", +} + +theme_dark_blue = theme_black.copy() +theme_dark_blue["header_fg"] = "#6ACAD8" +theme_dark_blue["header_selected_cells_fg"] = "#6ACAD8" +theme_dark_blue["index_fg"] = "#6ACAD8" +theme_dark_blue["index_selected_cells_fg"] = "#6ACAD8" +theme_dark_blue["top_left_fg_highlight"] = "#6ACAD8" +theme_dark_blue["table_selected_cells_border_fg"] = "#6ACAD8" +theme_dark_blue["header_selected_columns_bg"] = "#6ACAD8" +theme_dark_blue["index_selected_rows_bg"] = "#6ACAD8" +theme_dark_blue["table_selected_rows_border_fg"] = "#6ACAD8" +theme_dark_blue["table_selected_columns_border_fg"] = "#6ACAD8" + +theme_dark_green = theme_black.copy() +theme_dark_green["header_fg"] = "#66FFBF" +theme_dark_green["header_selected_cells_fg"] = "#66FFBF" +theme_dark_green["index_fg"] = "#66FFBF" +theme_dark_green["index_selected_cells_fg"] = "#66FFBF" +theme_dark_green["top_left_fg_highlight"] = "#66FFBF" +theme_dark_green["table_selected_cells_border_fg"] = "#66FFBF" +theme_dark_green["header_selected_columns_bg"] = "#66FFBF" +theme_dark_green["index_selected_rows_bg"] = "#66FFBF" +theme_dark_green["table_selected_rows_border_fg"] = "#66FFBF" +theme_dark_green["table_selected_columns_border_fg"] = "#66FFBF" + +Color_Map_ = { + "alice blue": "#F0F8FF", + "ALICE BLUE": "#F0F8FF", + "AliceBlue": "#F0F8FF", + "aliceblue": "#F0F8FF", + "ALICEBLUE": "#F0F8FF", + "antique white": "#FAEBD7", + "ANTIQUE WHITE": "#FAEBD7", + "AntiqueWhite": "#FAEBD7", + "antiquewhite": "#FAEBD7", + "#" "ANTIQUEWHITE": "#FAEBD7", + "AntiqueWhite1": "#FFEFDB", + "antiquewhite1": "#FFEFDB", + "ANTIQUEWHITE1": "#FFEFDB", + "AntiqueWhite2": "#EEDFCC", + "antiquewhite2": "#EEDFCC", + "ANTIQUEWHITE2": "#EEDFCC", + "AntiqueWhite3": "#CDC0B0", + "antiquewhite3": "#CDC0B0", + "ANTIQUEWHITE3": "#CDC0B0", + "AntiqueWhite4": "#8B8378", + "antiquewhite4": "#8B8378", + "ANTIQUEWHITE4": "#8B8378", + "aquamarine": "#7FFFD4", + "AQUAMARINE": "#7FFFD4", + "aquamarine1": "#7FFFD4", + "AQUAMARINE1": "#7FFFD4", + "aquamarine2": "#76EEC6", + "AQUAMARINE2": "#76EEC6", + "aquamarine3": "#66CDAA", + "AQUAMARINE3": "#66CDAA", + "aquamarine4": "#458B74", + "AQUAMARINE4": "#458B74", + "azure": "#F0FFFF", + "AZURE": "#F0FFFF", + "azure1": "#F0FFFF", + "AZURE1": "#F0FFFF", + "azure2": "#E0EEEE", + "AZURE2": "#E0EEEE", + "azure3": "#C1CDCD", + "AZURE3": "#C1CDCD", + "azure4": "#838B8B", + "AZURE4": "#838B8B", + "beige": "#F5F5DC", + "BEIGE": "#F5F5DC", + "bisque": "#FFE4C4", + "BISQUE": "#FFE4C4", + "bisque1": "#FFE4C4", + "BISQUE1": "#FFE4C4", + "bisque2": "#EED5B7", + "BISQUE2": "#EED5B7", + "bisque3": "#CDB79E", + "BISQUE3": "#CDB79E", + "bisque4": "#8B7D6B", + "BISQUE4": "#8B7D6B", + "black": "#000000", + "BLACK": "#000000", + "blanched almond": "#FFEBCD", + "BLANCHED ALMOND": "#FFEBCD", + "BlanchedAlmond": "#FFEBCD", + "blanchedalmond": "#FFEBCD", + "BLANCHEDALMOND": "#FFEBCD", + "blue": "#0000FF", + "BLUE": "#0000FF", + "blue violet": "#8A2BE2", + "BLUE VIOLET": "#8A2BE2", + "blue1": "#0000FF", + "BLUE1": "#0000FF", + "blue2": "#0000EE", + "BLUE2": "#0000EE", + "blue3": "#0000CD", + "BLUE3": "#0000CD", + "blue4": "#00008B", + "BLUE4": "#00008B", + "BlueViolet": "#8A2BE2", + "blueviolet": "#8A2BE2", + "BLUEVIOLET": "#8A2BE2", + "brown": "#A52A2A", + "BROWN": "#A52A2A", + "brown1": "#FF4040", + "BROWN1": "#FF4040", + "brown2": "#EE3B3B", + "BROWN2": "#EE3B3B", + "brown3": "#CD3333", + "BROWN3": "#CD3333", + "brown4": "#8B2323", + "BROWN4": "#8B2323", + "burlywood": "#DEB887", + "BURLYWOOD": "#DEB887", + "burlywood1": "#FFD39B", + "BURLYWOOD1": "#FFD39B", + "burlywood2": "#EEC591", + "BURLYWOOD2": "#EEC591", + "burlywood3": "#CDAA7D", + "BURLYWOOD3": "#CDAA7D", + "burlywood4": "#8B7355", + "BURLYWOOD4": "#8B7355", + "cadet blue": "#5F9EA0", + "CADET BLUE": "#5F9EA0", + "CadetBlue": "#5F9EA0", + "cadetblue": "#5F9EA0", + "CADETBLUE": "#5F9EA0", + "CadetBlue1": "#98F5FF", + "cadetblue1": "#98F5FF", + "CADETBLUE1": "#98F5FF", + "CadetBlue2": "#8EE5EE", + "cadetblue2": "#8EE5EE", + "CADETBLUE2": "#8EE5EE", + "CadetBlue3": "#7AC5CD", + "cadetblue3": "#7AC5CD", + "CADETBLUE3": "#7AC5CD", + "CadetBlue4": "#53868B", + "cadetblue4": "#53868B", + "CADETBLUE4": "#53868B", + "chartreuse": "#7FFF00", + "CHARTREUSE": "#7FFF00", + "chartreuse1": "#7FFF00", + "CHARTREUSE1": "#7FFF00", + "chartreuse2": "#76EE00", + "CHARTREUSE2": "#76EE00", + "chartreuse3": "#66CD00", + "CHARTREUSE3": "#66CD00", + "chartreuse4": "#458B00", + "CHARTREUSE4": "#458B00", + "chocolate": "#D2691E", + "CHOCOLATE": "#D2691E", + "chocolate1": "#FF7F24", + "CHOCOLATE1": "#FF7F24", + "chocolate2": "#EE7621", + "CHOCOLATE2": "#EE7621", + "chocolate3": "#CD661D", + "CHOCOLATE3": "#CD661D", + "chocolate4": "#8B4513", + "CHOCOLATE4": "#8B4513", + "coral": "#FF7F50", + "CORAL": "#FF7F50", + "coral1": "#FF7256", + "CORAL1": "#FF7256", + "coral2": "#EE6A50", + "CORAL2": "#EE6A50", + "coral3": "#CD5B45", + "CORAL3": "#CD5B45", + "coral4": "#8B3E2F", + "CORAL4": "#8B3E2F", + "cornflower blue": "#6495ED", + "CORNFLOWER BLUE": "#6495ED", + "CornflowerBlue": "#6495ED", + "cornflowerblue": "#6495ED", + "CORNFLOWERBLUE": "#6495ED", + "cornsilk": "#FFF8DC", + "CORNSILK": "#FFF8DC", + "cornsilk1": "#FFF8DC", + "CORNSILK1": "#FFF8DC", + "cornsilk2": "#EEE8CD", + "CORNSILK2": "#EEE8CD", + "cornsilk3": "#CDC8B1", + "CORNSILK3": "#CDC8B1", + "cornsilk4": "#8B8878", + "CORNSILK4": "#8B8878", + "cyan": "#00FFFF", + "CYAN": "#00FFFF", + "cyan1": "#00FFFF", + "CYAN1": "#00FFFF", + "cyan2": "#00EEEE", + "CYAN2": "#00EEEE", + "cyan3": "#00CDCD", + "CYAN3": "#00CDCD", + "cyan4": "#008B8B", + "CYAN4": "#008B8B", + "dark blue": "#00008B", + "DARK BLUE": "#00008B", + "dark cyan": "#008B8B", + "DARK CYAN": "#008B8B", + "dark goldenrod": "#B8860B", + "DARK GOLDENROD": "#B8860B", + "dark gray": "#A9A9A9", + "DARK GRAY": "#A9A9A9", + "dark green": "#006400", + "DARK GREEN": "#006400", + "dark grey": "#A9A9A9", + "DARK GREY": "#A9A9A9", + "dark khaki": "#BDB76B", + "DARK KHAKI": "#BDB76B", + "dark magenta": "#8B008B", + "DARK MAGENTA": "#8B008B", + "dark olive green": "#556B2F", + "DARK OLIVE GREEN": "#556B2F", + "dark orange": "#FF8C00", + "DARK ORANGE": "#FF8C00", + "dark orchid": "#9932CC", + "DARK ORCHID": "#9932CC", + "dark red": "#8B0000", + "DARK RED": "#8B0000", + "dark salmon": "#E9967A", + "DARK SALMON": "#E9967A", + "dark sea green": "#8FBC8F", + "DARK SEA GREEN": "#8FBC8F", + "dark slate blue": "#483D8B", + "DARK SLATE BLUE": "#483D8B", + "dark slate gray": "#2F4F4F", + "DARK SLATE GRAY": "#2F4F4F", + "dark slate grey": "#2F4F4F", + "DARK SLATE GREY": "#2F4F4F", + "dark turquoise": "#00CED1", + "DARK TURQUOISE": "#00CED1", + "dark violet": "#9400D3", + "DARK VIOLET": "#9400D3", + "DarkBlue": "#00008B", + "darkblue": "#00008B", + "DARKBLUE": "#00008B", + "DarkCyan": "#008B8B", + "darkcyan": "#008B8B", + "DARKCYAN": "#008B8B", + "DarkGoldenrod": "#B8860B", + "darkgoldenrod": "#B8860B", + "DARKGOLDENROD": "#B8860B", + "DarkGoldenrod1": "#FFB90F", + "darkgoldenrod1": "#FFB90F", + "DARKGOLDENROD1": "#FFB90F", + "DarkGoldenrod2": "#EEAD0E", + "darkgoldenrod2": "#EEAD0E", + "DARKGOLDENROD2": "#EEAD0E", + "DarkGoldenrod3": "#CD950C", + "darkgoldenrod3": "#CD950C", + "DARKGOLDENROD3": "#CD950C", + "DarkGoldenrod4": "#8B6508", + "darkgoldenrod4": "#8B6508", + "DARKGOLDENROD4": "#8B6508", + "DarkGray": "#A9A9A9", + "darkgray": "#A9A9A9", + "DARKGRAY": "#A9A9A9", + "DarkGreen": "#006400", + "darkgreen": "#006400", + "DARKGREEN": "#006400", + "DarkGrey": "#A9A9A9", + "darkgrey": "#A9A9A9", + "DARKGREY": "#A9A9A9", + "DarkKhaki": "#BDB76B", + "darkkhaki": "#BDB76B", + "DARKKHAKI": "#BDB76B", + "DarkMagenta": "#8B008B", + "darkmagenta": "#8B008B", + "DARKMAGENTA": "#8B008B", + "DarkOliveGreen": "#556B2F", + "darkolivegreen": "#556B2F", + "DARKOLIVEGREEN": "#556B2F", + "DarkOliveGreen1": "#CAFF70", + "darkolivegreen1": "#CAFF70", + "DARKOLIVEGREEN1": "#CAFF70", + "DarkOliveGreen2": "#BCEE68", + "darkolivegreen2": "#BCEE68", + "DARKOLIVEGREEN2": "#BCEE68", + "DarkOliveGreen3": "#A2CD5A", + "darkolivegreen3": "#A2CD5A", + "DARKOLIVEGREEN3": "#A2CD5A", + "DarkOliveGreen4": "#6E8B3D", + "darkolivegreen4": "#6E8B3D", + "DARKOLIVEGREEN4": "#6E8B3D", + "DarkOrange": "#FF8C00", + "darkorange": "#FF8C00", + "DARKORANGE": "#FF8C00", + "DarkOrange1": "#FF7F00", + "darkorange1": "#FF7F00", + "DARKORANGE1": "#FF7F00", + "DarkOrange2": "#EE7600", + "darkorange2": "#EE7600", + "DARKORANGE2": "#EE7600", + "DarkOrange3": "#CD6600", + "darkorange3": "#CD6600", + "DARKORANGE3": "#CD6600", + "DarkOrange4": "#8B4500", + "darkorange4": "#8B4500", + "DARKORANGE4": "#8B4500", + "DarkOrchid": "#9932CC", + "darkorchid": "#9932CC", + "DARKORCHID": "#9932CC", + "DarkOrchid1": "#BF3EFF", + "darkorchid1": "#BF3EFF", + "DARKORCHID1": "#BF3EFF", + "DarkOrchid2": "#B23AEE", + "darkorchid2": "#B23AEE", + "DARKORCHID2": "#B23AEE", + "DarkOrchid3": "#9A32CD", + "darkorchid3": "#9A32CD", + "DARKORCHID3": "#9A32CD", + "DarkOrchid4": "#68228B", + "darkorchid4": "#68228B", + "DARKORCHID4": "#68228B", + "DarkRed": "#8B0000", + "darkred": "#8B0000", + "DARKRED": "#8B0000", + "DarkSalmon": "#E9967A", + "darksalmon": "#E9967A", + "DARKSALMON": "#E9967A", + "DarkSeaGreen": "#8FBC8F", + "darkseagreen": "#8FBC8F", + "DARKSEAGREEN": "#8FBC8F", + "DarkSeaGreen1": "#C1FFC1", + "darkseagreen1": "#C1FFC1", + "DARKSEAGREEN1": "#C1FFC1", + "DarkSeaGreen2": "#B4EEB4", + "darkseagreen2": "#B4EEB4", + "DARKSEAGREEN2": "#B4EEB4", + "DarkSeaGreen3": "#9BCD9B", + "darkseagreen3": "#9BCD9B", + "DARKSEAGREEN3": "#9BCD9B", + "DarkSeaGreen4": "#698B69", + "darkseagreen4": "#698B69", + "DARKSEAGREEN4": "#698B69", + "DarkSlateBlue": "#483D8B", + "darkslateblue": "#483D8B", + "DARKSLATEBLUE": "#483D8B", + "DarkSlateGray": "#2F4F4F", + "darkslategray": "#2F4F4F", + "DARKSLATEGRAY": "#2F4F4F", + "DarkSlateGray1": "#97FFFF", + "darkslategray1": "#97FFFF", + "DARKSLATEGRAY1": "#97FFFF", + "DarkSlateGray2": "#8DEEEE", + "darkslategray2": "#8DEEEE", + "DARKSLATEGRAY2": "#8DEEEE", + "DarkSlateGray3": "#79CDCD", + "darkslategray3": "#79CDCD", + "DARKSLATEGRAY3": "#79CDCD", + "DarkSlateGray4": "#528B8B", + "darkslategray4": "#528B8B", + "DARKSLATEGRAY4": "#528B8B", + "DarkSlateGrey": "#2F4F4F", + "darkslategrey": "#2F4F4F", + "DARKSLATEGREY": "#2F4F4F", + "DarkTurquoise": "#00CED1", + "darkturquoise": "#00CED1", + "DARKTURQUOISE": "#00CED1", + "DarkViolet": "#9400D3", + "darkviolet": "#9400D3", + "DARKVIOLET": "#9400D3", + "deep pink": "#FF1493", + "DEEP PINK": "#FF1493", + "deep sky blue": "#00BFFF", + "DEEP SKY BLUE": "#00BFFF", + "DeepPink": "#FF1493", + "deeppink": "#FF1493", + "DEEPPINK": "#FF1493", + "DeepPink1": "#FF1493", + "deeppink1": "#FF1493", + "DEEPPINK1": "#FF1493", + "DeepPink2": "#EE1289", + "deeppink2": "#EE1289", + "DEEPPINK2": "#EE1289", + "DeepPink3": "#CD1076", + "deeppink3": "#CD1076", + "DEEPPINK3": "#CD1076", + "DeepPink4": "#8B0A50", + "deeppink4": "#8B0A50", + "DEEPPINK4": "#8B0A50", + "DeepSkyBlue": "#00BFFF", + "deepskyblue": "#00BFFF", + "DEEPSKYBLUE": "#00BFFF", + "DeepSkyBlue1": "#00BFFF", + "deepskyblue1": "#00BFFF", + "DEEPSKYBLUE1": "#00BFFF", + "DeepSkyBlue2": "#00B2EE", + "deepskyblue2": "#00B2EE", + "DEEPSKYBLUE2": "#00B2EE", + "DeepSkyBlue3": "#009ACD", + "deepskyblue3": "#009ACD", + "DEEPSKYBLUE3": "#009ACD", + "DeepSkyBlue4": "#00688B", + "deepskyblue4": "#00688B", + "DEEPSKYBLUE4": "#00688B", + "dim gray": "#696969", + "DIM GRAY": "#696969", + "dim grey": "#696969", + "DIM GREY": "#696969", + "DimGray": "#696969", + "dimgray": "#696969", + "DIMGRAY": "#696969", + "DimGrey": "#696969", + "dimgrey": "#696969", + "DIMGREY": "#696969", + "dodger blue": "#1E90FF", + "DODGER BLUE": "#1E90FF", + "DodgerBlue": "#1E90FF", + "dodgerblue": "#1E90FF", + "DODGERBLUE": "#1E90FF", + "DodgerBlue1": "#1E90FF", + "dodgerblue1": "#1E90FF", + "DODGERBLUE1": "#1E90FF", + "DodgerBlue2": "#1C86EE", + "dodgerblue2": "#1C86EE", + "DODGERBLUE2": "#1C86EE", + "DodgerBlue3": "#1874CD", + "dodgerblue3": "#1874CD", + "DODGERBLUE3": "#1874CD", + "DodgerBlue4": "#104E8B", + "dodgerblue4": "#104E8B", + "DODGERBLUE4": "#104E8B", + "firebrick": "#B22222", + "FIREBRICK": "#B22222", + "firebrick1": "#FF3030", + "FIREBRICK1": "#FF3030", + "firebrick2": "#EE2C2C", + "FIREBRICK2": "#EE2C2C", + "firebrick3": "#CD2626", + "FIREBRICK3": "#CD2626", + "firebrick4": "#8B1A1A", + "FIREBRICK4": "#8B1A1A", + "floral white": "#FFFAF0", + "FLORAL WHITE": "#FFFAF0", + "FloralWhite": "#FFFAF0", + "floralwhite": "#FFFAF0", + "FLORALWHITE": "#FFFAF0", + "forest green": "#228B22", + "FOREST GREEN": "#228B22", + "ForestGreen": "#228B22", + "forestgreen": "#228B22", + "FORESTGREEN": "#228B22", + "gainsboro": "#DCDCDC", + "GAINSBORO": "#DCDCDC", + "ghost white": "#F8F8FF", + "GHOST WHITE": "#F8F8FF", + "GhostWhite": "#F8F8FF", + "ghostwhite": "#F8F8FF", + "GHOSTWHITE": "#F8F8FF", + "gold": "#FFD700", + "GOLD": "#FFD700", + "gold1": "#FFD700", + "GOLD1": "#FFD700", + "gold2": "#EEC900", + "GOLD2": "#EEC900", + "gold3": "#CDAD00", + "GOLD3": "#CDAD00", + "gold4": "#8B7500", + "GOLD4": "#8B7500", + "goldenrod": "#DAA520", + "GOLDENROD": "#DAA520", + "goldenrod1": "#FFC125", + "GOLDENROD1": "#FFC125", + "goldenrod2": "#EEB422", + "GOLDENROD2": "#EEB422", + "goldenrod3": "#CD9B1D", + "GOLDENROD3": "#CD9B1D", + "goldenrod4": "#8B6914", + "GOLDENROD4": "#8B6914", + "gray": "#BEBEBE", + "GRAY": "#BEBEBE", + "gray0": "#000000", + "GRAY0": "#000000", + "gray1": "#030303", + "GRAY1": "#030303", + "gray2": "#050505", + "GRAY2": "#050505", + "gray3": "#080808", + "GRAY3": "#080808", + "gray4": "#0A0A0A", + "GRAY4": "#0A0A0A", + "gray5": "#0D0D0D", + "GRAY5": "#0D0D0D", + "gray6": "#0F0F0F", + "GRAY6": "#0F0F0F", + "gray7": "#121212", + "GRAY7": "#121212", + "gray8": "#141414", + "GRAY8": "#141414", + "gray9": "#171717", + "GRAY9": "#171717", + "gray10": "#1A1A1A", + "GRAY10": "#1A1A1A", + "gray11": "#1C1C1C", + "GRAY11": "#1C1C1C", + "gray12": "#1F1F1F", + "GRAY12": "#1F1F1F", + "gray13": "#212121", + "GRAY13": "#212121", + "gray14": "#242424", + "GRAY14": "#242424", + "gray15": "#262626", + "GRAY15": "#262626", + "gray16": "#292929", + "GRAY16": "#292929", + "gray17": "#2B2B2B", + "GRAY17": "#2B2B2B", + "gray18": "#2E2E2E", + "GRAY18": "#2E2E2E", + "gray19": "#303030", + "GRAY19": "#303030", + "gray20": "#333333", + "GRAY20": "#333333", + "gray21": "#363636", + "GRAY21": "#363636", + "gray22": "#383838", + "GRAY22": "#383838", + "gray23": "#3B3B3B", + "GRAY23": "#3B3B3B", + "gray24": "#3D3D3D", + "GRAY24": "#3D3D3D", + "gray25": "#404040", + "GRAY25": "#404040", + "gray26": "#424242", + "GRAY26": "#424242", + "gray27": "#454545", + "GRAY27": "#454545", + "gray28": "#474747", + "GRAY28": "#474747", + "gray29": "#4A4A4A", + "GRAY29": "#4A4A4A", + "gray30": "#4D4D4D", + "GRAY30": "#4D4D4D", + "gray31": "#4F4F4F", + "GRAY31": "#4F4F4F", + "gray32": "#525252", + "GRAY32": "#525252", + "gray33": "#545454", + "GRAY33": "#545454", + "gray34": "#575757", + "GRAY34": "#575757", + "gray35": "#595959", + "GRAY35": "#595959", + "gray36": "#5C5C5C", + "GRAY36": "#5C5C5C", + "gray37": "#5E5E5E", + "GRAY37": "#5E5E5E", + "gray38": "#616161", + "GRAY38": "#616161", + "gray39": "#636363", + "GRAY39": "#636363", + "gray40": "#666666", + "GRAY40": "#666666", + "gray41": "#696969", + "GRAY41": "#696969", + "gray42": "#6B6B6B", + "GRAY42": "#6B6B6B", + "gray43": "#707070", + "GRAY43": "#707070", + "gray44": "#707070", + "GRAY44": "#707070", + "gray45": "#707070", + "GRAY45": "#707070", + "gray46": "#757575", + "GRAY46": "#757575", + "gray47": "#787878", + "GRAY47": "#787878", + "gray48": "#7A7A7A", + "GRAY48": "#7A7A7A", + "gray49": "#707070", + "GRAY49": "#707070", + "gray50": "#7F7F7F", + "GRAY50": "#7F7F7F", + "gray51": "#828282", + "GRAY51": "#828282", + "gray52": "#858585", + "GRAY52": "#858585", + "gray53": "#878787", + "GRAY53": "#878787", + "gray54": "#8A8A8A", + "GRAY54": "#8A8A8A", + "gray55": "#8C8C8C", + "GRAY55": "#8C8C8C", + "gray56": "#8F8F8F", + "GRAY56": "#8F8F8F", + "gray57": "#919191", + "GRAY57": "#919191", + "gray58": "#949494", + "GRAY58": "#949494", + "gray59": "#969696", + "GRAY59": "#969696", + "gray60": "#999999", + "GRAY60": "#999999", + "gray61": "#9C9C9C", + "GRAY61": "#9C9C9C", + "gray62": "#9E9E9E", + "GRAY62": "#9E9E9E", + "gray63": "#A1A1A1", + "GRAY63": "#A1A1A1", + "gray64": "#A3A3A3", + "GRAY64": "#A3A3A3", + "gray65": "#A6A6A6", + "GRAY65": "#A6A6A6", + "gray66": "#A8A8A8", + "GRAY66": "#A8A8A8", + "gray67": "#ABABAB", + "GRAY67": "#ABABAB", + "gray68": "#ADADAD", + "GRAY68": "#ADADAD", + "gray69": "#B0B0B0", + "GRAY69": "#B0B0B0", + "gray70": "#B3B3B3", + "GRAY70": "#B3B3B3", + "gray71": "#B5B5B5", + "GRAY71": "#B5B5B5", + "gray72": "#B8B8B8", + "GRAY72": "#B8B8B8", + "gray73": "#BABABA", + "GRAY73": "#BABABA", + "gray74": "#BDBDBD", + "GRAY74": "#BDBDBD", + "gray75": "#BFBFBF", + "GRAY75": "#BFBFBF", + "gray76": "#C2C2C2", + "GRAY76": "#C2C2C2", + "gray77": "#C4C4C4", + "GRAY77": "#C4C4C4", + "gray78": "#C7C7C7", + "GRAY78": "#C7C7C7", + "gray79": "#C9C9C9", + "GRAY79": "#C9C9C9", + "gray80": "#CCCCCC", + "GRAY80": "#CCCCCC", + "gray81": "#CFCFCF", + "GRAY81": "#CFCFCF", + "gray82": "#D1D1D1", + "GRAY82": "#D1D1D1", + "gray83": "#D4D4D4", + "GRAY83": "#D4D4D4", + "gray84": "#D6D6D6", + "GRAY84": "#D6D6D6", + "gray85": "#D9D9D9", + "GRAY85": "#D9D9D9", + "gray86": "#DBDBDB", + "GRAY86": "#DBDBDB", + "gray87": "#DEDEDE", + "GRAY87": "#DEDEDE", + "gray88": "#E0E0E0", + "GRAY88": "#E0E0E0", + "gray89": "#E3E3E3", + "GRAY89": "#E3E3E3", + "gray90": "#E5E5E5", + "GRAY90": "#E5E5E5", + "gray91": "#E8E8E8", + "GRAY91": "#E8E8E8", + "gray92": "#EBEBEB", + "GRAY92": "#EBEBEB", + "gray93": "#EDEDED", + "GRAY93": "#EDEDED", + "gray94": "#F0F0F0", + "GRAY94": "#F0F0F0", + "gray95": "#F2F2F2", + "GRAY95": "#F2F2F2", + "gray96": "#F5F5F5", + "GRAY96": "#F5F5F5", + "gray97": "#F7F7F7", + "GRAY97": "#F7F7F7", + "gray98": "#FAFAFA", + "GRAY98": "#FAFAFA", + "gray99": "#FCFCFC", + "GRAY99": "#FCFCFC", + "gray100": "#FFFFFF", + "GRAY100": "#FFFFFF", + "green": "#00FF00", + "GREEN": "#00FF00", + "green yellow": "#ADFF2F", + "GREEN YELLOW": "#ADFF2F", + "green1": "#00FF00", + "GREEN1": "#00FF00", + "green2": "#00EE00", + "GREEN2": "#00EE00", + "green3": "#00CD00", + "GREEN3": "#00CD00", + "green4": "#008B00", + "GREEN4": "#008B00", + "GreenYellow": "#ADFF2F", + "greenyellow": "#ADFF2F", + "GREENYELLOW": "#ADFF2F", + "grey": "#BEBEBE", + "GREY": "#BEBEBE", + "grey0": "#000000", + "GREY0": "#000000", + "grey1": "#030303", + "GREY1": "#030303", + "grey2": "#050505", + "GREY2": "#050505", + "grey3": "#080808", + "GREY3": "#080808", + "grey4": "#0A0A0A", + "GREY4": "#0A0A0A", + "grey5": "#0D0D0D", + "GREY5": "#0D0D0D", + "grey6": "#0F0F0F", + "GREY6": "#0F0F0F", + "grey7": "#121212", + "GREY7": "#121212", + "grey8": "#141414", + "GREY8": "#141414", + "grey9": "#171717", + "GREY9": "#171717", + "grey10": "#1A1A1A", + "GREY10": "#1A1A1A", + "grey11": "#1C1C1C", + "GREY11": "#1C1C1C", + "grey12": "#1F1F1F", + "GREY12": "#1F1F1F", + "grey13": "#212121", + "GREY13": "#212121", + "grey14": "#242424", + "GREY14": "#242424", + "grey15": "#262626", + "GREY15": "#262626", + "grey16": "#292929", + "GREY16": "#292929", + "grey17": "#2B2B2B", + "GREY17": "#2B2B2B", + "grey18": "#2E2E2E", + "GREY18": "#2E2E2E", + "grey19": "#303030", + "GREY19": "#303030", + "grey20": "#333333", + "GREY20": "#333333", + "grey21": "#363636", + "GREY21": "#363636", + "grey22": "#383838", + "GREY22": "#383838", + "grey23": "#3B3B3B", + "GREY23": "#3B3B3B", + "grey24": "#3D3D3D", + "GREY24": "#3D3D3D", + "grey25": "#404040", + "GREY25": "#404040", + "grey26": "#424242", + "GREY26": "#424242", + "grey27": "#454545", + "GREY27": "#454545", + "grey28": "#474747", + "GREY28": "#474747", + "grey29": "#4A4A4A", + "GREY29": "#4A4A4A", + "grey30": "#4D4D4D", + "GREY30": "#4D4D4D", + "grey31": "#4F4F4F", + "GREY31": "#4F4F4F", + "grey32": "#525252", + "GREY32": "#525252", + "grey33": "#545454", + "GREY33": "#545454", + "grey34": "#575757", + "GREY34": "#575757", + "grey35": "#595959", + "GREY35": "#595959", + "grey36": "#5C5C5C", + "GREY36": "#5C5C5C", + "grey37": "#5E5E5E", + "GREY37": "#5E5E5E", + "grey38": "#616161", + "GREY38": "#616161", + "grey39": "#636363", + "GREY39": "#636363", + "grey40": "#666666", + "GREY40": "#666666", + "grey41": "#696969", + "GREY41": "#696969", + "grey42": "#6B6B6B", + "GREY42": "#6B6B6B", + "grey43": "#707070", + "GREY43": "#707070", + "grey44": "#707070", + "GREY44": "#707070", + "grey45": "#707070", + "GREY45": "#707070", + "grey46": "#757575", + "GREY46": "#757575", + "grey47": "#787878", + "GREY47": "#787878", + "grey48": "#7A7A7A", + "GREY48": "#7A7A7A", + "grey49": "#707070", + "GREY49": "#707070", + "grey50": "#7F7F7F", + "GREY50": "#7F7F7F", + "grey51": "#828282", + "GREY51": "#828282", + "grey52": "#858585", + "GREY52": "#858585", + "grey53": "#878787", + "GREY53": "#878787", + "grey54": "#8A8A8A", + "GREY54": "#8A8A8A", + "grey55": "#8C8C8C", + "GREY55": "#8C8C8C", + "grey56": "#8F8F8F", + "GREY56": "#8F8F8F", + "grey57": "#919191", + "GREY57": "#919191", + "grey58": "#949494", + "GREY58": "#949494", + "grey59": "#969696", + "GREY59": "#969696", + "grey60": "#999999", + "GREY60": "#999999", + "grey61": "#9C9C9C", + "GREY61": "#9C9C9C", + "grey62": "#9E9E9E", + "GREY62": "#9E9E9E", + "grey63": "#A1A1A1", + "GREY63": "#A1A1A1", + "grey64": "#A3A3A3", + "GREY64": "#A3A3A3", + "grey65": "#A6A6A6", + "GREY65": "#A6A6A6", + "grey66": "#A8A8A8", + "GREY66": "#A8A8A8", + "grey67": "#ABABAB", + "GREY67": "#ABABAB", + "grey68": "#ADADAD", + "GREY68": "#ADADAD", + "grey69": "#B0B0B0", + "GREY69": "#B0B0B0", + "grey70": "#B3B3B3", + "GREY70": "#B3B3B3", + "grey71": "#B5B5B5", + "GREY71": "#B5B5B5", + "grey72": "#B8B8B8", + "GREY72": "#B8B8B8", + "grey73": "#BABABA", + "GREY73": "#BABABA", + "grey74": "#BDBDBD", + "GREY74": "#BDBDBD", + "grey75": "#BFBFBF", + "GREY75": "#BFBFBF", + "grey76": "#C2C2C2", + "GREY76": "#C2C2C2", + "grey77": "#C4C4C4", + "GREY77": "#C4C4C4", + "grey78": "#C7C7C7", + "GREY78": "#C7C7C7", + "grey79": "#C9C9C9", + "GREY79": "#C9C9C9", + "grey80": "#CCCCCC", + "GREY80": "#CCCCCC", + "grey81": "#CFCFCF", + "GREY81": "#CFCFCF", + "grey82": "#D1D1D1", + "GREY82": "#D1D1D1", + "grey83": "#D4D4D4", + "GREY83": "#D4D4D4", + "grey84": "#D6D6D6", + "GREY84": "#D6D6D6", + "grey85": "#D9D9D9", + "GREY85": "#D9D9D9", + "grey86": "#DBDBDB", + "GREY86": "#DBDBDB", + "grey87": "#DEDEDE", + "GREY87": "#DEDEDE", + "grey88": "#E0E0E0", + "GREY88": "#E0E0E0", + "grey89": "#E3E3E3", + "GREY89": "#E3E3E3", + "grey90": "#E5E5E5", + "GREY90": "#E5E5E5", + "grey91": "#E8E8E8", + "GREY91": "#E8E8E8", + "grey92": "#EBEBEB", + "GREY92": "#EBEBEB", + "grey93": "#EDEDED", + "GREY93": "#EDEDED", + "grey94": "#F0F0F0", + "GREY94": "#F0F0F0", + "grey95": "#F2F2F2", + "GREY95": "#F2F2F2", + "grey96": "#F5F5F5", + "GREY96": "#F5F5F5", + "grey97": "#F7F7F7", + "GREY97": "#F7F7F7", + "grey98": "#FAFAFA", + "GREY98": "#FAFAFA", + "grey99": "#FCFCFC", + "GREY99": "#FCFCFC", + "grey100": "#FFFFFF", + "GREY100": "#FFFFFF", + "honeydew": "#F0FFF0", + "HONEYDEW": "#F0FFF0", + "honeydew1": "#F0FFF0", + "HONEYDEW1": "#F0FFF0", + "honeydew2": "#E0EEE0", + "HONEYDEW2": "#E0EEE0", + "honeydew3": "#C1CDC1", + "HONEYDEW3": "#C1CDC1", + "honeydew4": "#838B83", + "HONEYDEW4": "#838B83", + "hot pink": "#FF69B4", + "HOT PINK": "#FF69B4", + "HotPink": "#FF69B4", + "hotpink": "#FF69B4", + "HOTPINK": "#FF69B4", + "HotPink1": "#FF6EB4", + "hotpink1": "#FF6EB4", + "HOTPINK1": "#FF6EB4", + "HotPink2": "#EE6AA7", + "hotpink2": "#EE6AA7", + "HOTPINK2": "#EE6AA7", + "HotPink3": "#CD6090", + "hotpink3": "#CD6090", + "HOTPINK3": "#CD6090", + "HotPink4": "#8B3A62", + "hotpink4": "#8B3A62", + "HOTPINK4": "#8B3A62", + "indigo": "#4b0082", + "INDIGO": "#4b0082", + "indian red": "#CD5C5C", + "INDIAN RED": "#CD5C5C", + "IndianRed": "#CD5C5C", + "indianred": "#CD5C5C", + "INDIANRED": "#CD5C5C", + "IndianRed1": "#FF6A6A", + "indianred1": "#FF6A6A", + "INDIANRED1": "#FF6A6A", + "IndianRed2": "#EE6363", + "indianred2": "#EE6363", + "INDIANRED2": "#EE6363", + "IndianRed3": "#CD5555", + "indianred3": "#CD5555", + "INDIANRED3": "#CD5555", + "IndianRed4": "#8B3A3A", + "indianred4": "#8B3A3A", + "INDIANRED4": "#8B3A3A", + "ivory": "#FFFFF0", + "IVORY": "#FFFFF0", + "ivory1": "#FFFFF0", + "IVORY1": "#FFFFF0", + "ivory2": "#EEEEE0", + "IVORY2": "#EEEEE0", + "ivory3": "#CDCDC1", + "IVORY3": "#CDCDC1", + "ivory4": "#8B8B83", + "IVORY4": "#8B8B83", + "khaki": "#F0E68C", + "KHAKI": "#F0E68C", + "khaki1": "#FFF68F", + "KHAKI1": "#FFF68F", + "khaki2": "#EEE685", + "KHAKI2": "#EEE685", + "khaki3": "#CDC673", + "KHAKI3": "#CDC673", + "khaki4": "#8B864E", + "KHAKI4": "#8B864E", + "lavender": "#E6E6FA", + "LAVENDER": "#E6E6FA", + "lavender blush": "#FFF0F5", + "LAVENDER BLUSH": "#FFF0F5", + "LavenderBlush": "#FFF0F5", + "lavenderblush": "#FFF0F5", + "LAVENDERBLUSH": "#FFF0F5", + "LavenderBlush1": "#FFF0F5", + "lavenderblush1": "#FFF0F5", + "LAVENDERBLUSH1": "#FFF0F5", + "LavenderBlush2": "#EEE0E5", + "lavenderblush2": "#EEE0E5", + "LAVENDERBLUSH2": "#EEE0E5", + "LavenderBlush3": "#CDC1C5", + "lavenderblush3": "#CDC1C5", + "LAVENDERBLUSH3": "#CDC1C5", + "LavenderBlush4": "#8B8386", + "lavenderblush4": "#8B8386", + "LAVENDERBLUSH4": "#8B8386", + "lawn green": "#7CFC00", + "LAWN GREEN": "#7CFC00", + "LawnGreen": "#7CFC00", + "lawngreen": "#7CFC00", + "LAWNGREEN": "#7CFC00", + "lemon chiffon": "#FFFACD", + "LEMON CHIFFON": "#FFFACD", + "LemonChiffon": "#FFFACD", + "lemonchiffon": "#FFFACD", + "LEMONCHIFFON": "#FFFACD", + "LemonChiffon1": "#FFFACD", + "lemonchiffon1": "#FFFACD", + "LEMONCHIFFON1": "#FFFACD", + "LemonChiffon2": "#EEE9BF", + "lemonchiffon2": "#EEE9BF", + "LEMONCHIFFON2": "#EEE9BF", + "LemonChiffon3": "#CDC9A5", + "lemonchiffon3": "#CDC9A5", + "LEMONCHIFFON3": "#CDC9A5", + "LemonChiffon4": "#8B8970", + "lemonchiffon4": "#8B8970", + "LEMONCHIFFON4": "#8B8970", + "light blue": "#ADD8E6", + "LIGHT BLUE": "#ADD8E6", + "light coral": "#F08080", + "LIGHT CORAL": "#F08080", + "light cyan": "#E0FFFF", + "LIGHT CYAN": "#E0FFFF", + "light goldenrod": "#EEDD82", + "LIGHT GOLDENROD": "#EEDD82", + "light goldenrod yellow": "#FAFAD2", + "LIGHT GOLDENROD YELLOW": "#FAFAD2", + "light gray": "#D3D3D3", + "LIGHT GRAY": "#D3D3D3", + "light green": "#90EE90", + "LIGHT GREEN": "#90EE90", + "light grey": "#D3D3D3", + "LIGHT GREY": "#D3D3D3", + "light pink": "#FFB6C1", + "LIGHT PINK": "#FFB6C1", + "light salmon": "#FFA07A", + "LIGHT SALMON": "#FFA07A", + "light sea green": "#20B2AA", + "LIGHT SEA GREEN": "#20B2AA", + "light sky blue": "#87CEFA", + "LIGHT SKY BLUE": "#87CEFA", + "light slate blue": "#8470FF", + "LIGHT SLATE BLUE": "#8470FF", + "light slate gray": "#778899", + "LIGHT SLATE GRAY": "#778899", + "light slate grey": "#778899", + "LIGHT SLATE GREY": "#778899", + "light steel blue": "#B0C4DE", + "LIGHT STEEL BLUE": "#B0C4DE", + "light yellow": "#FFFFE0", + "LIGHT YELLOW": "#FFFFE0", + "LightBlue": "#ADD8E6", + "lightblue": "#ADD8E6", + "LIGHTBLUE": "#ADD8E6", + "LightBlue1": "#BFEFFF", + "lightblue1": "#BFEFFF", + "LIGHTBLUE1": "#BFEFFF", + "LightBlue2": "#B2DFEE", + "lightblue2": "#B2DFEE", + "LIGHTBLUE2": "#B2DFEE", + "LightBlue3": "#9AC0CD", + "lightblue3": "#9AC0CD", + "LIGHTBLUE3": "#9AC0CD", + "LightBlue4": "#68838B", + "lightblue4": "#68838B", + "LIGHTBLUE4": "#68838B", + "LightCoral": "#F08080", + "lightcoral": "#F08080", + "LIGHTCORAL": "#F08080", + "LightCyan": "#E0FFFF", + "lightcyan": "#E0FFFF", + "LIGHTCYAN": "#E0FFFF", + "LightCyan1": "#E0FFFF", + "lightcyan1": "#E0FFFF", + "LIGHTCYAN1": "#E0FFFF", + "LightCyan2": "#D1EEEE", + "lightcyan2": "#D1EEEE", + "LIGHTCYAN2": "#D1EEEE", + "LightCyan3": "#B4CDCD", + "lightcyan3": "#B4CDCD", + "LIGHTCYAN3": "#B4CDCD", + "LightCyan4": "#7A8B8B", + "lightcyan4": "#7A8B8B", + "LIGHTCYAN4": "#7A8B8B", + "LightGoldenrod": "#EEDD82", + "lightgoldenrod": "#EEDD82", + "LIGHTGOLDENROD": "#EEDD82", + "LightGoldenrod1": "#FFEC8B", + "lightgoldenrod1": "#FFEC8B", + "LIGHTGOLDENROD1": "#FFEC8B", + "LightGoldenrod2": "#EEDC82", + "lightgoldenrod2": "#EEDC82", + "LIGHTGOLDENROD2": "#EEDC82", + "LightGoldenrod3": "#CDBE70", + "lightgoldenrod3": "#CDBE70", + "LIGHTGOLDENROD3": "#CDBE70", + "LightGoldenrod4": "#8B814C", + "lightgoldenrod4": "#8B814C", + "LIGHTGOLDENROD4": "#8B814C", + "LightGoldenrodYellow": "#FAFAD2", + "lightgoldenrodyellow": "#FAFAD2", + "LIGHTGOLDENRODYELLOW": "#FAFAD2", + "LightGray": "#D3D3D3", + "lightgray": "#D3D3D3", + "LIGHTGRAY": "#D3D3D3", + "LightGreen": "#90EE90", + "lightgreen": "#90EE90", + "LIGHTGREEN": "#90EE90", + "LightGrey": "#D3D3D3", + "lightgrey": "#D3D3D3", + "LIGHTGREY": "#D3D3D3", + "LightPink": "#FFB6C1", + "lightpink": "#FFB6C1", + "LIGHTPINK": "#FFB6C1", + "LightPink1": "#FFAEB9", + "lightpink1": "#FFAEB9", + "LIGHTPINK1": "#FFAEB9", + "LightPink2": "#EEA2AD", + "lightpink2": "#EEA2AD", + "LIGHTPINK2": "#EEA2AD", + "LightPink3": "#CD8C95", + "lightpink3": "#CD8C95", + "LIGHTPINK3": "#CD8C95", + "LightPink4": "#8B5F65", + "lightpink4": "#8B5F65", + "LIGHTPINK4": "#8B5F65", + "LightSalmon": "#FFA07A", + "lightsalmon": "#FFA07A", + "LIGHTSALMON": "#FFA07A", + "LightSalmon1": "#FFA07A", + "lightsalmon1": "#FFA07A", + "LIGHTSALMON1": "#FFA07A", + "LightSalmon2": "#EE9572", + "lightsalmon2": "#EE9572", + "LIGHTSALMON2": "#EE9572", + "LightSalmon3": "#CD8162", + "lightsalmon3": "#CD8162", + "LIGHTSALMON3": "#CD8162", + "LightSalmon4": "#8B5742", + "lightsalmon4": "#8B5742", + "LIGHTSALMON4": "#8B5742", + "LightSeaGreen": "#20B2AA", + "lightseagreen": "#20B2AA", + "LIGHTSEAGREEN": "#20B2AA", + "LightSkyBlue": "#87CEFA", + "lightskyblue": "#87CEFA", + "LIGHTSKYBLUE": "#87CEFA", + "LightSkyBlue1": "#B0E2FF", + "lightskyblue1": "#B0E2FF", + "LIGHTSKYBLUE1": "#B0E2FF", + "LightSkyBlue2": "#A4D3EE", + "lightskyblue2": "#A4D3EE", + "LIGHTSKYBLUE2": "#A4D3EE", + "LightSkyBlue3": "#8DB6CD", + "lightskyblue3": "#8DB6CD", + "LIGHTSKYBLUE3": "#8DB6CD", + "LightSkyBlue4": "#607B8B", + "lightskyblue4": "#607B8B", + "LIGHTSKYBLUE4": "#607B8B", + "LightSlateBlue": "#8470FF", + "lightslateblue": "#8470FF", + "LIGHTSLATEBLUE": "#8470FF", + "LightSlateGray": "#778899", + "lightslategray": "#778899", + "LIGHTSLATEGRAY": "#778899", + "LightSlateGrey": "#778899", + "lightslategrey": "#778899", + "LIGHTSLATEGREY": "#778899", + "LightSteelBlue": "#B0C4DE", + "lightsteelblue": "#B0C4DE", + "LIGHTSTEELBLUE": "#B0C4DE", + "LightSteelBlue1": "#CAE1FF", + "lightsteelblue1": "#CAE1FF", + "LIGHTSTEELBLUE1": "#CAE1FF", + "LightSteelBlue2": "#BCD2EE", + "lightsteelblue2": "#BCD2EE", + "LIGHTSTEELBLUE2": "#BCD2EE", + "LightSteelBlue3": "#A2B5CD", + "lightsteelblue3": "#A2B5CD", + "LIGHTSTEELBLUE3": "#A2B5CD", + "LightSteelBlue4": "#6E7B8B", + "lightsteelblue4": "#6E7B8B", + "LIGHTSTEELBLUE4": "#6E7B8B", + "LightYellow": "#FFFFE0", + "lightyellow": "#FFFFE0", + "LIGHTYELLOW": "#FFFFE0", + "LightYellow1": "#FFFFE0", + "lightyellow1": "#FFFFE0", + "LIGHTYELLOW1": "#FFFFE0", + "LightYellow2": "#EEEED1", + "lightyellow2": "#EEEED1", + "LIGHTYELLOW2": "#EEEED1", + "LightYellow3": "#CDCDB4", + "lightyellow3": "#CDCDB4", + "LIGHTYELLOW3": "#CDCDB4", + "LightYellow4": "#8B8B7A", + "lightyellow4": "#8B8B7A", + "LIGHTYELLOW4": "#8B8B7A", + "lime green": "#32CD32", + "LIME GREEN": "#32CD32", + "LimeGreen": "#32CD32", + "limegreen": "#32CD32", + "LIMEGREEN": "#32CD32", + "linen": "#FAF0E6", + "LINEN": "#FAF0E6", + "magenta": "#FF00FF", + "MAGENTA": "#FF00FF", + "magenta1": "#FF00FF", + "MAGENTA1": "#FF00FF", + "magenta2": "#EE00EE", + "MAGENTA2": "#EE00EE", + "magenta3": "#CD00CD", + "MAGENTA3": "#CD00CD", + "magenta4": "#8B008B", + "MAGENTA4": "#8B008B", + "maroon": "#B03060", + "MAROON": "#B03060", + "maroon1": "#FF34B3", + "MAROON1": "#FF34B3", + "maroon2": "#EE30A7", + "MAROON2": "#EE30A7", + "maroon3": "#CD2990", + "MAROON3": "#CD2990", + "maroon4": "#8B1C62", + "MAROON4": "#8B1C62", + "medium aquamarine": "#66CDAA", + "MEDIUM AQUAMARINE": "#66CDAA", + "medium blue": "#0000CD", + "MEDIUM BLUE": "#0000CD", + "medium orchid": "#BA55D3", + "MEDIUM ORCHID": "#BA55D3", + "medium purple": "#9370DB", + "MEDIUM PURPLE": "#9370DB", + "medium sea green": "#3CB371", + "MEDIUM SEA GREEN": "#3CB371", + "medium slate blue": "#7B68EE", + "MEDIUM SLATE BLUE": "#7B68EE", + "medium spring green": "#00FA9A", + "MEDIUM SPRING GREEN": "#00FA9A", + "medium turquoise": "#48D1CC", + "MEDIUM TURQUOISE": "#48D1CC", + "medium violet red": "#C71585", + "MEDIUM VIOLET RED": "#C71585", + "MediumAquamarine": "#66CDAA", + "mediumaquamarine": "#66CDAA", + "MEDIUMAQUAMARINE": "#66CDAA", + "MediumBlue": "#0000CD", + "mediumblue": "#0000CD", + "MEDIUMBLUE": "#0000CD", + "MediumOrchid": "#BA55D3", + "mediumorchid": "#BA55D3", + "MEDIUMORCHID": "#BA55D3", + "MediumOrchid1": "#E066FF", + "mediumorchid1": "#E066FF", + "MEDIUMORCHID1": "#E066FF", + "MediumOrchid2": "#D15FEE", + "mediumorchid2": "#D15FEE", + "MEDIUMORCHID2": "#D15FEE", + "MediumOrchid3": "#B452CD", + "mediumorchid3": "#B452CD", + "MEDIUMORCHID3": "#B452CD", + "MediumOrchid4": "#7A378B", + "mediumorchid4": "#7A378B", + "MEDIUMORCHID4": "#7A378B", + "MediumPurple": "#9370DB", + "mediumpurple": "#9370DB", + "MEDIUMPURPLE": "#9370DB", + "MediumPurple1": "#AB82FF", + "mediumpurple1": "#AB82FF", + "MEDIUMPURPLE1": "#AB82FF", + "MediumPurple2": "#9F79EE", + "mediumpurple2": "#9F79EE", + "MEDIUMPURPLE2": "#9F79EE", + "MediumPurple3": "#8968CD", + "mediumpurple3": "#8968CD", + "MEDIUMPURPLE3": "#8968CD", + "MediumPurple4": "#5D478B", + "mediumpurple4": "#5D478B", + "MEDIUMPURPLE4": "#5D478B", + "MediumSeaGreen": "#3CB371", + "mediumseagreen": "#3CB371", + "MEDIUMSEAGREEN": "#3CB371", + "MediumSlateBlue": "#7B68EE", + "mediumslateblue": "#7B68EE", + "MEDIUMSLATEBLUE": "#7B68EE", + "MediumSpringGreen": "#00FA9A", + "mediumspringgreen": "#00FA9A", + "MEDIUMSPRINGGREEN": "#00FA9A", + "MediumTurquoise": "#48D1CC", + "mediumturquoise": "#48D1CC", + "MEDIUMTURQUOISE": "#48D1CC", + "MediumVioletRed": "#C71585", + "mediumvioletred": "#C71585", + "MEDIUMVIOLETRED": "#C71585", + "midnight blue": "#191970", + "MIDNIGHT BLUE": "#191970", + "MidnightBlue": "#191970", + "midnightblue": "#191970", + "MIDNIGHTBLUE": "#191970", + "mint cream": "#F5FFFA", + "MINT CREAM": "#F5FFFA", + "MintCream": "#F5FFFA", + "mintcream": "#F5FFFA", + "MINTCREAM": "#F5FFFA", + "misty rose": "#FFE4E1", + "MISTY ROSE": "#FFE4E1", + "MistyRose": "#FFE4E1", + "mistyrose": "#FFE4E1", + "MISTYROSE": "#FFE4E1", + "MistyRose1": "#FFE4E1", + "mistyrose1": "#FFE4E1", + "MISTYROSE1": "#FFE4E1", + "MistyRose2": "#EED5D2", + "mistyrose2": "#EED5D2", + "MISTYROSE2": "#EED5D2", + "MistyRose3": "#CDB7B5", + "mistyrose3": "#CDB7B5", + "MISTYROSE3": "#CDB7B5", + "MistyRose4": "#8B7D7B", + "mistyrose4": "#8B7D7B", + "MISTYROSE4": "#8B7D7B", + "moccasin": "#FFE4B5", + "MOCCASIN": "#FFE4B5", + "navajo white": "#FFDEAD", + "NAVAJO WHITE": "#FFDEAD", + "NavajoWhite": "#FFDEAD", + "navajowhite": "#FFDEAD", + "NAVAJOWHITE": "#FFDEAD", + "NavajoWhite1": "#FFDEAD", + "navajowhite1": "#FFDEAD", + "NAVAJOWHITE1": "#FFDEAD", + "NavajoWhite2": "#EECFA1", + "navajowhite2": "#EECFA1", + "NAVAJOWHITE2": "#EECFA1", + "NavajoWhite3": "#CDB38B", + "navajowhite3": "#CDB38B", + "NAVAJOWHITE3": "#CDB38B", + "NavajoWhite4": "#8B795E", + "navajowhite4": "#8B795E", + "NAVAJOWHITE4": "#8B795E", + "navy": "#000080", + "NAVY": "#000080", + "navy blue": "#000080", + "NAVY BLUE": "#000080", + "NavyBlue": "#000080", + "navyblue": "#000080", + "NAVYBLUE": "#000080", + "old lace": "#FDF5E6", + "OLD LACE": "#FDF5E6", + "OldLace": "#FDF5E6", + "oldlace": "#FDF5E6", + "OLDLACE": "#FDF5E6", + "olive drab": "#6B8E23", + "OLIVE DRAB": "#6B8E23", + "OliveDrab": "#6B8E23", + "olivedrab": "#6B8E23", + "OLIVEDRAB": "#6B8E23", + "OliveDrab1": "#C0FF3E", + "olivedrab1": "#C0FF3E", + "OLIVEDRAB1": "#C0FF3E", + "OliveDrab2": "#B3EE3A", + "olivedrab2": "#B3EE3A", + "OLIVEDRAB2": "#B3EE3A", + "OliveDrab3": "#9ACD32", + "olivedrab3": "#9ACD32", + "OLIVEDRAB3": "#9ACD32", + "OliveDrab4": "#698B22", + "olivedrab4": "#698B22", + "OLIVEDRAB4": "#698B22", + "orange": "#FFA500", + "ORANGE": "#FFA500", + "orange red": "#FF4500", + "ORANGE RED": "#FF4500", + "orange1": "#FFA500", + "ORANGE1": "#FFA500", + "orange2": "#EE9A00", + "ORANGE2": "#EE9A00", + "orange3": "#CD8500", + "ORANGE3": "#CD8500", + "orange4": "#8B5A00", + "ORANGE4": "#8B5A00", + "OrangeRed": "#FF4500", + "orangered": "#FF4500", + "ORANGERED": "#FF4500", + "OrangeRed1": "#FF4500", + "orangered1": "#FF4500", + "ORANGERED1": "#FF4500", + "OrangeRed2": "#EE4000", + "orangered2": "#EE4000", + "ORANGERED2": "#EE4000", + "OrangeRed3": "#CD3700", + "orangered3": "#CD3700", + "ORANGERED3": "#CD3700", + "OrangeRed4": "#8B2500", + "orangered4": "#8B2500", + "ORANGERED4": "#8B2500", + "orchid": "#DA70D6", + "ORCHID": "#DA70D6", + "orchid1": "#FF83FA", + "ORCHID1": "#FF83FA", + "orchid2": "#EE7AE9", + "ORCHID2": "#EE7AE9", + "orchid3": "#CD69C9", + "ORCHID3": "#CD69C9", + "orchid4": "#8B4789", + "ORCHID4": "#8B4789", + "pale goldenrod": "#EEE8AA", + "PALE GOLDENROD": "#EEE8AA", + "pale green": "#98FB98", + "PALE GREEN": "#98FB98", + "pale turquoise": "#AFEEEE", + "PALE TURQUOISE": "#AFEEEE", + "pale violet red": "#DB7093", + "PALE VIOLET RED": "#DB7093", + "PaleGoldenrod": "#EEE8AA", + "palegoldenrod": "#EEE8AA", + "PALEGOLDENROD": "#EEE8AA", + "PaleGreen": "#98FB98", + "palegreen": "#98FB98", + "PALEGREEN": "#98FB98", + "PaleGreen1": "#9AFF9A", + "palegreen1": "#9AFF9A", + "PALEGREEN1": "#9AFF9A", + "PaleGreen2": "#90EE90", + "palegreen2": "#90EE90", + "PALEGREEN2": "#90EE90", + "PaleGreen3": "#7CCD7C", + "palegreen3": "#7CCD7C", + "PALEGREEN3": "#7CCD7C", + "PaleGreen4": "#548B54", + "palegreen4": "#548B54", + "PALEGREEN4": "#548B54", + "PaleTurquoise": "#AFEEEE", + "paleturquoise": "#AFEEEE", + "PALETURQUOISE": "#AFEEEE", + "PaleTurquoise1": "#BBFFFF", + "paleturquoise1": "#BBFFFF", + "PALETURQUOISE1": "#BBFFFF", + "PaleTurquoise2": "#AEEEEE", + "paleturquoise2": "#AEEEEE", + "PALETURQUOISE2": "#AEEEEE", + "PaleTurquoise3": "#96CDCD", + "paleturquoise3": "#96CDCD", + "PALETURQUOISE3": "#96CDCD", + "PaleTurquoise4": "#668B8B", + "paleturquoise4": "#668B8B", + "PALETURQUOISE4": "#668B8B", + "PaleVioletRed": "#DB7093", + "palevioletred": "#DB7093", + "PALEVIOLETRED": "#DB7093", + "PaleVioletRed1": "#FF82AB", + "palevioletred1": "#FF82AB", + "PALEVIOLETRED1": "#FF82AB", + "PaleVioletRed2": "#EE799F", + "palevioletred2": "#EE799F", + "PALEVIOLETRED2": "#EE799F", + "PaleVioletRed3": "#CD687F", + "palevioletred3": "#CD687F", + "PALEVIOLETRED3": "#CD687F", + "PaleVioletRed4": "#8B475D", + "palevioletred4": "#8B475D", + "PALEVIOLETRED4": "#8B475D", + "papaya whip": "#FFEFD5", + "PAPAYA WHIP": "#FFEFD5", + "PapayaWhip": "#FFEFD5", + "papayawhip": "#FFEFD5", + "PAPAYAWHIP": "#FFEFD5", + "peach puff": "#FFDAB9", + "PEACH PUFF": "#FFDAB9", + "PeachPuff": "#FFDAB9", + "peachpuff": "#FFDAB9", + "PEACHPUFF": "#FFDAB9", + "PeachPuff1": "#FFDAB9", + "peachpuff1": "#FFDAB9", + "PEACHPUFF1": "#FFDAB9", + "PeachPuff2": "#EECBAD", + "peachpuff2": "#EECBAD", + "PEACHPUFF2": "#EECBAD", + "PeachPuff3": "#CDAF95", + "peachpuff3": "#CDAF95", + "PEACHPUFF3": "#CDAF95", + "PeachPuff4": "#8B7765", + "peachpuff4": "#8B7765", + "PEACHPUFF4": "#8B7765", + "peru": "#CD853F", + "PERU": "#CD853F", + "pink": "#FFC0CB", + "PINK": "#FFC0CB", + "pink1": "#FFB5C5", + "PINK1": "#FFB5C5", + "pink2": "#EEA9B8", + "PINK2": "#EEA9B8", + "pink3": "#CD919E", + "PINK3": "#CD919E", + "pink4": "#8B636C", + "PINK4": "#8B636C", + "plum": "#DDA0DD", + "PLUM": "#DDA0DD", + "plum1": "#FFBBFF", + "PLUM1": "#FFBBFF", + "plum2": "#EEAEEE", + "PLUM2": "#EEAEEE", + "plum3": "#CD96CD", + "PLUM3": "#CD96CD", + "plum4": "#8B668B", + "PLUM4": "#8B668B", + "powder blue": "#B0E0E6", + "POWDER BLUE": "#B0E0E6", + "PowderBlue": "#B0E0E6", + "powderblue": "#B0E0E6", + "POWDERBLUE": "#B0E0E6", + "purple": "#A020F0", + "PURPLE": "#A020F0", + "purple1": "#9B30FF", + "PURPLE1": "#9B30FF", + "purple2": "#912CEE", + "PURPLE2": "#912CEE", + "purple3": "#7D26CD", + "PURPLE3": "#7D26CD", + "purple4": "#551A8B", + "PURPLE4": "#551A8B", + "red": "#FF0000", + "RED": "#FF0000", + "red1": "#FF0000", + "RED1": "#FF0000", + "red2": "#EE0000", + "RED2": "#EE0000", + "red3": "#CD0000", + "RED3": "#CD0000", + "red4": "#8B0000", + "RED4": "#8B0000", + "rosy brown": "#BC8F8F", + "ROSY BROWN": "#BC8F8F", + "RosyBrown": "#BC8F8F", + "rosybrown": "#BC8F8F", + "ROSYBROWN": "#BC8F8F", + "RosyBrown1": "#FFC1C1", + "rosybrown1": "#FFC1C1", + "ROSYBROWN1": "#FFC1C1", + "RosyBrown2": "#EEB4B4", + "rosybrown2": "#EEB4B4", + "ROSYBROWN2": "#EEB4B4", + "RosyBrown3": "#CD9B9B", + "rosybrown3": "#CD9B9B", + "ROSYBROWN3": "#CD9B9B", + "RosyBrown4": "#8B6969", + "rosybrown4": "#8B6969", + "ROSYBROWN4": "#8B6969", + "royal blue": "#4169E1", + "ROYAL BLUE": "#4169E1", + "RoyalBlue": "#4169E1", + "royalblue": "#4169E1", + "ROYALBLUE": "#4169E1", + "RoyalBlue1": "#4876FF", + "royalblue1": "#4876FF", + "ROYALBLUE1": "#4876FF", + "RoyalBlue2": "#436EEE", + "royalblue2": "#436EEE", + "ROYALBLUE2": "#436EEE", + "RoyalBlue3": "#3A5FCD", + "royalblue3": "#3A5FCD", + "ROYALBLUE3": "#3A5FCD", + "RoyalBlue4": "#27408B", + "royalblue4": "#27408B", + "ROYALBLUE4": "#27408B", + "saddle brown": "#8B4513", + "SADDLE BROWN": "#8B4513", + "SaddleBrown": "#8B4513", + "saddlebrown": "#8B4513", + "SADDLEBROWN": "#8B4513", + "salmon": "#FA8072", + "SALMON": "#FA8072", + "salmon1": "#FF8C69", + "SALMON1": "#FF8C69", + "salmon2": "#EE8262", + "SALMON2": "#EE8262", + "salmon3": "#CD7054", + "SALMON3": "#CD7054", + "salmon4": "#8B4C39", + "SALMON4": "#8B4C39", + "sandy brown": "#F4A460", + "SANDY BROWN": "#F4A460", + "SandyBrown": "#F4A460", + "sandybrown": "#F4A460", + "SANDYBROWN": "#F4A460", + "sea green": "#2E8B57", + "SEA GREEN": "#2E8B57", + "SeaGreen": "#2E8B57", + "seagreen": "#2E8B57", + "SEAGREEN": "#2E8B57", + "SeaGreen1": "#54FF9F", + "seagreen1": "#54FF9F", + "SEAGREEN1": "#54FF9F", + "SeaGreen2": "#4EEE94", + "seagreen2": "#4EEE94", + "SEAGREEN2": "#4EEE94", + "SeaGreen3": "#43CD80", + "seagreen3": "#43CD80", + "SEAGREEN3": "#43CD80", + "SeaGreen4": "#2E8B57", + "seagreen4": "#2E8B57", + "SEAGREEN4": "#2E8B57", + "seashell": "#FFF5EE", + "SEASHELL": "#FFF5EE", + "seashell1": "#FFF5EE", + "SEASHELL1": "#FFF5EE", + "seashell2": "#EEE5DE", + "SEASHELL2": "#EEE5DE", + "seashell3": "#CDC5BF", + "SEASHELL3": "#CDC5BF", + "seashell4": "#8B8682", + "SEASHELL4": "#8B8682", + "sienna": "#A0522D", + "SIENNA": "#A0522D", + "sienna1": "#FF8247", + "SIENNA1": "#FF8247", + "sienna2": "#EE7942", + "SIENNA2": "#EE7942", + "sienna3": "#CD6839", + "SIENNA3": "#CD6839", + "sienna4": "#8B4726", + "SIENNA4": "#8B4726", + "sky blue": "#87CEEB", + "SKY BLUE": "#87CEEB", + "SkyBlue": "#87CEEB", + "skyblue": "#87CEEB", + "SKYBLUE": "#87CEEB", + "SkyBlue1": "#87CEFF", + "skyblue1": "#87CEFF", + "SKYBLUE1": "#87CEFF", + "SkyBlue2": "#7EC0EE", + "skyblue2": "#7EC0EE", + "SKYBLUE2": "#7EC0EE", + "SkyBlue3": "#6CA6CD", + "skyblue3": "#6CA6CD", + "SKYBLUE3": "#6CA6CD", + "SkyBlue4": "#4A708B", + "skyblue4": "#4A708B", + "SKYBLUE4": "#4A708B", + "slate blue": "#6A5ACD", + "SLATE BLUE": "#6A5ACD", + "slate gray": "#708090", + "SLATE GRAY": "#708090", + "slate grey": "#708090", + "SLATE GREY": "#708090", + "SlateBlue": "#6A5ACD", + "slateblue": "#6A5ACD", + "SLATEBLUE": "#6A5ACD", + "SlateBlue1": "#836FFF", + "slateblue1": "#836FFF", + "SLATEBLUE1": "#836FFF", + "SlateBlue2": "#7A67EE", + "slateblue2": "#7A67EE", + "SLATEBLUE2": "#7A67EE", + "SlateBlue3": "#6959CD", + "slateblue3": "#6959CD", + "SLATEBLUE3": "#6959CD", + "SlateBlue4": "#473C8B", + "slateblue4": "#473C8B", + "SLATEBLUE4": "#473C8B", + "SlateGray": "#708090", + "slategray": "#708090", + "SLATEGRAY": "#708090", + "SlateGray1": "#C6E2FF", + "slategray1": "#C6E2FF", + "SLATEGRAY1": "#C6E2FF", + "SlateGray2": "#B9D3EE", + "slategray2": "#B9D3EE", + "SLATEGRAY2": "#B9D3EE", + "SlateGray3": "#9FB6CD", + "slategray3": "#9FB6CD", + "SLATEGRAY3": "#9FB6CD", + "SlateGray4": "#6C7B8B", + "slategray4": "#6C7B8B", + "SLATEGRAY4": "#6C7B8B", + "SlateGrey": "#708090", + "slategrey": "#708090", + "SLATEGREY": "#708090", + "snow": "#FFFAFA", + "SNOW": "#FFFAFA", + "snow1": "#FFFAFA", + "SNOW1": "#FFFAFA", + "snow2": "#EEE9E9", + "SNOW2": "#EEE9E9", + "snow3": "#CDC9C9", + "SNOW3": "#CDC9C9", + "snow4": "#8B8989", + "SNOW4": "#8B8989", + "spring green": "#00FF7F", + "SPRING GREEN": "#00FF7F", + "SpringGreen": "#00FF7F", + "springgreen": "#00FF7F", + "SPRINGGREEN": "#00FF7F", + "SpringGreen1": "#00FF7F", + "springgreen1": "#00FF7F", + "SPRINGGREEN1": "#00FF7F", + "SpringGreen2": "#00EE76", + "springgreen2": "#00EE76", + "SPRINGGREEN2": "#00EE76", + "SpringGreen3": "#00CD66", + "springgreen3": "#00CD66", + "SPRINGGREEN3": "#00CD66", + "SpringGreen4": "#008B45", + "springgreen4": "#008B45", + "SPRINGGREEN4": "#008B45", + "steel blue": "#4682B4", + "STEEL BLUE": "#4682B4", + "SteelBlue": "#4682B4", + "steelblue": "#4682B4", + "STEELBLUE": "#4682B4", + "SteelBlue1": "#63B8FF", + "steelblue1": "#63B8FF", + "STEELBLUE1": "#63B8FF", + "SteelBlue2": "#5CACEE", + "steelblue2": "#5CACEE", + "STEELBLUE2": "#5CACEE", + "SteelBlue3": "#4F94CD", + "steelblue3": "#4F94CD", + "STEELBLUE3": "#4F94CD", + "SteelBlue4": "#36648B", + "steelblue4": "#36648B", + "STEELBLUE4": "#36648B", + "tan": "#D2B48C", + "TAN": "#D2B48C", + "tan1": "#FFA54F", + "TAN1": "#FFA54F", + "tan2": "#EE9A49", + "TAN2": "#EE9A49", + "tan3": "#CD853F", + "TAN3": "#CD853F", + "tan4": "#8B5A2B", + "TAN4": "#8B5A2B", + "thistle": "#D8BFD8", + "THISTLE": "#D8BFD8", + "thistle1": "#FFE1FF", + "THISTLE1": "#FFE1FF", + "thistle2": "#EED2EE", + "THISTLE2": "#EED2EE", + "thistle3": "#CDB5CD", + "THISTLE3": "#CDB5CD", + "thistle4": "#8B7B8B", + "THISTLE4": "#8B7B8B", + "tomato": "#FF6347", + "TOMATO": "#FF6347", + "tomato1": "#FF6347", + "TOMATO1": "#FF6347", + "tomato2": "#EE5C42", + "TOMATO2": "#EE5C42", + "tomato3": "#CD4F39", + "TOMATO3": "#CD4F39", + "tomato4": "#8B3626", + "TOMATO4": "#8B3626", + "turquoise": "#40E0D0", + "TURQUOISE": "#40E0D0", + "turquoise1": "#00F5FF", + "TURQUOISE1": "#00F5FF", + "turquoise2": "#00E5EE", + "TURQUOISE2": "#00E5EE", + "turquoise3": "#00C5CD", + "TURQUOISE3": "#00C5CD", + "turquoise4": "#00868B", + "TURQUOISE4": "#00868B", + "violet": "#EE82EE", + "VIOLET": "#EE82EE", + "violet red": "#D02090", + "VIOLET RED": "#D02090", + "VioletRed": "#D02090", + "violetred": "#D02090", + "VIOLETRED": "#D02090", + "VioletRed1": "#FF3E96", + "violetred1": "#FF3E96", + "VIOLETRED1": "#FF3E96", + "VioletRed2": "#EE3A8C", + "violetred2": "#EE3A8C", + "VIOLETRED2": "#EE3A8C", + "VioletRed3": "#CD3278", + "violetred3": "#CD3278", + "VIOLETRED3": "#CD3278", + "VioletRed4": "#8B2252", + "violetred4": "#8B2252", + "VIOLETRED4": "#8B2252", + "wheat": "#F5DEB3", + "WHEAT": "#F5DEB3", + "wheat1": "#FFE7BA", + "WHEAT1": "#FFE7BA", + "wheat2": "#EED8AE", + "WHEAT2": "#EED8AE", + "wheat3": "#CDBA96", + "WHEAT3": "#CDBA96", + "wheat4": "#8B7E66", + "WHEAT4": "#8B7E66", + "white": "#FFFFFF", + "WHITE": "#FFFFFF", + "white smoke": "#F5F5F5", + "WHITE SMOKE": "#F5F5F5", + "WhiteSmoke": "#F5F5F5", + "whitesmoke": "#F5F5F5", + "WHITESMOKE": "#F5F5F5", + "yellow": "#FFFF00", + "YELLOW": "#FFFF00", + "yellow green": "#9ACD32", + "YELLOW GREEN": "#9ACD32", + "yellow1": "#FFFF00", + "YELLOW1": "#FFFF00", + "yellow2": "#EEEE00", + "YELLOW2": "#EEEE00", + "yellow3": "#CDCD00", + "YELLOW3": "#CDCD00", + "yellow4": "#8B8B00", + "YELLOW4": "#8B8B00", + "YellowGreen": "#9ACD32", + "yellowgreen": "#9ACD32", + "YELLOWGREEN": "#9ACD32", +}