python/jabbalaci/JiVE-Image-Viewer/jive/main.py

main.py
#!/usr/bin/env python3

"""
This is the main file.
"""

# check compatibility
try:
    import sys
    assert sys.version_info.major == 3
    assert sys.version_info.minor >= 6
except AssertionError:
    raise RuntimeError("JiVE requires Python 3.6+!")

##############################################################################

if __name__ == "__main__":
    import os
    import sys

    # This is a trick. This way I can launch jive.py (this file) during
    # the development and I don't need to start ../start.py every time.
    folder = os.path.join(os.path.dirname(__file__), "..")
    if folder not in sys.path:
        sys.path.insert(0, folder)
    sys.argv[0] = "../start.py"
# endif

##############################################################################

import os
import sys
from functools import partial
from pathlib import Path
from typing import List, Optional, Tuple, Union

from PyQt5 import QtGui, sip
from PyQt5.QtCore import QPoint, Qt
from PyQt5.QtGui import QCursor, QKeySequence
from PyQt5.QtMultimedia import QSound
from PyQt5.QtWidgets import (QAction, QApplication, QDesktopWidget,
                             QFileDialog, QFrame, QInputDialog, QLabel,
                             QLineEdit, QMainWindow, QMenu, QMessageBox,
                             QScrollArea, QShortcut, QVBoxLayout, qApp)

from jive import autodetect, bookmarks, cache, categories
from jive import config as cfg
from jive import duplicates, help_dialogs, helper, opener, settings
from jive import shortcuts as scuts
from jive import statusbar as sbar
from jive.commit import Commit
from jive.customurls import CustomUrls
from jive.extractors import (fuskator, imagefap, imgur, sequence, subreddit,
                             tumblr, tumblr_blog)
from jive.helper import blue, bold, gray, green, pretty_num, red
from jive.imageinfo import ImageInfo
from jive.imagelist import ImageList
from jive.imageproperty import ImageProperty
from jive.imageview import ImageView
from jive.imagewithextra import ImageWithExtraInfo
from jive.important import ImportantFilesAndFolders
from jive.simplescrape import SimpleScrape
from jive.urlfolding import UrlFolding

log = cfg.log

ON, OFF = True, False

# Leave it here! This way we force pyinstaller to include PyQt5.sip.so
# in the EXE. Without that file the EXE fails to start and for some reason
# it was not added automatically.
sip_version: int = sip.SIP_VERSION

# TEST_IMG = "pinup.jpg"
# TEST_IMG = "girl.jpg"
# TEST_IMG = "burned_man.jpg"

# TEST_DIR = "/trash/images"
# TEST_DIR = "samples/"
# TEST_DIR = "/trash/image_viewer"

# TEST_SUBREDDIT = "EarthPorn"
# TEST_SUBREDDIT = "hardbodies"

# TEST_IMGUR_ALBUM = "https://imgur.com/gallery/9p0gCyv"    # pirates
# TEST_REMOTE_URL_FILE = "https://i.imgur.com/k489QN8.jpg"    # femme pirate
# TEST_TUMBLR_POST = "https://different-landscapes.tumblr.com/post/174158537319"    # tree


