2021-08-23 16:19:46 +03:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
#
|
|
|
|
|
# The MIT License (MIT)
|
|
|
|
|
#
|
2023-02-18 11:30:06 +03:00
|
|
|
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
2021-08-23 16:19:46 +03:00
|
|
|
#
|
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
|
|
|
# in the Software without restriction, including without limitation the rights
|
|
|
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
|
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
|
#
|
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
|
|
|
# all copies or substantial portions of the Software.
|
|
|
|
|
#
|
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
|
|
# THE SOFTWARE.
|
|
|
|
|
#
|
|
|
|
|
# Author: Dmitriy Yefremov
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
2017-11-09 19:01:09 +03:00
|
|
|
""" Common module for showing dialogs """
|
2021-08-17 11:00:13 +03:00
|
|
|
import gettext
|
2022-03-26 12:12:22 +03:00
|
|
|
import xml.etree.ElementTree as ET
|
2017-12-09 16:25:54 +03:00
|
|
|
from enum import Enum
|
2019-05-09 14:48:29 +03:00
|
|
|
from functools import lru_cache
|
2020-09-17 10:00:02 +03:00
|
|
|
from pathlib import Path
|
2017-12-09 16:25:54 +03:00
|
|
|
|
2018-02-18 17:14:02 +03:00
|
|
|
from app.commons import run_idle
|
2023-02-18 11:30:06 +03:00
|
|
|
from app.settings import SEP, IS_WIN, USE_HEADER_BAR
|
|
|
|
|
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
|
2019-05-08 23:05:32 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Dialog(Enum):
|
2019-05-08 23:35:42 +03:00
|
|
|
MESSAGE = """
|
|
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<interface>
|
2019-05-09 11:11:54 +03:00
|
|
|
<requires lib="gtk+" version="3.16"/>
|
2019-05-08 23:35:42 +03:00
|
|
|
<object class="GtkMessageDialog" id="message_dialog">
|
|
|
|
|
<property name="use-header-bar">{use_header}</property>
|
|
|
|
|
<property name="can_focus">False</property>
|
|
|
|
|
<property name="modal">True</property>
|
2021-08-23 16:19:46 +03:00
|
|
|
<property name="width_request">250</property>
|
2019-05-08 23:35:42 +03:00
|
|
|
<property name="destroy_with_parent">True</property>
|
|
|
|
|
<property name="type_hint">dialog</property>
|
2019-05-09 11:11:54 +03:00
|
|
|
<property name="skip_taskbar_hint">True</property>
|
|
|
|
|
<property name="skip_pager_hint">True</property>
|
|
|
|
|
<property name="message_type">{message_type}</property>
|
|
|
|
|
<property name="buttons">{buttons_type}</property>
|
2019-05-08 23:35:42 +03:00
|
|
|
</object>
|
|
|
|
|
</interface>
|
|
|
|
|
"""
|
2019-05-04 11:21:20 +03:00
|
|
|
|
2017-11-09 19:01:09 +03:00
|
|
|
|
2018-03-11 21:52:10 +03:00
|
|
|
class Action(Enum):
|
|
|
|
|
EDIT = 0
|
|
|
|
|
ADD = 1
|
|
|
|
|
|
|
|
|
|
|
2017-12-09 16:25:54 +03:00
|
|
|
class DialogType(Enum):
|
2019-05-09 11:11:54 +03:00
|
|
|
INPUT = "input"
|
|
|
|
|
CHOOSER = "chooser"
|
|
|
|
|
ERROR = "error"
|
|
|
|
|
QUESTION = "question"
|
|
|
|
|
INFO = "info"
|
2019-05-09 12:53:11 +03:00
|
|
|
ABOUT = "about"
|
|
|
|
|
WAIT = "wait"
|
2019-05-09 11:11:54 +03:00
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.value
|
2018-02-18 17:14:02 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class WaitDialog:
|
2018-04-09 21:28:19 +03:00
|
|
|
def __init__(self, transient, text=None):
|
2018-02-18 17:14:02 +03:00
|
|
|
builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient)
|
|
|
|
|
self._dialog = dialog
|
|
|
|
|
self._dialog.set_transient_for(transient)
|
2020-04-27 17:00:52 +03:00
|
|
|
self._label = builder.get_object("wait_dialog_label")
|
|
|
|
|
self._default_text = text or self._label.get_text()
|
2018-02-18 17:14:02 +03:00
|
|
|
|
2020-04-27 17:00:52 +03:00
|
|
|
def show(self, text=None):
|
|
|
|
|
self.set_text(text)
|
2018-02-18 17:14:02 +03:00
|
|
|
self._dialog.show()
|
|
|
|
|
|
2020-04-27 17:00:52 +03:00
|
|
|
@run_idle
|
|
|
|
|
def set_text(self, text):
|
2023-05-13 13:31:42 +03:00
|
|
|
self._label.set_text(translate(text or self._default_text))
|
2020-04-27 17:00:52 +03:00
|
|
|
|
2018-02-18 17:14:02 +03:00
|
|
|
@run_idle
|
|
|
|
|
def hide(self):
|
|
|
|
|
self._dialog.hide()
|
2017-12-09 16:25:54 +03:00
|
|
|
|
2018-04-09 21:28:19 +03:00
|
|
|
@run_idle
|
|
|
|
|
def destroy(self):
|
|
|
|
|
self._dialog.destroy()
|
|
|
|
|
|
2017-12-09 16:25:54 +03:00
|
|
|
|
2020-09-17 10:00:02 +03:00
|
|
|
def show_dialog(dialog_type, transient, text=None, settings=None, action_type=None, file_filter=None, buttons=None,
|
2020-12-13 15:19:01 +03:00
|
|
|
title=None, create_dir=False):
|
2020-09-17 10:00:02 +03:00
|
|
|
""" Shows dialogs by name. """
|
2019-05-09 11:11:54 +03:00
|
|
|
if dialog_type in (DialogType.INFO, DialogType.ERROR):
|
|
|
|
|
return get_message_dialog(transient, dialog_type, Gtk.ButtonsType.OK, text)
|
2019-12-13 13:31:07 +03:00
|
|
|
elif dialog_type is DialogType.CHOOSER and settings:
|
2020-12-13 15:19:01 +03:00
|
|
|
return get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons, title, create_dir)
|
2019-05-04 11:21:20 +03:00
|
|
|
elif dialog_type is DialogType.INPUT:
|
|
|
|
|
return get_input_dialog(transient, text)
|
2019-05-08 23:35:42 +03:00
|
|
|
elif dialog_type is DialogType.QUESTION:
|
2020-06-04 11:32:53 +03:00
|
|
|
action = action_type if action_type else Gtk.ButtonsType.OK_CANCEL
|
|
|
|
|
return get_message_dialog(transient, DialogType.QUESTION, action, text or "Are you sure?")
|
2019-05-09 12:53:11 +03:00
|
|
|
elif dialog_type is DialogType.ABOUT:
|
|
|
|
|
return get_about_dialog(transient)
|
2017-11-09 19:01:09 +03:00
|
|
|
|
2017-12-30 21:51:57 +03:00
|
|
|
|
2021-09-27 19:58:34 +03:00
|
|
|
def get_chooser_dialog(transient, settings, name, patterns, title=None, file_filter=None):
|
|
|
|
|
if not file_filter:
|
|
|
|
|
file_filter = Gtk.FileFilter()
|
|
|
|
|
file_filter.set_name(name)
|
|
|
|
|
for p in patterns:
|
|
|
|
|
file_filter.add_pattern(p)
|
2019-05-04 11:21:20 +03:00
|
|
|
|
|
|
|
|
return show_dialog(dialog_type=DialogType.CHOOSER,
|
|
|
|
|
transient=transient,
|
2019-12-13 13:31:07 +03:00
|
|
|
settings=settings,
|
2019-05-04 11:21:20 +03:00
|
|
|
action_type=Gtk.FileChooserAction.OPEN,
|
2020-09-17 10:00:02 +03:00
|
|
|
file_filter=file_filter,
|
|
|
|
|
title=title)
|
2017-12-09 16:25:54 +03:00
|
|
|
|
|
|
|
|
|
2020-12-13 15:19:01 +03:00
|
|
|
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
|
2020-09-17 10:00:02 +03:00
|
|
|
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
|
2023-05-13 13:31:42 +03:00
|
|
|
dialog = Gtk.FileChooserNative.new(translate(title) if title else "", transient, action_type)
|
2020-12-13 15:19:01 +03:00
|
|
|
dialog.set_create_folders(dirs)
|
2021-08-23 16:19:46 +03:00
|
|
|
dialog.set_modal(True)
|
2020-09-17 10:00:02 +03:00
|
|
|
|
2019-05-04 11:21:20 +03:00
|
|
|
if file_filter is not None:
|
|
|
|
|
dialog.add_filter(file_filter)
|
|
|
|
|
|
2021-08-30 15:04:15 +03:00
|
|
|
dialog.set_current_folder(settings.profile_data_path)
|
2019-05-04 11:21:20 +03:00
|
|
|
response = dialog.run()
|
2020-09-17 10:00:02 +03:00
|
|
|
|
2021-08-23 16:19:46 +03:00
|
|
|
if response == Gtk.ResponseType.ACCEPT:
|
2020-09-17 10:00:02 +03:00
|
|
|
path = Path(dialog.get_filename() or dialog.get_current_folder())
|
|
|
|
|
if path.is_dir():
|
2021-08-23 16:19:46 +03:00
|
|
|
response = "{}{}".format(path.resolve(), SEP)
|
2020-09-17 10:00:02 +03:00
|
|
|
elif path.is_file():
|
|
|
|
|
response = str(path.resolve())
|
2019-05-04 11:21:20 +03:00
|
|
|
dialog.destroy()
|
2017-12-09 16:25:54 +03:00
|
|
|
|
2019-05-04 11:21:20 +03:00
|
|
|
return response
|
2017-11-09 19:01:09 +03:00
|
|
|
|
2017-12-09 16:25:54 +03:00
|
|
|
|
2019-05-04 11:21:20 +03:00
|
|
|
def get_input_dialog(transient, text):
|
2023-02-18 11:30:06 +03:00
|
|
|
builder, dialog = get_dialog_from_xml(DialogType.INPUT, transient, use_header=USE_HEADER_BAR)
|
2019-05-04 11:21:20 +03:00
|
|
|
entry = builder.get_object("input_entry")
|
|
|
|
|
entry.set_text(text if text else "")
|
2017-11-09 19:01:09 +03:00
|
|
|
response = dialog.run()
|
2019-05-04 11:21:20 +03:00
|
|
|
txt = entry.get_text()
|
2018-03-13 10:42:56 +03:00
|
|
|
dialog.destroy()
|
2017-11-09 19:01:09 +03:00
|
|
|
|
2019-05-04 11:21:20 +03:00
|
|
|
return txt if response == Gtk.ResponseType.OK else Gtk.ResponseType.CANCEL
|
2017-11-09 19:01:09 +03:00
|
|
|
|
|
|
|
|
|
2019-05-09 11:11:54 +03:00
|
|
|
def get_message_dialog(transient, message_type, buttons_type, text):
|
2019-05-08 23:35:42 +03:00
|
|
|
builder = Gtk.Builder()
|
2019-05-09 00:01:49 +03:00
|
|
|
builder.set_translation_domain(TEXT_DOMAIN)
|
2019-05-11 00:09:20 +03:00
|
|
|
dialog_str = Dialog.MESSAGE.value.format(use_header=0, message_type=message_type, buttons_type=int(buttons_type))
|
|
|
|
|
builder.add_from_string(dialog_str)
|
2019-05-08 23:35:42 +03:00
|
|
|
dialog = builder.get_object("message_dialog")
|
|
|
|
|
dialog.set_transient_for(transient)
|
2023-05-13 13:31:42 +03:00
|
|
|
dialog.set_markup(translate(text))
|
2019-05-08 23:35:42 +03:00
|
|
|
response = dialog.run()
|
|
|
|
|
dialog.destroy()
|
2019-05-09 12:53:11 +03:00
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_about_dialog(transient):
|
|
|
|
|
builder, dialog = get_dialog_from_xml(DialogType.ABOUT, transient)
|
|
|
|
|
dialog.set_transient_for(transient)
|
|
|
|
|
response = dialog.run()
|
|
|
|
|
dialog.destroy()
|
|
|
|
|
|
2019-05-08 23:35:42 +03:00
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
2019-05-11 00:09:20 +03:00
|
|
|
def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
|
2019-05-09 12:53:11 +03:00
|
|
|
dialog_name = dialog_type.value + "_dialog"
|
2018-02-18 17:14:02 +03:00
|
|
|
builder = Gtk.Builder()
|
2018-03-02 17:06:53 +03:00
|
|
|
builder.set_translation_domain(TEXT_DOMAIN)
|
2019-05-11 00:09:20 +03:00
|
|
|
dialog_str = get_dialogs_string(UI_RESOURCES_PATH + "dialogs.glade").format(use_header=use_header, title=title)
|
|
|
|
|
builder.add_objects_from_string(dialog_str, (dialog_name,))
|
2019-05-09 12:53:11 +03:00
|
|
|
dialog = builder.get_object(dialog_name)
|
2018-02-18 17:14:02 +03:00
|
|
|
dialog.set_transient_for(transient)
|
2018-03-13 10:42:56 +03:00
|
|
|
|
2018-02-18 17:14:02 +03:00
|
|
|
return builder, dialog
|
|
|
|
|
|
|
|
|
|
|
2023-05-13 13:31:42 +03:00
|
|
|
def translate(message):
|
2018-03-06 11:34:06 +03:00
|
|
|
""" returns translated message """
|
2021-08-17 11:00:13 +03:00
|
|
|
return gettext.dgettext(TEXT_DOMAIN, message)
|
2018-03-06 11:34:06 +03:00
|
|
|
|
|
|
|
|
|
2019-05-09 14:48:29 +03:00
|
|
|
@lru_cache(maxsize=5)
|
2021-08-23 16:19:46 +03:00
|
|
|
def get_dialogs_string(path, tag="property"):
|
|
|
|
|
if IS_WIN:
|
|
|
|
|
return translate_xml(path, tag)
|
|
|
|
|
else:
|
|
|
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
|
|
|
return "".join(f)
|
2019-05-09 14:48:29 +03:00
|
|
|
|
|
|
|
|
|
2021-08-23 16:19:46 +03:00
|
|
|
def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"):
|
2021-04-28 14:12:59 +03:00
|
|
|
""" Creates and returns a Gtk.Builder instance. """
|
|
|
|
|
builder = Gtk.Builder()
|
|
|
|
|
builder.set_translation_domain(TEXT_DOMAIN)
|
|
|
|
|
|
|
|
|
|
if use_str:
|
|
|
|
|
if objects:
|
2023-02-18 11:30:06 +03:00
|
|
|
builder.add_objects_from_string(get_dialogs_string(path, tag).format(use_header=USE_HEADER_BAR), objects)
|
2021-04-28 14:12:59 +03:00
|
|
|
else:
|
2023-02-18 11:30:06 +03:00
|
|
|
builder.add_from_string(get_dialogs_string(path, tag).format(use_header=USE_HEADER_BAR))
|
2021-04-28 14:12:59 +03:00
|
|
|
else:
|
|
|
|
|
if objects:
|
2021-08-23 16:19:46 +03:00
|
|
|
builder.add_objects_from_string(get_dialogs_string(path, tag), objects)
|
2021-04-28 14:12:59 +03:00
|
|
|
else:
|
2021-08-23 16:19:46 +03:00
|
|
|
builder.add_from_string(get_dialogs_string(path, tag))
|
2021-04-28 14:12:59 +03:00
|
|
|
|
|
|
|
|
builder.connect_signals(handlers or {})
|
|
|
|
|
|
|
|
|
|
return builder
|
|
|
|
|
|
|
|
|
|
|
2021-08-23 16:19:46 +03:00
|
|
|
def translate_xml(path, tag="property"):
|
2022-03-26 12:12:22 +03:00
|
|
|
""" Used to translate GUI from * .glade files in MS Windows.
|
2021-08-23 16:19:46 +03:00
|
|
|
|
|
|
|
|
More info: https://gitlab.gnome.org/GNOME/gtk/-/issues/569
|
|
|
|
|
"""
|
|
|
|
|
et = ET.parse(path)
|
|
|
|
|
root = et.getroot()
|
2022-03-26 12:12:22 +03:00
|
|
|
for e in root.iter():
|
|
|
|
|
if e.tag == tag and e.attrib.get("translatable", None) == "yes":
|
2023-05-13 13:31:42 +03:00
|
|
|
e.text = translate(e.text)
|
2022-03-26 12:12:22 +03:00
|
|
|
elif e.tag == "item" and e.attrib.get("translatable", None) == "yes":
|
2023-05-13 13:31:42 +03:00
|
|
|
e.text = translate(e.text)
|
2021-08-23 16:19:46 +03:00
|
|
|
|
|
|
|
|
return ET.tostring(root, encoding="unicode", method="xml")
|
|
|
|
|
|
|
|
|
|
|
2017-11-09 19:01:09 +03:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
pass
|