Skip to content

Commit

Permalink
Jump to last active tab on tab close (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
andreasgriffin authored Nov 2, 2024
1 parent 52fad76 commit a7f2cff
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 16 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,15 @@

## Installation from Git repository


### Ubuntu, Debian, Windows

Install dependencies:

```sh
sudo apt install qt6-tools-dev-tools libqt6*
```



### Ubuntu, Debian, Windows

- Install `poetry` and run `bitcoin_safe`

```sh
Expand Down
21 changes: 11 additions & 10 deletions bitcoin_safe/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@


import logging
from collections import deque
from pathlib import Path

from packaging import version

from bitcoin_safe.gui.qt.unique_deque import UniqueDeque

from .execute_config import DEFAULT_MAINNET

logger = logging.getLogger(__name__)
Expand All @@ -56,6 +57,9 @@
NO_FEE_WARNING_BELOW = 10 # sat/vB


RECENT_WALLET_MAXLEN = 15


class UserConfig(BaseSaveableClass):
known_classes = {**BaseSaveableClass.known_classes, "NetworkConfigs": NetworkConfigs}
VERSION = "0.1.6"
Expand All @@ -79,17 +83,13 @@ def __init__(self) -> None:
self.opened_txlike: Dict[str, List[str]] = {} # network:[serializedtx, serialized psbt]
self.data_dir = appdirs.user_data_dir(self.app_name)
self.is_maximized = False
self.recently_open_wallets: Dict[bdk.Network, deque[str]] = {
network: deque(maxlen=15) for network in bdk.Network
self.recently_open_wallets: Dict[bdk.Network, UniqueDeque[str]] = {
network: UniqueDeque(maxlen=RECENT_WALLET_MAXLEN) for network in bdk.Network
}
self.language_code: Optional[str] = None

def add_recently_open_wallet(self, file_path: str) -> None:
# ensure that the newest open file moves to the top of the queue, but isn't added multiple times
recent_wallets = self.recently_open_wallets[self.network]
if file_path in recent_wallets:
recent_wallets.remove(file_path)
recent_wallets.append(file_path)
self.recently_open_wallets[self.network].append(file_path)

@property
def network_config(self) -> NetworkConfig:
Expand Down Expand Up @@ -122,9 +122,10 @@ def dump(self) -> Dict[str, Any]:
def from_dump(cls, dct: Dict, class_kwargs=None) -> "UserConfig":
super()._from_dump(dct, class_kwargs=class_kwargs)
dct["recently_open_wallets"] = {
bdk.Network._member_map_[k]: deque(v, maxlen=5)
bdk.Network._member_map_[k]: UniqueDeque(v, maxlen=RECENT_WALLET_MAXLEN)
for k, v in dct.get(
"recently_open_wallets", {network.name: deque(maxlen=5) for network in bdk.Network}
"recently_open_wallets",
{network.name: UniqueDeque(maxlen=RECENT_WALLET_MAXLEN) for network in bdk.Network},
).items()
}
# for better portability between computers the saved string is relative to the home folder
Expand Down
6 changes: 4 additions & 2 deletions bitcoin_safe/gui/qt/data_tab_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,19 @@
import logging
from typing import Dict, Generic, Type, TypeVar

from bitcoin_safe.gui.qt.histtabwidget import HistTabWidget

logger = logging.getLogger(__name__)

from typing import Dict, Generic, Type, TypeVar

from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QApplication, QTabWidget, QWidget
from PyQt6.QtWidgets import QApplication, QWidget

T = TypeVar("T")


class DataTabWidget(Generic[T], QTabWidget):
class DataTabWidget(Generic[T], HistTabWidget):
def __init__(self, data_class: Type[T], parent=None) -> None:
super().__init__(parent)
self._data_class = data_class
Expand Down
125 changes: 125 additions & 0 deletions bitcoin_safe/gui/qt/histtabwidget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#
# Bitcoin Safe
# Copyright (C) 2024 Andreas Griffin
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of version 3 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html
#
# 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.


from typing import Optional

from PyQt6.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QTabWidget,
QVBoxLayout,
QWidget,
)

from bitcoin_safe.gui.qt.unique_deque import UniqueDeque


class HistTabWidget(QTabWidget):
"""Stores the closing activation history of the tabs and upon close, activates the last active one.
Args:
QTabWidget: Inherits from QTabWidget.
"""

def __init__(self, parent: Optional[QWidget] = None):
super().__init__(parent)
self._tab_history: UniqueDeque[int] = UniqueDeque(maxlen=100000) # History of activated tab indices
self.currentChanged.connect(self.on_current_changed)

def on_current_changed(self, index: int) -> None:
"""Updates the tab history when the current tab is changed.
Args:
index (int): The index of the newly activated tab.
"""
if index >= 0:
self._tab_history.append(index)

def remove_tab_from_history(self, index: int) -> None:
"""Handles the tab close request, updating history and setting the last active tab.
Args:
index (int): The index of the tab that is being closed.
"""
# Remove the closed tab from history and adjust the indices
if index in self._tab_history:
self._tab_history = UniqueDeque([i for i in self._tab_history if i != index])
self._tab_history = UniqueDeque([i - 1 if i > index else i for i in self._tab_history])

def get_last_active_tab(self) -> int:
if len(self._tab_history) >= 2:
return self._tab_history[-2]
elif len(self._tab_history) >= 1:
return self._tab_history[-1]
return self.currentIndex()

def jump_to_last_active_tab(self) -> None:
"""Sets the current tab to the last active one from history or to the first tab if history is empty."""
index = self.get_last_active_tab()
if index >= 0:
self.setCurrentIndex(index)

def removeTab(self, index: int) -> None:
self.remove_tab_from_history(index)
return super().removeTab(index)


if __name__ == "__main__":

class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.tab_widget = HistTabWidget()
self.tab_widget.setTabsClosable(True)

def remove(index):
self.tab_widget.jump_to_last_active_tab()
self.tab_widget.removeTab(index)

self.tab_widget.tabCloseRequested.connect(remove)
self.tab_widget.currentChanged.connect(
lambda: print(f"on_currentChanged = {self.tab_widget._tab_history}")
)
self.setCentralWidget(self.tab_widget)
# Adding example tabs
for i in range(5):
tab = QWidget()
layout = QVBoxLayout()
label = QLabel(f"Content of tab {i + 1}")
layout.addWidget(label)
tab.setLayout(layout)
self.tab_widget.addTab(tab, f"Tab {i + 1}")

self.setGeometry(300, 300, 400, 300)

app = QApplication([])
window = MainWindow()
window.show()
app.exec()
2 changes: 2 additions & 0 deletions bitcoin_safe/gui/qt/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,8 @@ def remove_all_qt_wallet(self) -> None:
self.remove_qt_wallet(qt_wallet)

def close_tab(self, index: int) -> None:
self.tab_wallets.jump_to_last_active_tab()

# qt_wallet
qt_wallet = self.get_qt_wallet(tab=self.tab_wallets.widget(index))
if qt_wallet:
Expand Down
52 changes: 52 additions & 0 deletions bitcoin_safe/gui/qt/unique_deque.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#
# Bitcoin Safe
# Copyright (C) 2024 Andreas Griffin
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of version 3 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html
#
# 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.


import logging
from collections import deque
from typing import Generic, MutableSequence, TypeVar

_T = TypeVar("_T")

logger = logging.getLogger(__name__)

from collections import deque
from typing import Any


class UniqueDeque(
deque,
MutableSequence[_T],
Generic[_T],
):
def append(self, item: Any) -> None:
# If the item is already in the deque, remove it
while item in self:
self.remove(item)
# Append the new item (deque automatically handles maxlen overflow)
super().append(item)
23 changes: 23 additions & 0 deletions tools/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# SOFTWARE.


import argparse
import datetime
import getpass
import hashlib
Expand Down Expand Up @@ -239,7 +240,29 @@ def get_input_with_default(prompt: str, default: str = "") -> str:
return user_input if user_input else default


RELEASE_INSTRUCTIONS = """
1. Manually create a git tag
2. Build all artifacts
3. Sign all artifacts (build.py --sign)
4. release.py (will also create and publish a pypi package)
5. Manually update the description of the release and click publish
"""


def parse_args() -> argparse.Namespace:

parser = argparse.ArgumentParser(description="Release Bitcoin Safe")
parser.add_argument("--more_help", action="store_true", help=RELEASE_INSTRUCTIONS)

return parser.parse_args()


def main() -> None:
args = parse_args()
if args.more_help:
print(RELEASE_INSTRUCTIONS)
return

get_checkout_main()

print("Running tests before proceeding...")
Expand Down

0 comments on commit a7f2cff

Please sign in to comment.