class MainWindow(QMainWindow):
    def __init__(self, argv) -> None:
        super().__init__()
        self.argv = argv

        # log.debug(f"argv: {argv}")

        self.title = "JiVE"
        self.top = 50
        self.left = 50
        self.width = 900    # type: ignore
        self.height = 600    # type: ignore

        self.auto_fit = False
        self.auto_width = False
        self.mouse_pointer = ON
        self.show_image_path = True

        self.settings = settings.Settings()

        self.setMouseTracking(True)

        self.setAcceptDrops(True)    # enable drag & drop

        self._fit_window_to_image_status = OFF
        self._fit_window_to_image_width = None     # will be set later
        self._fit_window_to_image_height = None    # will be set later

        self.imgList = ImageList(self)

        self.shortcuts = scuts.Shortcuts()
        self.add_shortcuts()

        self.image_info_dialog = None                     # will be set later
        self.important_files_and_folders_dialog = None    # will be set later

        # it must be here, before calling init_ui()
        self.show_subreddits = \
            True if cfg.PREFERENCES_OPTIONS.get("enable_subreddits", "") == "yes" else False

        # it must be here, before calling init_ui()
        self.show_bookmarks = \
            True if cfg.PREFERENCES_OPTIONS.get("enable_bookmarks", "") == "yes" else False

        self.init_ui()

        self.commit = Commit(self)    # it must come after the init_ui()

        self.cache = cache.Cache(cfg.PREFERENCES_OPTIONS, cfg.PLATFORM_SETTINGS["cache_dir"])

        self.preload = True if cfg.PREFERENCES_OPTIONS.get("preload", "") == "yes" else False

        self.use_audio = True if cfg.PREFERENCES_OPTIONS.get("use_audio", "") == "yes" else False
        if self.use_audio:
            self.error_sound = QSound(cfg.ERROR_SOUND, self)

        self.auto_load_next_subreddit_page = \
            True if cfg.PREFERENCES_OPTIONS.get("auto_load_next_subreddit_page", "") == "yes" else False

        self.toggle_auto_fit()           # set it ON and show the flash message
        self.toggle_show_image_path()    # make it False and hide it

        self.reset()    # it must be last thing here

        if len(self.argv) > 1:
            self.process_arguments(self.argv)

        # These are here after reset() just for testing.
        # TO BE REMOVED in the release version.
        # self.open_local_dir(TEST_DIR)
        # self.open_subreddit(TEST_SUBREDDIT)
        # self.open_imgur_album(TEST_IMGUR_ALBUM)
        # self.open_remote_url_file(TEST_REMOTE_URL_FILE)
        # self.open_tumblr_post(TEST_TUMBLR_POST)

    def reset(self, msg: Optional[str] = None) -> None:
        self.setWindowTitle(self.title)

        self.imgList.reset()

        if self.imgList.get_curr_img() is None:
            self.show_logo()

        self.info_line.setText("")
        self.statusbar.reset()
        if msg:
            self.statusbar.flash_message(msg)

        # if categories.yaml changed
        self.create_contextmenu()

        # if bookmarks.yaml changed
        self.create_menubar()

        # remove on-screen flags (S, D, W):
        self.flags_line.setText("")

    def process_arguments(self, argv) -> None:
        param = argv[1]
        self.auto_detect(param)

    def auto_detect(self, text: str) -> None:
        # log.debug(f"param: {text}")

        # try to open it as a local file / dir.
        res = self.open_local_file_or_dir(text)
        if res:
            return
        # else, try to open it as a remote URL / subreddit / etc.
        self.auto_detect_and_open(text, called_from_gui=False)

    # def mouseMoveEvent(self, event):    # doesn't work :( I wanted to monitor the cursor position
    #     p = event.pos()
    #     x, y = p.x(), p.y()
    #     print(f"x: {x}, y: {y}")

    def mousePressEvent(self, event) -> None:
        """
        If you left click on the left 25% (by width), go to the previous image.
        If you left click on the right 25% (by width), go to the next image.

        Only left click is accepted for this kind of browsing.
        """
        if event.button() != Qt.LeftButton:
            return
        # else
        p = event.pos()
        x, y = p.x(), p.y()
        # print(x, y)
        width = self.img_view.width()
        if x  <  width * (1 / 4):
            self.imgList.jump_to_prev_image()
            # print("prev")
        if x > width * (3 / 4):
            self.imgList.jump_to_next_image()
            # print("next")

    def wheelEvent(self, event) -> None:
        p = event.angleDelta()
        x, y = p.x(), p.y()
        offset = 75
        if y  <  0:
            self.scroll_down(offset)
        else:
            self.scroll_up(offset)

    def set_title(self, prefix: str = "") -> None:
        if prefix:
            self.setWindowTitle(f"{prefix} - {self.title}")

    def open_local_dir(self, local_folder: str, redraw: bool = False) -> None:
        self.imgList.set_list_of_images(self.read_local_dir(local_folder))
        if len(self.imgList.get_list_of_images()) == 0:
            log.warning("no images were found")
            return
        # else
        self.imgList.set_curr_img_idx(-1)
        self.imgList.jump_to_image(0)  # this way the 2nd image will be preloaded
        # self.imgList.curr_img_idx = 0
        # self.imgList.curr_img = self.imgList.list_of_images[0].read()
        #
        if redraw:
            self.redraw()

    def open_local_file(self, local_file: str, redraw: bool = False) -> None:
        self.imgList.set_list_of_images(self.read_local_dir(str(Path(local_file).parent)))
        if len(self.imgList.get_list_of_images()) == 0:
            log.warning("no images were found")
            return
        # else
        jump_here = 0
        for i in range(len(self.imgList.get_list_of_images())):
            if self.imgList.get_list_of_images()[i].name == local_file:
                jump_here = i
                break
        self.imgList.set_curr_img_idx(-1)
        self.imgList.jump_to_image(jump_here)
        # self.imgList.curr_img = self.imgList.list_of_images[self.imgList.curr_img_idx].read()
        #
        if redraw:
            self.redraw()

    def open_local_file_or_dir(self, name: str) -> bool:
        """
        Returns True if it was a local file or a local directory.
        Otherwise, it returns False.
        """
        p = Path(name)
        # log.debug(p.absolute())
        if p.is_file():
            self.open_local_file(str(p), redraw=True)
            return True
        if p.is_dir():
            self.open_local_dir(str(p), redraw=True)
            return True
        #
        return False

    def open_remote_url_file(self, url: str) -> None:
        if Path(url).suffix.lower() not in cfg.SUPPORTED_FORMATS:
            log.warning("unsupported file format")
            return
        # else
        self.imgList.set_list_of_images([ImageProperty(url, self)])
        self.imgList.set_curr_img_idx(0)
        self.imgList.set_curr_img(self.imgList.get_list_of_images()[0].read())

    def open_subreddit(self, text: str, after_id: Optional[str] = None) -> None:
        subreddit_name = subreddit.get_subreddit_name(text)
        if not subreddit_name:
            log.warning("that's not a subreddit")
            return
        # else
        urls: List[ImageWithExtraInfo] = subreddit.read_subreddit(subreddit_name, after_id,
                                                                  statusbar=self.statusbar, mainWindow=self)
        self.open_urls(urls)

    def open_tumblr_blog(self, blog_name: str) -> None:
        urls: List[ImageWithExtraInfo] = tumblr_blog.get_photo_urls(blog_name, offset=0,
                                                                    statusbar=self.statusbar, mainWindow=self)
        self.open_urls(urls)

    def open_urls(self, urls: Union[List[str], List[ImageWithExtraInfo]]) -> None:
        if len(urls) == 0:
            log.warning("no images could be extracted")
            self.statusbar.flash_message(red("no images found"))
            return
        # else
        self.imgList.set_list_of_images([ImageProperty(url, self) for url in urls])
        self.imgList.set_curr_img_idx(-1)   # refresh the first image if we are there
        self.imgList.jump_to_image(0)    # this way the 2nd image will be preloaded

    def open_sequence_urls(self, seq_url: str) -> None:
        urls = sequence.get_urls_from_sequence_url(seq_url)
        self.open_urls(urls)

    def open_imagefap_photo(self, url: str) -> None:
        urls = imagefap.get_urls(url)
        self.open_urls(urls)

    def open_fuskator_gallery(self, url: str) -> None:
        embedded_url = fuskator.extract_embedded_url(url)
        if embedded_url:
            log.info(f"embedded URL: {embedded_url}")
            self.auto_detect_and_open(embedded_url, called_from_gui=False)
        else:
            log.warning("no embedded URL was found")

    def open_imgur_album(self, text: str) -> None:
        urls = []
        if imgur.is_album(text):
            images = imgur.extract_images_from_an_album(text)
            for img_url in images:
                if Path(img_url).suffix.lower() in cfg.SUPPORTED_FORMATS:
                    urls.append(img_url)
                #
            #
        else:
            log.warning("that's not an Imgur album")
        self.imgList.set_list_of_images([ImageProperty(url, self) for url in urls])
        if len(self.imgList.get_list_of_images()) > 0:
            self.imgList.set_curr_img_idx(0)
            self.imgList.set_curr_img(self.imgList.get_list_of_images()[0].read())

    def open_tumblr_post(self, text: str) -> None:
        url = text
        if not tumblr.is_post(url):
            log.warning("that's not a tumblr post")
            return

        urls = tumblr.extract_images_from_a_specific_post(url)
        self.open_urls(urls)

    def play_error_sound(self) -> None:
        if self.use_audio:
            self.error_sound.play()

    def read_local_dir(self, dir_path: str) -> List[ImageProperty]:
        lst = helper.read_image_files(dir_path)
        return [ImageProperty(img, self) for img in lst]    # without .read()

    def init_ui(self) -> None:
        # Option 1:
        self.setGeometry(self.top, self.left, self.width, self.height)    # type: ignore
        # Option 2:
        # self.center()
        # self.resize(self.width, self.height)

        self.setWindowTitle(self.title)
        self.setWindowIcon(QtGui.QIcon(cfg.ICON))

        self.img_view = ImageView(self)
        # self.central_widget.setStyleSheet("background-color: black")
        # black background

        # self.central_widget.setBackgroundBrush(Qt.black)
        # self.central_widget.setStyleSheet("background-color: black")
        self.setCentralWidget(self.img_view)

        self.scroll = QScrollArea()    # type: ignore
        # no border in fullscreen mode:
        self.scroll.setFrameShape(QFrame.NoFrame)    # type: ignore
        self.image_label = QLabel()
        self.scroll.setAlignment(Qt.AlignCenter)    # type: ignore
        self.scroll.setWidget(self.image_label)    # type: ignore
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.scroll)    # type: ignore

        self.img_view.setLayout(layout)

        self.info_line = QLabel(self.img_view)
        self.info_line.setMinimumWidth(cfg.LONG)
        self.info_line.move(20, 10)
        self.info_line.show()

        self.path_line = QLabel(self.img_view)
        self.path_line.setMinimumWidth(cfg.LONG)
        self.path_line.move(20, 30)
        self.path_line.show()

        self.loading_line = QLabel(self.img_view)
        self.loading_line.setText(green("Loading...", bold=True))
        default_font_name = QtGui.QFont().defaultFamily()
        new_font = QtGui.QFont(default_font_name, 15)
        self.loading_line.setFont(new_font)
        self.loading_line.setMinimumWidth(cfg.LONG)
        self.loading_line.setMinimumHeight(cfg.LONG)
        self.loading_line.setAlignment(Qt.AlignTop)
        self.loading_line.hide()

        self.flags_line = QLabel(self.img_view)
        default_font_name = QtGui.QFont().defaultFamily()
        new_font = QtGui.QFont(default_font_name, 30)
        self.flags_line.setFont(new_font)
        self.flags_line.setMinimumWidth(cfg.LONG)
        self.flags_line.setMinimumHeight(cfg.LONG)
        self.flags_line.setAlignment(Qt.AlignTop)
        self.flags_line.move(20, 60)
        self.flags_line.show()

        self.statusbar = sbar.StatusBar(self)
        self.statusBar().setStyleSheet(cfg.TOP_AND_BOTTOM_BAR_STYLESHEET)
        self.statusBar().addWidget(self.statusbar, 1)

        # self.set_current_image(TEST_IMG)

        self.show_image()
        self.make_scrollbars_disappear()
        # self.fit_window_to_image()

        self.create_menu_actions()
        self.create_contextmenu()
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_contextmenu)

        self.create_menubar()

    def show_scrollbars(self) -> None:
        self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)    # type: ignore
        self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)    # type: ignore

    def hide_scrollbars(self) -> None:
        self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)    # type: ignore
        self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)    # type: ignore

    def make_scrollbars_disappear(self) -> None:
        self.resize(self.geometry().width(),
                    self.geometry().height())

    def center(self) -> None:
        # geometry of the main window
        qr = self.frameGeometry()

        # center point of screen
        cp = QDesktopWidget().availableGeometry().center()

        # move rectangle's center point to screen's center point
        qr.moveCenter(cp)

        # top left of rectangle becomes top left of window centering it
        self.move(qr.topLeft())

    def menu_open_subreddit(self) -> None:
        text, okPressed = QInputDialog.getText(self,
                                               "Open subreddit",
                                               "Subreddit's name or its URL:" + " " * 50,
                                               QLineEdit.Normal,
                                               "")
        text = text.strip()
        if okPressed and text:
            what = autodetect.detect(text)
            kind = what[0] if what else None
            if kind in (autodetect.AutoDetectEnum.subreddit_url,
                        autodetect.AutoDetectEnum.subreddit_name,
                        autodetect.AutoDetectEnum.subreddit_r_name):
                self.open_subreddit(text)
            else:
                log.warning("that's not a subreddit")
                self.statusbar.flash_message(red("not a subreddit"))
                self.play_error_sound()

    def menu_open_imgur_album(self) -> None:
        text, okPressed = QInputDialog.getText(self,
                                               "Open Imgur album",
                                               "Complete URL:" + " " * 80,
                                               QLineEdit.Normal,
                                               "")
        text = text.strip()
        if okPressed and text:
            what = autodetect.detect(text)
            kind = what[0] if what else None
            if kind:
                if kind == autodetect.AutoDetectEnum.imgur_album:
                    self.open_imgur_album(text)
                    self.redraw()
                if kind == autodetect.AutoDetectEnum.imgur_html_page_with_embedded_image:
                    img = what[1]    # type: ignore
                    log.info("it seems to be an imgur HTML page with an embedded image")
                    self.open_remote_url_file(img)    # type: ignore
                    self.redraw()
            else:
                log.warning("that's not an imgur album / gallery / HTML")
                self.statusbar.flash_message(red("not an imgur link"))
                self.play_error_sound()

    def menu_open_url_auto_detect(self) -> None:
        text, okPressed = QInputDialog.getText(self,
                                               "Auto detect URL",
                                               "URL / subreddit / etc.:" + " " * 80,
                                               QLineEdit.Normal,
                                               self.settings.get_last_open_url_auto_detect())
        text = text.strip()
        if okPressed and text:
            self.auto_detect_and_open(text)

    def auto_detect_and_open(self, text: str, called_from_gui: bool = True) -> None:
        if called_from_gui:
            self.settings.set_last_open_url_auto_detect(text)

        what = autodetect.detect(text)
        if what is None:
            log.warning("hmm, it seems to be something new...")
            return

        # else, if it was detected
        kind = what[0]    # since "type" is a keyword
        if kind == autodetect.AutoDetectEnum.image_url:
            log.info("it seems to be a remote image")
            self.open_remote_url_file(text)
            self.redraw()
            return
        if kind in (autodetect.AutoDetectEnum.subreddit_url,
                    autodetect.AutoDetectEnum.subreddit_name,
                    autodetect.AutoDetectEnum.subreddit_r_name):
            log.info("it seems to be a subreddit")
            self.open_subreddit(text)
            return
        if kind == autodetect.AutoDetectEnum.tumblr_blog:
            log.info("it seems to be a tumblr blog")
            blog_name = what[1]
            self.open_tumblr_blog(blog_name)    # type: ignore
            return
        if kind == autodetect.AutoDetectEnum.imgur_album:
            log.info("it seems to be an Imgur album")
            self.open_imgur_album(text)
            self.redraw()
            return
        if kind == autodetect.AutoDetectEnum.tumblr_post:
            log.info("it seems to be a Tumblr post")
            self.open_tumblr_post(text)
            self.redraw()
            return
        if kind == autodetect.AutoDetectEnum.sequence_url:
            log.info("it seems to be a sequence URL")
            self.open_sequence_urls(text)
            return
        if kind == autodetect.AutoDetectEnum.imgur_html_page_with_embedded_image:
            img = what[1]
            log.info("it seems to be an imgur HTML page with an embedded image")
            self.open_remote_url_file(img)    # type: ignore
            self.redraw()
            return
        if kind == autodetect.AutoDetectEnum.imagefap_photo:
            log.info("it seems to be an imagefap photo series")
            self.open_imagefap_photo(text)
            return
        if kind == autodetect.AutoDetectEnum.fuskator_gallery:
            log.info("it seems to be a fuskator gallery")
            self.open_fuskator_gallery(text)
            return

        log.info("it was detected but it was not handled...")

    def dragEnterEvent(self, event) -> None:
        if event.mimeData().hasFormat('text/plain'):
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event) -> None:
        text = event.mimeData().text().strip()
        if text.startswith("file://"):
            text = text[len("file://"):]
        self.open_local_file_or_dir(text)
        log.debug("dropEvent: {}".format(event.mimeData().text()))

    #########################
    ## BEGIN: top menu bar ##
    #########################
    def create_menu_actions(self) -> None:
        key = "Ctrl+O"
        self.open_file_act = QAction("Open &file", self)
        self.shortcuts.register_menubar_action(key, self.open_file_act, self.open_file)
        #
        key = "Ctrl+D"
        self.open_dir_act = QAction("Open &directory", self)
        self.shortcuts.register_menubar_action(key, self.open_dir_act, self.open_dir)
        #
        key = "Ctrl+U"
        self.open_url_auto_detect_act = QAction("Auto detect &URL", self)
        self.shortcuts.register_menubar_action(key, self.open_url_auto_detect_act, self.menu_open_url_auto_detect)
        #
        self.open_url_open_subreddit_act = QAction("Open sub&reddit", self)
        self.open_url_open_subreddit_act.triggered.connect(self.menu_open_subreddit)
        self.open_url_open_imgur_album_act = QAction("Open &Imgur album / gallery / HTML", self)
        self.open_url_open_imgur_album_act.triggered.connect(self.menu_open_imgur_album)
        #
        key = "Ctrl+S"
        self.save_image_act = QAction("&Save current image as...", self)
        self.shortcuts.register_menubar_action(key, self.save_image_act, self.save_image)
        #
        key = "Ctrl+R"
        self.open_random_subreddit_act = QAction("Open random subreddit", self)
        self.shortcuts.register_menubar_action(key, self.open_random_subreddit_act, self.open_random_subreddit)
        #
        self.save_image_list_act = QAction("Save image list as...", self)
        self.save_image_list_act.triggered.connect(self.save_image_list)
        #
        self.export_image_list_to_clipboard_act = QAction("E&xport image list to clipboard", self)
        self.export_image_list_to_clipboard_act.triggered.connect(self.export_image_list_to_clipboard)
        #
        key = "F5"
        self.reload_current_image_act = QAction("&Reload current image", self)
        self.shortcuts.register_menubar_action(key, self.reload_current_image_act, self.reload_current_image)
        #
        key = "I"
        self.image_info_act = QAction("Image &info", self)
        self.shortcuts.register_menubar_action(key, self.image_info_act, self.image_info)
        # self.image_info_act = QAction("Image info", self)
        # self.image_info_act.triggered.connect(self.image_info)
        #
        self.slideshow_act = QAction("Slideshow", self)
        self.slideshow_act.triggered.connect(self.slideshow)
        #
        self.important_files_and_folders_act = QAction("Important &files and folders", self)
        self.important_files_and_folders_act.triggered.connect(self.important_files_and_folders)
        #
        self.help_act = QAction("&Help", self)
        self.help_act.triggered.connect(help_dialogs.open_help)
        #
        self.about_act = QAction("&About", self)
        self.about_act.triggered.connect(partial(help_dialogs.open_about, self))
        #
        self.about_qt_act = QAction("About &Qt", self)
        self.about_qt_act.triggered.connect(qApp.aboutQt)
        #
        key = "Ctrl+Alt+R"
        self.reset_act = QAction("Reset", self)
        self.shortcuts.register_menubar_action(key, self.reset_act, partial(self.reset, "reset"))
        #
        key = "Q"
        self.quit_act = QAction("&Quit", self)
        self.shortcuts.register_menubar_action(key, self.quit_act, self.close)
        #
        key = "I"
        self.image_info_act = QAction("Image &info", self)
        self.shortcuts.register_menubar_action(key, self.image_info_act, self.image_info)
        #
        key = "Alt+M"
        self.hide_menubar_act = QAction("&Hide menu bar", self)
        self.shortcuts.register_menubar_action(key, self.hide_menubar_act, self.toggle_menubar)
        #
        key = "Ctrl+M"
        self.show_mouse_pointer_act = QAction("Show &mouse pointer", self, checkable=True, checked=True)    # type: ignore
        self.shortcuts.register_menubar_action(key, self.show_mouse_pointer_act, self.toggle_mouse_pointer)
        #
        self.shuffle_images_act = QAction("&Shuffle images", self)
        self.shuffle_images_act.triggered.connect(self.imgList.shuffle_images)
        #
        self.open_with_gimp_act = QAction("&Gimp", self)
        self.open_with_gimp_act.triggered.connect(self.open_with_gimp)
        #
        self.open_with_browser_act = QAction("&Browser", self)
        self.open_with_browser_act.triggered.connect(self.open_with_browser)
        #
        self.find_duplicates_act = QAction("Find &duplicates", self)
        self.find_duplicates_act.triggered.connect(self.find_duplicates)
        #
        self.sequence_urls_act = QAction("Open se&quence URL", self)
        self.sequence_urls_act.triggered.connect(self.sequence_urls)
        #
        self.image_url_act = QAction("Open image URL", self)
        self.image_url_act.triggered.connect(self.image_url)
        #
        self.open_url_open_tumblr_post_act = QAction("Open &Tumblr post", self)
        self.open_url_open_tumblr_post_act.triggered.connect(self.menu_open_tumblr_post)
        #
        key = "Ctrl+Shift+U"
        self.extract_images_from_webpage_act = QAction("E&xtract images from a webpage", self)
        self.shortcuts.register_menubar_action(key, self.extract_images_from_webpage_act, self.extract_images_from_webpage)
        #
        self.open_custom_url_list_act = QAction("Open &list of image URLs", self)
        self.open_custom_url_list_act.triggered.connect(self.open_custom_url_list)
        #
        self.url_folding_act = QAction("URL &folding / unfolding", self)
        self.url_folding_act.triggered.connect(self.url_folding)
        #
        self.open_bookmarks_file_act = QAction("Edit bookmarks", self)
        self.open_bookmarks_file_act.triggered.connect(partial(opener.open_file_with_editor, self, cfg.bookmarks_file()))

    def create_menubar(self) -> None:
        self.menubar = self.menuBar()
        self.menubar.clear()
        self.shortcuts.disable_conflicting_window_shortcuts()
        # self.menubar.setStyleSheet(cfg.TOP_AND_BOTTOM_BAR_STYLESHEET)

        open_url_acts = [self.open_url_auto_detect_act,
                         cfg.SEPARATOR,
                         self.image_url_act,
                         self.open_url_open_subreddit_act,
                         self.open_url_open_imgur_album_act,
                         self.open_url_open_tumblr_post_act,
                         self.sequence_urls_act]

        fileMenu = self.menubar.addMenu("&File")
        viewMenu = self.menubar.addMenu("&View")
        toolsMenu = self.menubar.addMenu("&Tools")
        if self.show_bookmarks:
            bookmarksMenu = self.menubar.addMenu("&Bookmarks")
        helpMenu = self.menubar.addMenu("&Help")

        # fileMenu
        fileMenu.addAction(self.open_file_act)
        fileMenu.addAction(self.open_dir_act)
        open_url_menu = QMenu(self.menubar)
        open_url_menu.setTitle("Open &URL")
        fileMenu.addMenu(open_url_menu)
        for entry in open_url_acts:
            if isinstance(entry, str):
                open_url_menu.addSeparator()
            else:
                open_url_menu.addAction(entry)    # type: ignore
        fileMenu.addAction(self.open_custom_url_list_act)
        fileMenu.addAction(self.open_random_subreddit_act)
        fileMenu.addSeparator()
        fileMenu.addAction(self.save_image_act)
        fileMenu.addAction(self.save_image_list_act)
        fileMenu.addAction(self.export_image_list_to_clipboard_act)
        fileMenu.addAction(self.reload_current_image_act)
        fileMenu.addSeparator()
        fileMenu.addAction(self.reset_act)
        fileMenu.addAction(self.quit_act)

        # viewMenu
        viewMenu.addAction(self.image_info_act)
        viewMenu.addAction(self.important_files_and_folders_act)
        viewMenu.addSeparator()
        viewMenu.addAction(self.hide_menubar_act)
        viewMenu.addAction(self.show_mouse_pointer_act)

        # bookmarksMenu
        if self.show_bookmarks:
            my_bookmarks_menu = QMenu(self.menubar)
            my_bookmarks_menu.setTitle("My &bookmarks...")
            bookmarksMenu.addMenu(my_bookmarks_menu)
            bookmarks.Bookmarks(self, my_bookmarks_menu, helper.open_new_browser_tab).populate()
            bookmarksMenu.addSeparator()
            bookmarksMenu.addAction(self.open_bookmarks_file_act)
        # endif

        # toolsMenu
        toolsMenu.addAction(self.shuffle_images_act)
        toolsMenu.addAction(self.find_duplicates_act)
        toolsMenu.addAction(self.extract_images_from_webpage_act)
        toolsMenu.addAction(self.url_folding_act)

        # helpMenu
        helpMenu.addAction(self.help_act)
        helpMenu.addAction(self.about_act)
        helpMenu.addAction(self.about_qt_act)
    #
    #######################
    ## END: top menu bar ##
    #######################

    #################################
    ## BEGIN: popup (context) menu ##
    #################################
    def create_contextmenu(self) -> None:
        self.menu = QMenu(self)

        open_url_acts = [self.open_url_auto_detect_act,
                         cfg.SEPARATOR,
                         self.image_url_act,
                         self.open_url_open_subreddit_act,
                         self.open_url_open_imgur_album_act,
                         self.open_url_open_tumblr_post_act,
                         self.sequence_urls_act]

        open_with_acts = [self.open_with_gimp_act, self.open_with_browser_act]

        # When I right-click, very often the first menu item gets selected.
        # "Nothing" is added to avoid that problem.
        nothing = "--"
        self.menu.addAction(QAction(nothing, self))
        self.menu.addSeparator()
        self.menu.addAction(self.open_file_act)
        self.menu.addAction(self.open_dir_act)
        open_url_menu = QMenu(self.menu)
        open_url_menu.setTitle("Open &URL...")
        self.menu.addMenu(open_url_menu)
        self.menu.addAction(self.open_random_subreddit_act)
        for entry in open_url_acts:
            if isinstance(entry, str):
                open_url_menu.addSeparator()
            else:
                open_url_menu.addAction(entry)    # type: ignore
        # self.menu.addAction(self.open_url_act)
        if self.show_subreddits:
            open_subreddit_categories = QMenu(self.menu)
            open_subreddit_categories.setTitle("Select subreddit...")
            self.menu.addMenu(open_subreddit_categories)
            categories.Categories(self, open_subreddit_categories, self.open_subreddit).populate()
        # endif
        self.menu.addSeparator()
        open_with_menu = QMenu(self.menu)
        open_with_menu.setTitle("Open with...")
        self.menu.addMenu(open_with_menu)
        self.menu.addAction(self.save_image_act)
        for entry in open_with_acts:
            if isinstance(entry, str):
                open_with_menu.addSeparator()
            else:
                open_with_menu.addAction(entry)
        self.menu.addSeparator()
        self.menu.addAction(self.image_info_act)
        self.menu.addAction(self.slideshow_act)
        self.menu.addSeparator()
        self.menu.addAction(self.quit_act)
        # When I right-click at the bottom of the screen, very often the last menu item gets selected.
        # "Nothing" is added to avoid an accidental quit.
        self.menu.addSeparator()
        self.menu.addAction(QAction(nothing, self))

    def show_contextmenu(self, pos) -> None:
        self.menu.popup(self.mapToGlobal(pos))
    #
    ###############################
    ## END: popup (context) menu ##
    ###############################

    def open_dir(self) -> None:
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog    # type: ignore
        folder = QFileDialog.getExistingDirectory(self,
                                                  caption="Open Image Directory",
                                                  directory=self.settings.get_last_dir_opened(),
                                                  options=options)
        if os.path.isdir(folder):
            self.open_local_dir(folder)
            self.settings.set_last_dir_opened(folder)
            self.redraw()

    def open_file(self) -> None:
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog    # type: ignore
        filter = "Images (*.bmp *.jpg *.jpe *.jpeg *.png *.pbm *.pgm *.ppm *.xbm *.xpm)"
        file_obj = QFileDialog.getOpenFileName(self,
                                               caption="Open Image File",
                                               directory=str(Path(self.settings.get_last_file_opened()).parent),
                                               filter=filter,
                                               initialFilter=filter,
                                               options=options)
        fname = file_obj[0]
        if os.path.isfile(fname):
            self.open_local_file(fname)
            self.settings.set_last_file_opened(fname)
            self.redraw()

    def add_shortcuts(self) -> None:
        key = "Ctrl+O"
        self.shortcutOpenFile = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutOpenFile, self.open_file)

        key = "Ctrl+D"
        self.shortcutOpenDir = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutOpenDir, self.open_dir)

        key = "Q"
        self.shortcutQuit = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutQuit, self.close)
        #
        key = "Ctrl+Q"
        self.shortcutCtrlQuit = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutCtrlQuit, self.close)

        key = "Ctrl+U"
        self.shortcutAutoDetect = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutAutoDetect, self.menu_open_url_auto_detect)

        key = "Z"
        self.shortcutFitWindowToImage = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutFitWindowToImage, self.toggle_fit_window_to_image)

        key = "F11"
        self.shortcutFullscreen = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutFullscreen, self.toggle_fullscreen)

        key = "Esc"
        self.shortcutFromFullscreenToNormal = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutFromFullscreenToNormal, self.from_fullscreen_to_normal)

        key = "+"
        self.shortcutZoomIn = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutZoomIn, self.zoom_in)

        key = "-"
        self.shortcutZoomOut = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutZoomOut, self.zoom_out)

        key = "="
        self.shortcutZoomReset = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutZoomReset, self.zoom_reset)

        key = "M"
        self.shortcutMaximized = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutMaximized, self.toggle_maximized)

        key = "F"
        self.shortcutFitImageToWindow = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutFitImageToWindow, self.fit_image_to_window)

        key = "Left"
        self.shortcutPrevImage = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutPrevImage, self.imgList.jump_to_prev_image)
        #
        key ="PgUp"
        self.shortcutNextImagePgUp = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutNextImagePgUp, self.imgList.jump_five_percent_backward)

        #####################
        ## BEGIN scrolling ##
        #####################
        key = "Shift+Down"
        self.shortcutScrollDown = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollDown, self.scroll_down)
        key = "2"
        self.shortcutScrollDownNum = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollDownNum, self.scroll_down)
        key = "Down"
        self.shortcutScrollDownArrow = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollDownArrow, self.scroll_down)
        #
        key ="Shift+Up"
        self.shortcutScrollUp = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollUp, self.scroll_up)
        key = "8"
        self.shortcutScrollUpNum = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollUpNum, self.scroll_up)
        key = "Up"
        self.shortcutScrollUpArrow = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollUpArrow, self.scroll_up)
        #
        key = "Shift+Right"
        self.shortcutScrollRight = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollRight, self.scroll_right)
        key = "6"
        self.shortcutScrollRightNum = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollRightNum, self.scroll_right)
        #
        key = "Shift+Left"
        self.shortcutScrollLeft = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollLeft, self.scroll_left)
        key = "4"
        self.shortcutScrollLeftNum = QShortcut(QKeySequence("4"), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutScrollLeftNum, self.scroll_left)
        ###################
        ## END scrolling ##
        ###################

        key = "Right"
        self.shortcutNextImage = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutNextImage, self.imgList.jump_to_next_image)
        #
        key = "PgDown"
        self.shortcutNextImagePgDn = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutNextImagePgDn, self.imgList.jump_five_percent_forward)

        key = "Home"
        self.shortcutFirstImage = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutFirstImage, self.imgList.jump_to_first_image)

        key = "End"
        self.shortcutLastImage = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutLastImage, self.imgList.jump_to_last_image)

        key = "Ctrl+F"
        self.shortcutAutoFit = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutAutoFit, self.toggle_auto_fit)
        #
        key = "Ctrl+w"
        self.shortcutAutoWidth = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutAutoWidth, self.toggle_auto_width)

        key = "Ctrl+M"
        self.shortcutHideMouse = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutHideMouse, self.toggle_mouse_pointer)

        key = "R"
        self.shortcutJumpToRandomImg = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutJumpToRandomImg, self.imgList.jump_to_random_image)
        #
        key = "Shift+R"
        self.shortcutJumpToPrevRandomImg = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutJumpToPrevRandomImg, self.imgList.jump_to_prev_random_image)

        key = "P"
        self.shortcutToggleShowImagePath = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutToggleShowImagePath, self.toggle_show_image_path)

        key = "Shift+P"
        self.shortcutCopyPathToClipboard = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutCopyPathToClipboard, self.copy_path_to_clipboard)

        key = "G"
        self.shortcutGoToImage = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutGoToImage, self.dialog_go_to_image)

        key = "Ctrl+Alt+R"
        self.shortcutReset = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutReset, partial(self.reset, "reset"))

        key = "Alt+M"
        self.shortcutToggleMenubar = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutToggleMenubar, self.toggle_menubar)

        key = "Menu"
        self.shortcutContextMenu = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutContextMenu, self.show_popup)

        key = "I"
        self.shortcutImageInfo = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutImageInfo, self.image_info)

        key = "S"
        self.shortcutMarkToSave = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutMarkToSave, self.toggle_img_save)

        key = "D"
        self.shortcutMarkToDelete = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutMarkToDelete, self.toggle_img_delete)

        key = "W"
        self.shortcutMarkToWallpaper = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutMarkToWallpaper, self.toggle_img_wallpaper)

        key = "C"
        self.shortcutCommit = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutCommit, self.commit_changes)

        key = "Ctrl+A"
        self.shortcutMarkAllToSave = QShortcut(QKeySequence(key), self)
        self.shortcuts.register_window_shortcut(key, self.shortcutMarkAllToSave, self.mark_all_images_to_save)

    def mark_all_images_to_save(self) -> None:
        self.imgList.mark_all_images_to_save()
        self.statusbar.flash_message(blue("all marked for save"))
        self.redraw()

    def commit_changes(self) -> None:
        if not self.commit.has_something_to_commit():
            QMessageBox.information(self, "Info", "There's nothing to commit.")
            return
        # else, if there's something to commit
        to_del = self.commit.to_delete()
        remain = len(self.imgList.get_list_of_images()) - to_del
        msg = f"""
Do you want to commit your changes?

Save: {self.commit.to_save()}
Save as wallpaper: {self.commit.to_wallpaper()}
Delete: {to_del} (remain {remain})
""".strip()

        reply = QMessageBox.question(self,
                                     'Question',
                                     msg,
                                     QMessageBox.Yes | QMessageBox.No,
                                     QMessageBox.Yes)

        if reply == QMessageBox.No:
            return

        # else, if the user wants to commit the changes

        marked_to_wallpaper = self.commit.to_wallpaper()                # How many were marked?
        marked_to_wallpaper_success = self.commit.save_wallpapers()     # How many were saved successfully?
        w_ok = (marked_to_wallpaper == marked_to_wallpaper_success)     # OK, if the two values are identical

        marked_to_save = self.commit.to_save()
        marked_to_save_success = self.commit.save_others()
        s_ok = (marked_to_save == marked_to_save_success)

        marked_to_delete = self.commit.to_delete()
        marked_to_delete_success = self.commit.delete_files()
        d_ok = (marked_to_delete == marked_to_delete_success)

        ok = all([w_ok, s_ok, d_ok])            # OK, if everything was processed successfully
        popup = QMessageBox.information         # if OK, show an information popup
        if not ok:                              # otherwise show a warning popup
            popup = QMessageBox.warning

        self.redraw()    # hide flags on the screen if the flags were removed

        text = f"""
{marked_to_save_success} (of {marked_to_save}) images were saved
{marked_to_wallpaper_success} (of {marked_to_wallpaper}) images were saved as wallpapers
{marked_to_delete_success} (of {marked_to_delete}) images were deleted
""".strip()
        popup(self, "Commit summary", text)

    def toggle_img_save(self) -> None:
        if self.imgList.get_curr_img().image_state == ImageProperty.IMAGE_STATE_PROBLEM:    # type: ignore
            self.statusbar.flash_message(red("no"))
            return
        # else
