diff --git a/src/fime/config.py b/src/fime/config.py index eff46f2..f8025a8 100644 --- a/src/fime/config.py +++ b/src/fime/config.py @@ -1,4 +1,5 @@ from configparser import ConfigParser +from io import StringIO from pathlib import Path from loguru import logger @@ -22,32 +23,63 @@ class Config: def __init__(self): self._configparser = ConfigParser() config_dir_path = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppConfigLocation)) - config_path = config_dir_path / "fime.conf" - if config_path.exists(): - logger.info(f'Reading config file "{config_path}"') - with open(config_path) as f: + self.config_path = config_dir_path / "fime.conf" + if self.config_path.exists(): + logger.info(f'reading config file "{self.config_path}"') + with self.config_path.open(encoding="utf-8") as f: config_text = f.read() config_text = "[DEFAULT]\n" + config_text self._configparser.read_string(config_text) + # TODO change menu items if (not self._configparser.has_option("DEFAULT", "jira_url") or not self._configparser.has_option("DEFAULT", "jira_token")): - raise FimeException(f'Please add config file {config_path} ' + raise FimeException(f'Please add config file {self.config_path} ' f'with config keys "jira_url" and "jira_token" in INI style') + def save(self): + logger.info(f'writing config file "{self.config_path}"') + config_str = StringIO() + self._configparser.write(config_str) + # do not conform to configparser's stupid section requirement + config_str = "\n".join(config_str.getvalue().splitlines()[1:]) + with self.config_path.open("w", encoding="utf-8") as f: + f.write(config_str) + @property def jira_url(self): return dequotify(self._configparser["DEFAULT"]["jira_url"]) + @jira_url.setter + def jira_url(self, value): + self._configparser["DEFAULT"]["jira_url"] = f'"{value}"' + @property def jira_token(self): return dequotify(self._configparser["DEFAULT"]["jira_token"]) + @jira_token.setter + def jira_token(self, value): + self._configparser["DEFAULT"]["jira_token"] = f'"{value}"' + @property def tray_theme(self): val = dequotify(self._configparser.get("DEFAULT", "tray_theme", fallback="dark")).lower() return val if val in ["light", "dark"] else "dark" + @tray_theme.setter + def tray_theme(self, value): + value = value.lower() + if value not in ["light", "dark"]: + raise RuntimeError('config key "tray_theme" can only be set to "light" or "dark"') + self._configparser["DEFAULT"]["tray_theme"] = f'"{value}"' + @property def flip_menu(self): val = dequotify(self._configparser.get("DEFAULT", "flip_menu", fallback="no")).lower() return val in ["yes", "true", "1"] + + @flip_menu.setter + def flip_menu(self, value): + if type(value) is not bool: + raise RuntimeError('config key "flip_menu" must be a bool') + self._configparser["DEFAULT"]["flip_menu"] = f'"{value}"' diff --git a/src/fime/import_task.py b/src/fime/import_task.py index 3dc2aff..737e3d5 100644 --- a/src/fime/import_task.py +++ b/src/fime/import_task.py @@ -14,17 +14,19 @@ class ImportTask(QtWidgets.QDialog): super().__init__(parent, *args, **kwargs) self.setWindowTitle("New Tasks") + self.config = config + self.line_edit = QtWidgets.QLineEdit(self) - completer = TaskCompleter(config) - self.line_edit.setCompleter(completer) - self.line_edit.textChanged.connect(completer.update_picker) + self.completer = TaskCompleter(config) + self.line_edit.setCompleter(self.completer) + self.line_edit.textChanged.connect(self.completer.update_picker) self.line_edit.setFocus() self.indicator = ProgressIndicator(self) self.indicator.setAnimationDelay(70) self.indicator.setDisplayedWhenStopped(False) - completer.running.connect(self.spin) - completer.stopped.connect(self.no_spin) + self.completer.running.connect(self.spin) + self.completer.stopped.connect(self.no_spin) ok_button = QtWidgets.QPushButton() ok_button.setText("OK") @@ -77,6 +79,8 @@ class ImportTask(QtWidgets.QDialog): return self.line_edit.text() def showEvent(self, _): + # pick up config changes + self.completer.update_urls() self.line_edit.setText("") self.raise_() self.activateWindow() diff --git a/src/fime/main.py b/src/fime/main.py index 7492163..f492fb1 100755 --- a/src/fime/main.py +++ b/src/fime/main.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import os import signal import sys from functools import partial @@ -22,6 +21,7 @@ from fime.data import Tasks, Log, Data, LogCommentsData, Worklog, Report from fime.exceptions import FimeException from fime.import_task import ImportTask from fime.report import ReportDialog +from fime.settings import Settings from fime.task_edit import TaskEdit from fime.util import get_screen_height, get_icon @@ -37,17 +37,11 @@ class App: self.log = Log(lcd) self._active_task = self.log.last_log() or "Nothing" - config = Config() - if config.tray_theme == "light": - icon = get_icon("appointment-new-light") - else: - icon = get_icon("appointment-new") - - self.menu_flipped = config.flip_menu + self.config = Config() self.menu = QtWidgets.QMenu(None) - self.import_task = ImportTask(config, None) + self.import_task = ImportTask(self.config, None) self.import_task.accepted.connect(self.new_task_imported) self.taskEdit = TaskEdit(self.tasks, None) @@ -56,20 +50,31 @@ class App: self.reportDialog = ReportDialog(self.tasks, Report(lcd), None) self.reportDialog.accepted.connect(self.log_edited) - self.worklogDialog = WorklogDialog(config, Worklog(lcd), None) + self.worklogDialog = WorklogDialog(self.config, Worklog(lcd), None) self.worklogDialog.accepted.connect(self.log_edited) + self.settings = Settings(self.config, None) + self.settings.accepted.connect(self.update_icon) + self.settings.accepted.connect(self.update_tray_menu) + self.about = About(None) self.tray = QtWidgets.QSystemTrayIcon() - self.tray.setIcon(icon) self.tray.setContextMenu(self.menu) + self.update_icon() self.tray.show() self.tray.setToolTip("fime") self.update_tray_menu() self.last_dialog: Optional[QtWidgets.QDialog] = None + def update_icon(self): + if self.config.tray_theme == "light": + icon = get_icon("appointment-new-light") + else: + icon = get_icon("appointment-new") + self.tray.setIcon(icon) + @QtCore.Slot() def new_task_imported(self): if self.import_task.task_text: @@ -123,7 +128,7 @@ class App: add_tasks(self.tasks.tasks) menu_items.append((1, None)) - already_taken = (len(self.tasks.tasks) + 5) * action_height + already_taken = (len(self.tasks.tasks) + 6) * action_height available_space = get_screen_height(self.menu) * 0.8 - already_taken jira_entry_count = int(available_space // action_height) add_tasks(self.tasks.jira_tasks[-jira_entry_count:]) @@ -141,10 +146,11 @@ class App: menu_items.append((1, None)) + menu_items.append(("Settings", partial(self.open_new_dialog, self.settings))) menu_items.append(("About", partial(self.open_new_dialog, self.about))) menu_items.append(("Close", self.app.quit)) - if self.menu_flipped: + if self.config.flip_menu: menu_items.reverse() seps = 0 @@ -177,7 +183,7 @@ class App: def init_logging(): log_dir_path = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation)) / "logs" - logger.add(log_dir_path / "fime_{time:YYYY-MM-DD}.log", rotation="1d", retention="30d", compression="zip", level="INFO") + logger.add(log_dir_path / "fime_{time:YYYY-MM-DD}.log", rotation="1d", retention="30d", compression="zip", level="DEBUG") def excepthook(e_type, e_value, tb_obj): diff --git a/src/fime/settings.py b/src/fime/settings.py new file mode 100644 index 0000000..35ecca7 --- /dev/null +++ b/src/fime/settings.py @@ -0,0 +1,93 @@ +from fime.util import get_icon + +try: + from PySide6 import QtCore, QtGui, QtWidgets +except ImportError: + from PySide2 import QtCore, QtGui, QtWidgets + +from fime.config import Config + + +class Settings(QtWidgets.QDialog): + def __init__(self, config: Config, parent, *args, **kwargs): + super().__init__(parent, *args, **kwargs) + self.setWindowTitle("Settings") + + self.config = config + + caption_label = QtWidgets.QLabel() + caption_label.setText("Settings") + caption_label.setAlignment(QtCore.Qt.AlignHCenter) + + settings_layout = QtWidgets.QGridLayout() + + jira_url_label = QtWidgets.QLabel() + jira_url_label.setText("Jira URL") + settings_layout.addWidget(jira_url_label, 0, 0) + self.jira_url_edit = QtWidgets.QLineEdit() + settings_layout.addWidget(self.jira_url_edit, 0, 1) + + jira_token_label = QtWidgets.QLabel() + jira_token_label.setText("Jira Personal Access Token
see here for how to get one") + jira_token_label.setTextFormat(QtCore.Qt.RichText) + jira_token_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + jira_token_label.setOpenExternalLinks(True) + settings_layout.addWidget(jira_token_label, 1, 0) + self.jira_token_edit = QtWidgets.QLineEdit() + settings_layout.addWidget(self.jira_token_edit, 1, 1) + + tray_theme_label = QtWidgets.QLabel() + tray_theme_label.setText("Tray theme") + settings_layout.addWidget(tray_theme_label, 2, 0) + self.tray_theme_combo_box = QtWidgets.QComboBox() + self.tray_theme_combo_box.addItem("Light") + self.tray_theme_combo_box.addItem("Dark") + settings_layout.addWidget(self.tray_theme_combo_box, 2, 1, QtCore.Qt.AlignRight) + + flip_menu_label = QtWidgets.QLabel() + flip_menu_label.setText("Flip menu") + settings_layout.addWidget(flip_menu_label, 3, 0) + self.flip_menu_check_box = QtWidgets.QCheckBox() + settings_layout.addWidget(self.flip_menu_check_box, 3, 1, QtCore.Qt.AlignRight) + + self.ok_button = QtWidgets.QPushButton() + self.ok_button.setText("OK") + self.ok_button.setIcon(get_icon("dialog-ok")) + self.ok_button.pressed.connect(self.accept) + self.ok_button.setAutoDefault(True) + + button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch(66) + button_layout.addWidget(self.ok_button, 33) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(caption_label) + layout.addLayout(settings_layout) + layout.addLayout(button_layout) + + self.setLayout(layout) + self.resize(500, 0) + self.accepted.connect(self._accepted) + + def showEvent(self, _): + self.jira_url_edit.setText(self.config.jira_url) + self.jira_token_edit.setText(self.config.jira_token) + self.tray_theme_combo_box.setCurrentText(self.config.tray_theme.capitalize()) + self.flip_menu_check_box.setChecked(self.config.flip_menu) + + def _accepted(self): + self.config.jira_url = self.jira_url_edit.text() + self.config.jira_token = self.jira_token_edit.text() + self.config.tray_theme = self.tray_theme_combo_box.currentText() + self.config.flip_menu = self.flip_menu_check_box.isChecked() + self.config.save() + + +# only for dev/debug +if __name__ == "__main__": + QtCore.QCoreApplication.setApplicationName("fime") + app = QtWidgets.QApplication() + cfg = Config() + settings = Settings(cfg, None) + settings.show() + app.exec() diff --git a/src/fime/task_completer.py b/src/fime/task_completer.py index e0d2b8b..d539ddf 100644 --- a/src/fime/task_completer.py +++ b/src/fime/task_completer.py @@ -29,8 +29,9 @@ class TaskCompleter(QtWidgets.QCompleter): self.setCaseSensitivity(QtCore.Qt.CaseInsensitive) self.session = FuturesSession() self.config = config - self.picker_url = os.path.join(self.config.jira_url, "rest/api/2/issue/picker") - self.search_url = os.path.join(self.config.jira_url, "rest/api/2/search") + self.picker_url = None + self.search_url = None + self.update_urls() self.text = "" self.response_text = "" self.model_data = set() @@ -44,6 +45,10 @@ class TaskCompleter(QtWidgets.QCompleter): self.rif_counter_lock = threading.Lock() self.last_rif_state = TaskCompleter.RifState.STOPPED + def update_urls(self): + self.picker_url = os.path.join(self.config.jira_url, "rest/api/2/issue/picker") + self.search_url = os.path.join(self.config.jira_url, "rest/api/2/search") + @QtCore.Slot() def process_response(self): with self.rif_counter_lock: diff --git a/src/fime/worklog.py b/src/fime/worklog.py index 603bcb7..a08ba40 100644 --- a/src/fime/worklog.py +++ b/src/fime/worklog.py @@ -175,7 +175,7 @@ class WorklogDialog(QtWidgets.QDialog): self.update_timer.timeout.connect(self.update_statuses) def showEvent(self, _): - # reinitialize to purge caches + # reinitialize to purge caches and pick up config changes self.rest = WorklogRest(self.config) self._worklog.date = date.today() self.update_all()