#         if self.imgList.get_curr_img().local_file:    # type: ignore
#             msg = """
# This is a  < strong>local < /strong> file. < br>
#  < br>
# It makes no sense to mark it to be saved.
# """.strip()
#             QMessageBox.warning(self, "Warning", msg)
#             return
        # else
        self.imgList.get_curr_img().toggle_save()    # type: ignore
        if self.imgList.get_curr_img().to_save:    # type: ignore
            self.statusbar.flash_message("+ save", cfg.MESSAGE_FLASH_TIME_1)
        else:
            self.statusbar.flash_message("- save", cfg.MESSAGE_FLASH_TIME_1)
        self.redraw()
        if self.imgList.get_curr_img().to_save:    # type: ignore
            self.imgList.jump_to_next_image()

    def toggle_img_delete(self) -> None:
        if self.imgList.get_curr_img().image_state == ImageProperty.IMAGE_STATE_PROBLEM:    # type: ignore
            self.statusbar.flash_message(red("no"))
            return
        # else
        if not self.imgList.get_curr_img().local_file:    # type: ignore
            msg = """
This is a  < strong>remote < /strong> file with a URL. < br>
 < br>
You cannot delete it.
""".strip()
            QMessageBox.warning(self, "Warning", msg)
            return
        # else
        self.imgList.get_curr_img().toggle_delete()    # type: ignore
        if self.imgList.get_curr_img().to_delete:    # type: ignore
            self.statusbar.flash_message("+ delete", cfg.MESSAGE_FLASH_TIME_1)
        else:
            self.statusbar.flash_message("- delete", cfg.MESSAGE_FLASH_TIME_1)
        self.redraw()
        if self.imgList.get_curr_img().to_delete:    # type: ignore
            self.imgList.jump_to_next_image()

    def toggle_img_wallpaper(self) -> None:
        if self.imgList.get_curr_img().image_state == ImageProperty.IMAGE_STATE_PROBLEM:    # type: ignore
            self.statusbar.flash_message(red("no"))
            return
        # else
        self.imgList.get_curr_img().toggle_wallpaper()    # type: ignore
        if self.imgList.get_curr_img().to_wallpaper:    # type: ignore
            self.statusbar.flash_message("+ wallpaper", cfg.MESSAGE_FLASH_TIME_1)
        else:
            self.statusbar.flash_message("- wallpaper", cfg.MESSAGE_FLASH_TIME_1)
        self.redraw()
        if self.imgList.get_curr_img().to_wallpaper:    # type: ignore
            self.imgList.jump_to_next_image()

    def image_info(self) -> None:
        if not self.imgList.get_curr_img():
            self.statusbar.flash_message(red("no"))
            self.play_error_sound()
            return
        # else
        if self.imgList.get_curr_img().image_state == ImageProperty.IMAGE_STATE_PROBLEM:    # type: ignore
            self.statusbar.flash_message(red("no"))
            return
        # else
        if self.image_info_dialog:
            self.image_info_dialog.close()    # allow just 1 instance; not needed if that window is modal
        self.image_info_dialog = ImageInfo(self, self.imgList.get_curr_img())    # type: ignore

    def important_files_and_folders(self) -> None:
        if self.important_files_and_folders_dialog:
            self.important_files_and_folders_dialog.close()    # allow just 1 instance; not needed if that window is modal
        self.important_files_and_folders_dialog = ImportantFilesAndFolders(self)    # type: ignore

    def slideshow(self) -> None:
        self.not_yet_implemented()

    def not_yet_implemented(self) -> None:
        self.statusbar.flash_message(red("not yet implemented"))

    def reload_current_image(self) -> None:
        if not self.imgList.get_curr_img():
            self.statusbar.flash_message(red("no"))
            self.play_error_sound()
            return
        # else
        self.statusbar.flash_message(blue("reload"))
        self.imgList.get_curr_img().read(force=True)    # type: ignore
        self.redraw()

    def save_image(self) -> None:
        if not self.imgList.get_curr_img() or self.imgList.get_curr_img().image_state == ImageProperty.IMAGE_STATE_PROBLEM:    # type: ignore
            self.statusbar.flash_message(red("no"))
            self.play_error_sound()
            return
        # else
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog    # type: ignore
        filter = "Images (*.bmp *.jpg *.jpe *.jpeg *.png *.pbm *.pgm *.ppm *.xbm *.xpm)"
        offer_fname = str(Path(self.settings.get_last_dir_save_as(), self.imgList.get_curr_img().get_file_name_only()))    # type: ignore
        # print(offer_fname)
        file_obj = QFileDialog.getSaveFileName(self,
                                               caption="Save current image",
                                               directory=offer_fname,
                                               filter=filter,
                                               options=options)
        fname = file_obj[0]
        if fname:
            res = self.imgList.get_curr_img().save_as(fname)    # type: ignore
            if res:
                log.info(f"the file was saved as {fname}")
                self.statusbar.flash_message(blue("saved"))
                self.settings.set_last_dir_save_as(str(Path(fname).parent))
            else:
                log.info(f"the file was NOT saved")

    def export_image_list_to_clipboard(self) -> None:
        lst = self.imgList.get_image_list()
        if len(lst) == 0:
            self.statusbar.flash_message(red("no"))
            self.play_error_sound()
            return
        # else
        content = ("\n".join(lst)).strip() + "\n"
        helper.copy_text_to_clipboard(content)
        log.info("the image list was copied to the clipboard")
        self.statusbar.flash_message(blue("copied to clipboard"))

    def open_random_subreddit(self) -> None:
        subreddit = categories.Categories.get_random_subreddit()
        log.info(f"random subreddit: {subreddit}")
        self.statusbar.flash_message(f"/r/{subreddit}", wait=cfg.MESSAGE_FLASH_TIME_8)
        self.open_subreddit(subreddit)

    def save_image_list(self) -> None:
        lst = self.imgList.get_image_list()
        if len(lst) == 0:
            self.statusbar.flash_message(red("no"))
            self.play_error_sound()
            return
        # else
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog    # type: ignore
        offer_fname = "image_list.txt"
        file_obj = QFileDialog.getSaveFileName(self,
                                               caption="Save image list",
                                               directory=offer_fname,
                                               options=options)
        fname = file_obj[0]
        if fname:
            content = "\n".join(lst)
            with open(fname, "w") as f:
                print(content, file=f)
            if Path(fname).is_file():
                log.info(f"image list was saved to {fname}")
                self.statusbar.flash_message(blue("saved"))

    def open_with_gimp(self) -> None:
        if not self.imgList.get_curr_img():
            self.statusbar.flash_message(red("no image"))
            return
        # else
        name = self.imgList.get_curr_img().get_absolute_path_or_url()    # type: ignore
        opener.open_file_with_gimp(self, name)

    def open_with_browser(self) -> None:
        if not self.imgList.get_curr_img():
            self.statusbar.flash_message(red("no image"))
            return
        # else
        name = self.imgList.get_curr_img().get_absolute_path_or_url()    # type: ignore
        opener.open_file_with_browser(self, name)

    def find_duplicates(self) -> None:
        if len(self.imgList.get_list_of_images()) == 0:
            QMessageBox.information(self, "Info", "There are no duplicates.")
            return
        # else, there is at least 1 image
        if not self.imgList.get_curr_img().local_file:    # type: ignore
            QMessageBox.information(self, "Info", "Finding duplicates works with  < strong>local < /strong> files only!")
            return
        # else, we only have local file(s)
        # find and mark duplicates
        cnt = duplicates.mark_duplicates(self.imgList.get_list_of_images())
        if cnt == 0:
            msg = "There are no duplicates."
        else:
            msg = f"""
{cnt} images were  < strong>marked < /strong> to be deleted.

If you want to delete them from the
file system, then  < strong>commit < /strong> your changes.
""".strip().replace("\n", " < br>")
        QMessageBox.information(self, "Info", msg)

    def sequence_urls(self) -> None:
        text, okPressed = QInputDialog.getText(self,
                                               "Sequence URL",
                                               "Sequence URL:" + " " * 80,
                                               QLineEdit.Normal,
                                               "")
        text = text.strip()
        if okPressed and text:
            what = autodetect.detect(text)
            kind = what[0] if what else None
            if kind == autodetect.AutoDetectEnum.sequence_url:
                self.open_sequence_urls(text)
            else:
                log.warning("that's not a sequence URL")
                self.statusbar.flash_message(red("not a sequence"))
                self.play_error_sound()

    def image_url(self) -> None:
        text, okPressed = QInputDialog.getText(self,
                                               "Image URL",
                                               "Image URL:" + " " * 80,
                                               QLineEdit.Normal,
                                               "")
        text = text.strip()
        if okPressed and text:
            what = autodetect.detect(text)
            kind = what[0] if what else None
            if kind == autodetect.AutoDetectEnum.image_url:
                self.open_remote_url_file(text)
                self.redraw()
            else:
                log.warning("that's not an image URL")
                self.statusbar.flash_message(red("not an image URL"))
                self.play_error_sound()

    def extract_images_from_webpage(self) -> None:
        self.simple_scrape = SimpleScrape(log)
        self.simple_scrape.show()
        self.simple_scrape.setFixedSize(self.simple_scrape.size())  # disable resize
        self.simple_scrape.urlList.connect(self.open_urls)

    def open_custom_url_list(self) -> None:
        self.custom_url_list = CustomUrls(log)
        self.custom_url_list.show()
        self.custom_url_list.setFixedSize(self.custom_url_list.size())  # disable resize
        self.custom_url_list.urlList.connect(self.open_urls)

    def url_folding(self) -> None:
        self.url_folding_window = UrlFolding()
        self.url_folding_window.show()
        self.url_folding_window.setFixedSize(self.url_folding_window.size())    # disable resize
        self.url_folding_window.urlList.connect(self.open_urls)

    def menu_open_tumblr_post(self) -> None:
        text, okPressed = QInputDialog.getText(self,
                                               "Open Tumblr post",
                                               "Complete URL:" + " " * 80,
                                               QLineEdit.Normal,
                                               "")
        text = text.strip()
        if okPressed and text:
            what = autodetect.detect(text)
            kind = what[0] if what else None
            if kind == autodetect.AutoDetectEnum.tumblr_post:
                self.open_tumblr_post(text)
                self.redraw()
            else:
                log.warning("that's not a tumblr post")
                self.statusbar.flash_message(red("not a tumblr post"))
                self.play_error_sound()

    def show_popup(self) -> None:
        """
        When the "Menu" key is pressed, show the context menu right at the mouse pointer.
        """
        p = QCursor.pos()
        x, y = p.x(), p.y()
        qr = self.frameGeometry()
        top_left = qr.topLeft()
        x_offset, y_offset = top_left.x(), top_left.y()
        self.menu.popup(self.mapToGlobal(QPoint(x - x_offset, y - y_offset - self.menuBar().height())))

    def toggle_menubar(self) -> None:
        if self.menubar.isVisible():
            self.hide_menubar()
        else:
            self.show_menubar()
        #
        self.redraw()

    def show_menubar(self) -> None:
        self.menubar.show()
        self.shortcuts.disable_conflicting_window_shortcuts()

    def hide_menubar(self) -> None:
        self.menubar.hide()
        self.statusbar.flash_message("Alt+M: show menu bar")
        self.shortcuts.enable_all_window_shortcuts()

    def dialog_go_to_image(self) -> None:
        total = len(self.imgList.get_list_of_images())
        if total == 0:
            self.statusbar.flash_message(red("Where to? It's empty."))
            return
        # else
        text, okPressed = QInputDialog.getText(self, "Go To Image", f"Enter a value between 1 and {total}:", QLineEdit.Normal, "")
        if okPressed and text:
            try:
                value = int(text)
                if not (1  < = value  < = total):
                    raise ValueError
                # else
                idx = value - 1
                self.imgList.jump_to_image(idx)
            except ValueError:
                self.statusbar.flash_message(red("invalid value"))

    def scroll_to_top(self) -> None:
        self.scroll.verticalScrollBar().setValue(0)    # type: ignore

    def scroll_down(self, offset: int = 100) -> None:
        val = self.scroll.verticalScrollBar().value()    # type: ignore
        self.scroll.verticalScrollBar().setValue(val + offset)    # type: ignore

    def scroll_right(self) -> None:
        val = self.scroll.horizontalScrollBar().value()    # type: ignore
        self.scroll.horizontalScrollBar().setValue(val + 100)    # type: ignore

    def scroll_up(self, offset: int = 100) -> None:
        val = self.scroll.verticalScrollBar().value()    # type: ignore
        self.scroll.verticalScrollBar().setValue(val - offset)    # type: ignore

    def scroll_left(self) -> None:
        val = self.scroll.horizontalScrollBar().value()    # type: ignore
        self.scroll.horizontalScrollBar().setValue(val - 100)    # type: ignore

    def copy_path_to_clipboard(self) -> None:
        text = self.imgList.get_curr_img().get_absolute_path_or_url()    # type: ignore
        helper.copy_text_to_clipboard(text)
        msg = "{0} copied to clipboard".format("path" if self.imgList.get_curr_img().local_file else "URL")    # type: ignore
        self.statusbar.flash_message(msg, wait=cfg.MESSAGE_FLASH_TIME_3)

    def toggle_show_image_path(self) -> None:
        if self.show_image_path:
            self.show_image_path = False
            self.path_line.hide()
        else:
            self.show_image_path = True
            self.path_line.show()

    def toggle_mouse_pointer(self) -> None:
        if self.mouse_pointer == ON:
            self.setCursor(Qt.BlankCursor)  # hide cursor
            self.mouse_pointer = OFF
            self.statusbar.flash_message("Ctrl+M: show mouse pointer")
        else:
            self.unsetCursor()
            self.mouse_pointer = ON
            # self.statusbar.flash_message("show mouse cursor")

    # auto_fit and auto_width are not compatible
    # choose only one of them
    # if one of them is enabled, the other must be disabled

    def toggle_auto_fit(self) -> None:
        self.auto_fit = not self.auto_fit
        if self.auto_fit:
            self.auto_width = False    # disable the other
            self.statusbar.mode_label.setText(bold("AUTO FIT ON"))
            self.hide_scrollbars()
        else:
            self.statusbar.mode_label.setText(gray("AUTO FIT OFF"))
            self.show_scrollbars()
        self.redraw()

    def toggle_auto_width(self) -> None:
        self.auto_width = not self.auto_width
        if self.auto_width:
            self.auto_fit = False    # disable the other
            self.statusbar.mode_label.setText(bold("AUTO WIDTH {0}%".format(cfg.IMG_WIDTH_TO_WINDOW_WIDTH_IN_PERCENT)))
            self.hide_scrollbars()
        else:
            self.statusbar.mode_label.setText(gray("AUTO WIDTH OFF"))
            self.show_scrollbars()
        self.redraw()

    def zoom_in(self) -> None:
        if self.auto_fit or self.auto_width:
            self.statusbar.flash_message(red("oops, auto stuff is on"))
            return
        # else
        self.statusbar.flash_message("zoom in")
        self.imgList.get_curr_img().zoom_in()    # type: ignore
        self.redraw()

    def zoom_out(self) -> None:
        if self.auto_fit or self.auto_width:
            self.statusbar.flash_message(red("oops, auto stuff is on"))
            return
        # else
        self.statusbar.flash_message("zoom out")
        self.imgList.get_curr_img().zoom_out()    # type: ignore
        self.redraw()

    def zoom_reset(self) -> None:
        if self.auto_fit or self.auto_width:
            self.statusbar.flash_message(red("oops, auto stuff is on"))
            return
        # else
        self.statusbar.flash_message("reset zoom")
        self.imgList.get_curr_img().zoom_reset()    # type: ignore
        self.redraw()

    def toggle_fullscreen(self) -> None:
        if self.isFullScreen():
            self.showNormal()
            self.statusBar().show()
            self.show_menubar()
        else:
            self.showFullScreen()
            self.statusBar().hide()
            self.hide_menubar()

    def from_fullscreen_to_normal(self) -> None:
        if self.isFullScreen():
            self.toggle_fullscreen()

    def toggle_fit_window_to_image(self) -> None:
        if self._fit_window_to_image_status == OFF:
            self._fit_window_to_image_width = self.geometry().width()    # type: ignore
            self._fit_window_to_image_height = self.geometry().height()    # type: ignore
            #
            self.resize(self.imgList.get_curr_img().zoomed_img.width(),    # type: ignore
                        self.imgList.get_curr_img().zoomed_img.height())    # type: ignore
            self._fit_window_to_image_status = ON
        else:
            self.resize(self._fit_window_to_image_width,    # type: ignore
                        self._fit_window_to_image_height)
            self._fit_window_to_image_status = OFF

    def fit_image_to_window(self) -> None:
        if self.auto_width:
            self.statusbar.flash_message(red("oops, auto width is on"))
            return
        # else
        self.statusbar.flash_message("fit to window")
        self.imgList.get_curr_img().fit_img_to_window()    # type: ignore
        self.redraw()

    def fit_image_to_window_width(self) -> None:
        if self.auto_fit:
            self.statusbar.flash_message(red("oops, auto fit is on"))
            return
        # else
        self.statusbar.flash_message("fit width")
        self.imgList.get_curr_img().fit_img_to_window_width()    # type: ignore
        self.redraw()

    def toggle_maximized(self) -> None:
        if self.isFullScreen():
            self.toggle_fullscreen()    # back to normal
            self.toggle_maximized()     # maximize it
            return    # stop recursion, keep it maximized
        #
        if not self.isMaximized():
            self.showMaximized()
            self.statusbar.flash_message("maximized")
        else:
            self.showNormal()
            self.statusbar.flash_message("un-maximized")

    def resizeEvent(self, event) -> None:
        self.redraw()

    def show_logo(self) -> None:
        scale = 0.3
        pm = ImageProperty.to_pixmap(cfg.LOGO, self.cache)[0]
        pm = pm.scaled(int(self.geometry().width() * scale),
                       int(self.geometry().height() * scale),
                       Qt.KeepAspectRatio,
                       Qt.SmoothTransformation)
        self.image_label.setPixmap(pm)
        self.image_label.resize(pm.width(), pm.height())

    def available_width_and_height(self) -> Tuple[int, int]:
        """
        Available width and height for the images.
        """
        width, height = self.img_view.width(), self.img_view.height()
        #
        if not self.menubar.isVisible():
            height += self.menubar.height()
        if not self.statusbar.isVisible():
            height += self.statusbar.height()
        #
        return width, height

    def show_image(self) -> None:
        if self.imgList.get_curr_img() is None:
            return
        #
        pm = self.imgList.get_curr_img().original_img.scaled(self.imgList.get_curr_img().zoom_ratio * self.geometry().width(),    # type: ignore
                                               self.imgList.get_curr_img().zoom_ratio * self.geometry().height(),    # type: ignore
                                               Qt.KeepAspectRatio,
                                               Qt.SmoothTransformation)
        # avoid upscale
        if pm.width() > self.imgList.get_curr_img().original_img.width() or pm.height() > self.imgList.get_curr_img().original_img.height():    # type: ignore
            pm = self.imgList.get_curr_img().original_img    # type: ignore
        self.imgList.get_curr_img().set_zoomed_img(pm)    # type: ignore
        self.redraw()

    def redraw(self) -> None:
        # log.info("redraw")
        #
        if self.imgList.get_curr_img() is None:
            return
        #
        if self.auto_fit:
            self.imgList.get_curr_img().fit_img_to_window()    # type: ignore
        if self.auto_width:
            self.imgList.get_curr_img().fit_img_to_window_width()    # type: ignore
        pm = self.imgList.get_curr_img().zoomed_img    # type: ignore
        self.image_label.setPixmap(pm)    # type: ignore
        self.image_label.resize(pm.width(), pm.height())    # type: ignore
        #
        resolution = "{w} x {h}".format(w=self.imgList.get_curr_img().original_img.width(), h=self.imgList.get_curr_img().original_img.height())    # type: ignore
        # file_size = helper.file_size_fmt(self.imgList.curr_img.file_size) if self.imgList.curr_img.file_size > -1 else ""
        file_size_hr = self.imgList.get_curr_img().get_file_size(human_readable=True)    # type: ignore
        zoom = int(self.imgList.get_curr_img().zoom_ratio * 100)    # type: ignore
        #
        self.info_line.setText(green("{0} of {1}".format(pretty_num(self.imgList.get_curr_img_idx() + 1), pretty_num(len(self.imgList.get_list_of_images())))))
        #
        self.path_line.setText(green(self.imgList.get_curr_img().get_file_name_or_url()))    # type: ignore
        #
        text = green(self.imgList.get_curr_img().get_short_flags())    # type: ignore
        self.flags_line.setText(text)
        #
        self.statusbar.curr_pos_label.setText("{0} of {1}".format(pretty_num(self.imgList.get_curr_img_idx() + 1),
                                                                  pretty_num(len(self.imgList.get_list_of_images()))))
        self.statusbar.file_name_label.setText("{0}    {1}".format(helper.shorten(self.imgList.get_curr_img().get_file_name_or_url()),    # type: ignore
                                                                   file_size_hr))
        self.statusbar.resolution_label.setText(f"{resolution} @ {zoom}%")
        # self.statusbar.memory_label.setText(helper.get_memory_usage())
        self.set_title(self.imgList.get_curr_img().get_file_name_only())    # type: ignore
        if self.imgList.get_curr_img().image_state == ImageProperty.IMAGE_STATE_PROBLEM:    # type: ignore
            self.statusbar.flash_message(red("problem"))

        p = self.img_view.geometry().topRight()
        self.loading_line.move(QPoint(p.x() - 150, p.y() + 10))

        # It's here because of preload. With this the next / prev. image appears and then the preload happens.
        # Without this preload happened and then appeared the image.
        QApplication.processEvents()

    def closeEvent(self, event) -> None:
        if self.image_info_dialog:
            self.image_info_dialog.close()
        #
        if self.important_files_and_folders_dialog:
            self.important_files_and_folders_dialog.close()
        #
        try:
            # maybe it doesn't exist at all (the window was never opened), thus we'd refer to
            # a non-existing attribute -> exception
            self.url_folding_window.close()
        except:
            pass
        #
        try:
            self.simple_scrape.close()
        except:
            pass
        #
        try:
            self.custom_url_list.close()
        except:
            pass
        #
        self.settings.write()
        #
        if self.commit.has_something_to_commit():
            msg = """You have some un-committed changes.
If you quit, you'll lose your changes.

Do you really want to quit?

Tip: hit No and commit your changes.
""".strip()
            reply = QMessageBox.question(self,
                                         'Quit Message',
                                         msg,
                                         QMessageBox.Yes | QMessageBox.No,
                                         QMessageBox.No)

            if reply == QMessageBox.Yes:
                event.accept()
            else:
                event.ignore()
# endclass MainWindow(QMainWindow)


def check_api_keys() -> None:
    if not cfg.TUMBLR_API_KEY:
        log.warning("missing environment variable: TUMBLR_API_KEY")
        log.warning("without this we cannot process tumblr posts")
        log.warning("acquire a tumblr API key (free) and set it as an env. variable")
        log.info(cfg.SEPARATOR)
    else:
        log.info("tumblr API key was found")

    if not cfg.IMGUR_CLIENT_ID or not cfg.IMGUR_CLIENT_SECRET:
        log.warning("missing environment variables: IMGUR_CLIENT_ID and/or IMGUR_CLIENT_SECRET")
        log.warning("without them we cannot process imgur albums / galleries")
        log.warning("acquire an imgur API key (free) and set them as env. variables")
        log.info(cfg.SEPARATOR)
    else:
        log.info("imgur API keys were found")


def main(argv) -> None:
    check_api_keys()
    #
    App = QApplication(argv)
    window = MainWindow(argv)
    window.show()
    sys.exit(App.exec())

##############################################################################

if __name__ == "__main__":
    # log.debug(sys.argv[0])
    # log.debug(sys.executable)
    main(sys.argv)